Skip to content

Commit ad16d96

Browse files
authored
Merge pull request #625 from hilary/slugs_everywhere
generator: slugs everywhere [closes #624]
2 parents bc475a7 + 1d04783 commit ad16d96

18 files changed

+92
-91
lines changed

README.md

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,16 @@ the language, so you're all set.
1212

1313
## Anatomy of an Exercise
1414

15-
The files for an exercise live in `exercises/<exercise_name>` (where
16-
`<exercise_name>` is the slug for the exercise, e.g. `clock` or
17-
`atbash-cipher`). Inside its directory, each exercise has:
15+
The files for an exercise live in `exercises/<slug>`. The slug for an exercise
16+
is a unique nickname composed of a-z (lowercase) and -, e.g. `clock` or
17+
`atbash-cipher`. Inside its directory, each exercise has:
1818

1919
* a test suite, `<exercise_name>_test.rb`
2020
* an example solution, `.meta/solutions/<exercise_name>.rb`
2121

22+
where `<exercise_name>` is the underscored version of the exercise's slug, e.g.,
23+
`clock` or `atbash_cipher`.
24+
2225
If the exercise has a test generator, the directory will also contain:
2326

2427
* `.version`
@@ -47,6 +50,7 @@ rake test:clock
4750

4851
To pass arguments to the test command, like `-p` for example, you can run
4952
the following:
53+
5054
```sh
5155
rake test:clock -- -p
5256
```
@@ -93,13 +97,11 @@ tree -L 1 ~/code/exercism
9397

9498
From within the xruby directory, run the following command:
9599

96-
```
97-
bin/generate <exercise_name>
98-
```
100+
bin/generate <slug>
99101

100102
#### Changing a Generated Exercise
101103

102-
Do not edit `<exercise_name>/<exercise_name>_test.rb`. Any changes you make will
104+
Do not edit `<slug>/<exercise_name>_test.rb`. Any changes you make will
103105
be overwritten when the test suite is regenerated.
104106

105107
There are two reasons why a test suite might change:
@@ -111,7 +113,7 @@ In the first case, the changes need to be made to the `canonical-data.json` file
111113
the exercise, which lives in the x-common repository.
112114

113115
```
114-
../x-common/exercises/<exercise_name>/
116+
../x-common/exercises/<slug>/
115117
├── canonical-data.json
116118
├── description.md
117119
└── metadata.yml
@@ -124,20 +126,20 @@ exercise.
124126
Changes that don't have to do directly with the test inputs and outputs should
125127
be made to the exercise's test case generator, discussed
126128
in [implementing a new generator](#implementing-a-generator), next. Then you
127-
can regenerate the exercise with `bin/generate <exercise_name>`.
129+
can regenerate the exercise with `bin/generate <slug>`.
128130

129131
#### Implementing a Generator
130132

131133
An exercise's test case generator class produces the code that goes inside
132134
the minitest `test_<whatever>` methods. An exercise's generator lives in
133-
`exercises/<exercise_name>/.meta/generator/<exercise_name>_cases.rb`.
135+
`exercises/<slug>/.meta/generator/<exercise_name>_cases.rb`.
134136

135137
The test case generator is a derived class of `ExerciseCase` (in
136138
`lib/generator/exercise_case.rb`). `ExerciseCase` does most of the work of
137139
extracting the canonical data. The derived class wraps the JSON for a single
138140
test case. The default version looks something like this:
139141

140-
```
142+
```ruby
141143
require 'generator/exercise_case'
142144

143145
class <ExerciseName>Case < Generator::ExerciseCase
@@ -150,9 +152,9 @@ class <ExerciseName>Case < Generator::ExerciseCase
150152
end
151153
```
152154

153-
where `<ExerciseName>` is the <exercise_name> slug in CamelCase. This is
154-
important, since the generator script will infer the name of the class from the
155-
<exercise_name> slug.
155+
where `<ExerciseName>` is the CamelCased version of the exercise's slug. This is
156+
important, since the generator script will infer the name of the class from
157+
`<slug>`.
156158

157159
This class must provide the methods used by the test
158160
template. A
@@ -162,8 +164,10 @@ base class provides methods for the default template for everything except
162164
`#workload`.
163165

164166
`#workload` generates the code for the body of a test, including the assertion
165-
and any setup required. The base class provides a variety of assertion and
166-
helper methods. Beyond that, you can implement any helper methods that you need
167+
and any setup required. The base class provides a variety of
168+
[assertion](https://github.com/exercism/xruby/blob/master/lib/generator/exercise_case/assertion.rb) and
169+
[helper](https://github.com/exercism/xruby/blob/master/lib/generator/exercise_case.rb) methods.
170+
Beyond that, you can implement any helper methods that you need
167171
as private methods in your derived class. See below for more information
168172
about [the intention of #workload](#workload-philosophy)
169173

lib/generator/command_line.rb

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,11 @@ def parse(args)
1616
attr_reader :paths
1717

1818
def generators
19-
exercises.map do |exercise_name|
20-
generator(repository(exercise_name))
21-
end
19+
exercises.map { |slug| generator(repository(slug)) }
2220
end
2321

2422
def exercises
25-
@options[:all] ? Files::GeneratorCases.available(paths.track) :
26-
[@options[:exercise_name]]
23+
@options[:all] ? Files::GeneratorCases.available(paths.track) : [@options[:slug]]
2724
end
2825

2926
def generator(repository)
@@ -38,9 +35,9 @@ def freeze?
3835
@options[:freeze] || @options[:all]
3936
end
4037

41-
def repository(exercise_name)
38+
def repository(slug)
4239
LoggingRepository.new(
43-
repository: Repository.new(paths: paths, exercise_name: exercise_name),
40+
repository: Repository.new(paths: paths, slug: slug),
4441
logger: logger
4542
)
4643
end

lib/generator/command_line/generator_optparser.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ class GeneratorOptparser
66
freeze: false,
77
all: false,
88
verbose: false,
9-
exercise_name: nil
9+
slug: nil
1010
}.freeze
1111

1212
attr_reader :options
@@ -27,7 +27,7 @@ def options_valid?
2727
def parse_options
2828
@options = DEFAULT_OPTIONS.dup
2929
option_parser.parse!(@args)
30-
options.tap { |opts| opts[:exercise_name] = @args.shift unless opts[:all] }
30+
options.tap { |opts| opts[:slug] = @args.shift unless opts[:all] }
3131
end
3232

3333
def option_parser
@@ -75,15 +75,15 @@ def validate_options
7575
end
7676

7777
def validate_exercise
78-
return true if options[:exercise_name]
78+
return true if options[:slug]
7979
$stderr.puts "Exercise name required!\n"
8080
$stdout.puts usage
8181
false
8282
end
8383

8484
def validate_cases
85-
return true if available_generators.include?(options[:exercise_name])
86-
$stderr.puts "A generator does not currently exist for #{options[:exercise_name]}!"
85+
return true if available_generators.include?(options[:slug])
86+
$stderr.puts "A generator does not currently exist for #{options[:slug]}!"
8787
false
8888
end
8989
end

lib/generator/files.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,13 @@ def write(content)
4848
end
4949

5050
# An Exercise is used as part of a Repository
51-
# so expects :paths and :exercise_name to be defined.
51+
# so expects :paths and :slug to be defined.
5252
module Exercise
5353
def paths
5454
fail NotImplementedError, 'Should return a Generator::Paths object'
5555
end
5656

57-
def exercise_name
57+
def slug
5858
fail NotImplementedError, 'Should return a String object'
5959
end
6060
end

lib/generator/files/generator_cases.rb

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@ module Files
33
module GeneratorCases
44
class << self
55
def available(track_path)
6-
cases_filepaths(track_path).map { |filepath| exercise_name(filepath) }.sort
6+
cases_filepaths(track_path).map { |filepath| slugify(filepath) }.sort
77
end
88

9-
def class_name(exercise_name)
10-
filename(exercise_name)[0..-2].split('_').map(&:capitalize).join
9+
def class_name(exercise_name_or_slug)
10+
filename(exercise_name_or_slug)[0..-2].split('_').map(&:capitalize).join
1111
end
1212

13-
def source_filepath(track_path, exercise_name)
14-
path = meta_generator_path(track_path, exercise_name)
15-
filename = filename(exercise_name) + '.rb'
13+
def source_filepath(track_path, slug)
14+
path = meta_generator_path(track_path, slug)
15+
filename = filename(slug) + '.rb'
1616
File.join(path, filename)
1717
end
1818

@@ -23,16 +23,16 @@ def cases_filepaths(track_path)
2323
Dir.glob(generator_glob, File::FNM_DOTMATCH)
2424
end
2525

26-
def exercise_name(filepath)
26+
def slugify(filepath)
2727
%r{([^/]*)_cases\.rb$}.match(filepath).captures[0].tr('_', '-')
2828
end
2929

30-
def filename(exercise_name)
31-
"#{exercise_name.tr('-', '_')}_cases"
30+
def filename(exercise_name_or_slug)
31+
"#{exercise_name_or_slug.tr('-', '_')}_cases"
3232
end
3333

34-
def meta_generator_path(track_path, exercise_name)
35-
File.join(track_path, 'exercises', exercise_name, '.meta', 'generator')
34+
def meta_generator_path(track_path, slug)
35+
File.join(track_path, 'exercises', slug, '.meta', 'generator')
3636
end
3737
end
3838
end

lib/generator/files/metadata_files.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def canonical_data
1212
private
1313

1414
def exercise_metadata_path
15-
File.join(paths.metadata, 'exercises', exercise_name)
15+
File.join(paths.metadata, 'exercises', slug)
1616
end
1717
end
1818

lib/generator/files/track_files.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def tests_template
2424
private
2525

2626
def exercise_path
27-
File.join(paths.track, 'exercises', exercise_name)
27+
File.join(paths.track, 'exercises', slug)
2828
end
2929

3030
def meta_path
@@ -36,15 +36,15 @@ def solutions_path
3636
end
3737

3838
def minitest_tests_filename
39-
"#{exercise_name.gsub(/[ -]/, '_')}_test.rb"
39+
"#{slug.gsub(/[ -]/, '_')}_test.rb"
4040
end
4141

4242
def version_filename
4343
'.version'
4444
end
4545

4646
def example_filename
47-
"#{exercise_name}.rb"
47+
"#{slug}.rb"
4848
end
4949

5050
def tests_template_absolute_filename

lib/generator/repository.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ class Repository
66
include Files::MetadataFiles
77
include TemplateValuesFactory
88

9-
def initialize(paths:, exercise_name:)
9+
def initialize(paths:, slug:)
1010
@paths = paths
11-
@exercise_name = exercise_name
11+
@slug = slug
1212
end
1313

14-
attr_reader :paths, :exercise_name
14+
attr_reader :paths, :slug
1515

1616
def version
1717
tests_version.to_i
@@ -56,7 +56,7 @@ def update_example_solution
5656

5757
def create_tests_file
5858
@repository.create_tests_file
59-
@logger.info "Generated #{exercise_name} tests version #{version}"
59+
@logger.info "Generated #{slug} tests version #{version}"
6060
end
6161
end
6262
end

lib/generator/template_values.rb

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ class TemplateValues
66
def initialize(abbreviated_commit_hash:, version:, exercise_name:, test_cases:, canonical_data_version: nil)
77
@abbreviated_commit_hash = abbreviated_commit_hash
88
@version = version
9-
@exercise_name = exercise_name ? exercise_name.tr('-_', '_') : ''
9+
@exercise_name = exercise_name
1010
@test_cases = test_cases
1111
@canonical_data_version = canonical_data_version
1212
end
@@ -16,7 +16,7 @@ def get_binding
1616
end
1717

1818
def exercise_name_camel
19-
exercise_name.split(/[-_]/).map(&:capitalize).join
19+
exercise_name.split('_').map(&:capitalize).join
2020
end
2121
end
2222

@@ -26,26 +26,30 @@ def template_values
2626
abbreviated_commit_hash: canonical_data.abbreviated_commit_hash,
2727
canonical_data_version: canonical_data.version,
2828
version: version,
29-
exercise_name: exercise_name,
29+
exercise_name: slug_underscore,
3030
test_cases: extract
3131
)
3232
end
3333

3434
private
3535

36+
def slug_underscore
37+
slug ? slug.tr('-_', '_') : ''
38+
end
39+
3640
def extract
3741
load cases_load_name
3842
extractor.cases(canonical_data.to_s)
3943
end
4044

4145
def extractor
4246
CaseValues::Extractor.new(
43-
case_class: Object.const_get(Files::GeneratorCases.class_name(exercise_name))
47+
case_class: Object.const_get(Files::GeneratorCases.class_name(slug))
4448
)
4549
end
4650

4751
def cases_load_name
48-
Files::GeneratorCases.source_filepath(paths.track, exercise_name)
52+
Files::GeneratorCases.source_filepath(paths.track, slug)
4953
end
5054
end
5155
end

lib/tasks/exercise.rb

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
class Exercise
22
class << self
33
def all
4-
exercise_names.map { |e| new(e) }
4+
slugs.map { |e| new(e) }
55
end
66

77
private
88

9-
def exercise_names
9+
def slugs
1010
FileList['exercises/*'].pathmap('%f').exclude('TRACK_HINTS.md')
1111
end
1212
end
@@ -23,7 +23,7 @@ def directory
2323
end
2424

2525
def example_file
26-
example_filename
26+
File.join('.meta', 'solutions', "#{name}.rb")
2727
end
2828

2929
def testable_example_file
@@ -36,10 +36,6 @@ def test_file
3636

3737
private
3838

39-
def example_filename
40-
File.join('.meta', 'solutions', "#{name}.rb")
41-
end
42-
4339
def base_file_name
4440
@_base_file_name ||= name.tr('-', '_')
4541
end

0 commit comments

Comments
 (0)