diff --git a/track/config.go b/track/config.go index e9a50ce..a622fd0 100644 --- a/track/config.go +++ b/track/config.go @@ -100,6 +100,36 @@ func (cfg *Config) ToJSON() ([]byte, error) { return json.MarshalIndent(&cfg, "", " ") } +// MarshalJSON marshals a exercise metadata to JSON, +// only marshalling certain fields if the exercise is deprecated. +func (e *ExerciseMetadata) MarshalJSON() ([]byte, error) { + if e.IsDeprecated { + // Only marshal Slug, UUID, Deprecated. + return json.Marshal(&struct { + Slug string `json:"slug"` + UUID string `json:"uuid"` + IsDeprecated bool `json:"deprecated"` + }{ + Slug: e.Slug, + UUID: e.UUID, + IsDeprecated: true, + }) + } else { + // Use the default marshalling. + // We can't embed ExerciseMetadata into an anonymous struct, + // since that will cause infinite recursion on this MarshalJSON, + // But we can embed a new typedef of it, + // since the typedef does not have this MarshalJSON function. + // Technique discovered from http://choly.ca/post/go-json-marshalling/ + type ExerciseMetadataJ ExerciseMetadata + return json.Marshal(&struct { + *ExerciseMetadataJ + }{ + ExerciseMetadataJ: (*ExerciseMetadataJ)(e), + }) + } +} + func normalizeTopic(t string) string { s := strings.ToLower(t) s = rgxFunkyChars.ReplaceAllString(s, "") diff --git a/track/config_test.go b/track/config_test.go index 10bee94..bae808c 100644 --- a/track/config_test.go +++ b/track/config_test.go @@ -105,6 +105,87 @@ func TestMarshalingNormalizesTopics(t *testing.T) { assert.Equal(t, []string{"apple", "fig", "honeydew_melon"}, dstCfg.Exercises[0].Topics) } +var allExercisesKeys = []string{ + "slug", + "uuid", +} + +var activeExercisesKeys = []string{ + "core", + // not auto_approve, since it is omitempty + "unlocked_by", + "difficulty", + "topics", +} + +func TestMarshalActive(t *testing.T) { + srcCfg := Config{ + Exercises: []ExerciseMetadata{ + ExerciseMetadata{ + Slug: "active", + Topics: []string{"topic_one", "topic_two"}, + IsDeprecated: false, + }, + }, + } + + dst, err := srcCfg.ToJSON() + if err != nil { + t.Fatal(err) + } + + // contains all the keys we expect it to contain: + for _, key := range append(allExercisesKeys, activeExercisesKeys...) { + assert.True(t, strings.Contains(string(dst), key), "expected JSON representation to contain %q, but it didn't: %s", key, string(dst)) + } + + var dstCfg Config + if err := json.NewDecoder(bytes.NewReader(dst)).Decode(&dstCfg); err != nil { + t.Fatal(err) + } + + // survived an encode -> decode: + assert.Equal(t, "active", dstCfg.Exercises[0].Slug) + assert.False(t, dstCfg.Exercises[0].IsDeprecated) + assert.Equal(t, []string{"topic_one", "topic_two"}, dstCfg.Exercises[0].Topics) +} + +func TestMarshalDeprecated(t *testing.T) { + srcCfg := Config{ + Exercises: []ExerciseMetadata{ + ExerciseMetadata{ + Slug: "deprecated", + Topics: []string{"topic_one", "topic_two"}, + IsDeprecated: true, + }, + }, + } + + dst, err := srcCfg.ToJSON() + if err != nil { + t.Fatal(err) + } + + // contains the keys we want, and not the ones we don't: + for _, key := range append(allExercisesKeys, "deprecated") { + assert.True(t, strings.Contains(string(dst), key), "expected JSON representation to contain %q, but it didn't: %s", key, string(dst)) + } + for _, key := range activeExercisesKeys { + assert.False(t, strings.Contains(string(dst), key), "expected JSON representation NOT to contain %q, but it did: %s", key, string(dst)) + } + + var dstCfg Config + if err := json.NewDecoder(bytes.NewReader(dst)).Decode(&dstCfg); err != nil { + t.Fatal(err) + } + + // survived an encode -> decode: + assert.Equal(t, "deprecated", dstCfg.Exercises[0].Slug) + assert.True(t, dstCfg.Exercises[0].IsDeprecated) + // Note that since topics was never marshalled, it should be nil. + assert.Nil(t, dstCfg.Exercises[0].Topics) +} + func TestSemanticsOfMissingTopics(t *testing.T) { src := ` {