github.com/exercism/configlet@v3.9.3-0.20200318193232-c70be6269e71+incompatible/track/problem_specification.go (about) 1 package track 2 3 import ( 4 "fmt" 5 "io/ioutil" 6 "os" 7 "path/filepath" 8 "strings" 9 10 yaml "gopkg.in/yaml.v2" 11 ) 12 13 const ( 14 // ProblemSpecificationsDir is the default name of the cloned problem-specifications repository. 15 ProblemSpecificationsDir = "problem-specifications" 16 filenameDescription = "description.md" 17 filenameMetadata = "metadata.yml" 18 ) 19 20 var ( 21 // ProblemSpecificationsPath is the location of the cloned problem-specifications repository. 22 ProblemSpecificationsPath string 23 ) 24 25 // ProblemSpecification contains metadata describing an exercise. 26 type ProblemSpecification struct { 27 Slug string 28 Description string 29 Title string `yaml:"title"` 30 Source string `yaml:"source"` 31 SourceURL string `yaml:"source_url"` 32 root string 33 trackID string 34 metadataPath string 35 descriptionPath string 36 } 37 38 // NewProblemSpecification loads the specification from files on disk. 39 // It will default to a custom specification, falling back to the generic specification 40 // if no custom one is found. 41 func NewProblemSpecification(root, trackID, slug string) (*ProblemSpecification, error) { 42 spec := &ProblemSpecification{ 43 root: root, 44 trackID: trackID, 45 Slug: slug, 46 } 47 spec.Title = spec.titleCasedSlug() 48 49 if err := spec.loadMetadata(); err != nil { 50 return nil, err 51 } 52 53 if err := spec.loadDescription(); err != nil { 54 return nil, err 55 } 56 57 return spec, nil 58 } 59 60 // Name is a readable version of the slug. 61 func (spec *ProblemSpecification) Name() string { 62 if spec.Title == "" { 63 spec.Title = spec.titleCasedSlug() 64 } 65 return spec.Title 66 } 67 68 // MixedCaseName returns the name with all spaces removed. 69 func (spec *ProblemSpecification) MixedCaseName() string { 70 return strings.Replace(spec.titleCasedSlug(), " ", "", -1) 71 } 72 73 // SnakeCaseName converts the slug to snake case. 74 func (spec *ProblemSpecification) SnakeCaseName() string { 75 return strings.Replace(spec.Slug, "-", "_", -1) 76 } 77 78 // Credits are a markdown-formatted version of the source of the exercise. 79 func (spec *ProblemSpecification) Credits() string { 80 if spec.SourceURL == "" { 81 return spec.Source 82 } 83 if spec.Source == "" { 84 return fmt.Sprintf("[%s](%s)", spec.SourceURL, spec.SourceURL) 85 } 86 return fmt.Sprintf("%s [%s](%s)", spec.Source, spec.SourceURL, spec.SourceURL) 87 } 88 89 func (spec *ProblemSpecification) titleCasedSlug() string { 90 return strings.Title(strings.Join(strings.Split(spec.Slug, "-"), " ")) 91 } 92 93 func (spec *ProblemSpecification) loadMetadata() error { 94 metadataPath := filepath.Join(spec.customPath(), filenameMetadata) 95 if _, err := os.Stat(metadataPath); os.IsNotExist(err) { 96 metadataPath = filepath.Join(spec.sharedPath(), filenameMetadata) 97 } 98 spec.metadataPath = metadataPath 99 100 b, err := ioutil.ReadFile(spec.metadataPath) 101 if err != nil { 102 return err 103 } 104 return yaml.Unmarshal(b, &spec) 105 } 106 107 func (spec *ProblemSpecification) loadDescription() error { 108 descriptionPath := filepath.Join(spec.customPath(), filenameDescription) 109 if _, err := os.Stat(descriptionPath); os.IsNotExist(err) { 110 descriptionPath = filepath.Join(spec.sharedPath(), filenameDescription) 111 } 112 spec.descriptionPath = descriptionPath 113 114 b, err := ioutil.ReadFile(spec.descriptionPath) 115 if err != nil { 116 return err 117 } 118 spec.Description = string(b) 119 120 return nil 121 } 122 123 func (spec *ProblemSpecification) sharedPath() string { 124 if ProblemSpecificationsPath != "" { 125 return filepath.Join(ProblemSpecificationsPath, "exercises", spec.Slug) 126 } 127 return filepath.Join(spec.root, ProblemSpecificationsDir, "exercises", spec.Slug) 128 } 129 130 func (spec *ProblemSpecification) customPath() string { 131 return filepath.Join(spec.root, spec.trackID, "exercises", spec.Slug, ".meta") 132 }