reform 2.3.2 → 2.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +17 -0
  3. data/.gitignore +1 -1
  4. data/CHANGES.md +23 -0
  5. data/Gemfile +1 -1
  6. data/LICENSE.txt +1 -1
  7. data/README.md +5 -5
  8. data/Rakefile +1 -12
  9. data/lib/reform/contract/validate.rb +1 -1
  10. data/lib/reform/form/dry.rb +47 -9
  11. data/lib/reform/form/populator.rb +13 -3
  12. data/lib/reform/form/prepopulate.rb +1 -1
  13. data/lib/reform/form/validate.rb +3 -3
  14. data/lib/reform/validation/groups.rb +0 -1
  15. data/lib/reform/version.rb +1 -1
  16. data/reform.gemspec +2 -2
  17. data/test/call_test.rb +23 -0
  18. data/test/changed_test.rb +6 -6
  19. data/test/coercion_test.rb +17 -17
  20. data/test/{composition_new_api.rb → composition_test.rb} +27 -28
  21. data/test/{contract_new_api.rb → contract_test.rb} +8 -8
  22. data/test/default_test.rb +2 -2
  23. data/test/deserialize_test.rb +8 -8
  24. data/test/docs/validation_test.rb +134 -0
  25. data/test/{errors_new_api.rb → errors_test.rb} +41 -41
  26. data/test/feature_test.rb +2 -2
  27. data/test/fixtures/dry_error_messages.yml +64 -54
  28. data/test/{form_option_new_api.rb → form_option_test.rb} +1 -1
  29. data/test/{form_new_api.rb → form_test.rb} +3 -3
  30. data/test/from_test.rb +10 -10
  31. data/test/{inherit_new_api.rb → inherit_test.rb} +17 -17
  32. data/test/{module_new_api.rb → module_test.rb} +10 -10
  33. data/test/parse_option_test.rb +7 -7
  34. data/test/parse_pipeline_test.rb +1 -1
  35. data/test/{populate_new_api.rb → populate_test.rb} +136 -53
  36. data/test/populator_skip_test.rb +2 -2
  37. data/test/prepopulator_test.rb +16 -16
  38. data/test/read_only_test.rb +2 -2
  39. data/test/readable_test.rb +3 -3
  40. data/test/{reform_new_api.rb → reform_test.rb} +19 -19
  41. data/test/{save_new_api.rb → save_test.rb} +4 -4
  42. data/test/setup_test.rb +9 -9
  43. data/test/{skip_if_new_api.rb → skip_if_test.rb} +12 -12
  44. data/test/skip_setter_and_getter_test.rb +6 -6
  45. data/test/test_helper.rb +5 -6
  46. data/test/{validate_new_api.rb → validate_test.rb} +65 -78
  47. data/test/validation/{dry_validation_new_api.rb → dry_validation_test.rb} +128 -128
  48. data/test/validation/result_test.rb +14 -14
  49. data/test/virtual_test.rb +7 -7
  50. data/test/writeable_test.rb +8 -8
  51. metadata +42 -75
  52. data/.travis.yml +0 -16
  53. data/Appraisals +0 -8
  54. data/gemfiles/0.13.0.gemfile +0 -8
  55. data/gemfiles/1.5.0.gemfile +0 -9
  56. data/lib/reform/form/dry/new_api.rb +0 -47
  57. data/lib/reform/form/dry/old_api.rb +0 -61
  58. data/test/call_new_api.rb +0 -23
  59. data/test/call_old_api.rb +0 -23
  60. data/test/composition_old_api.rb +0 -184
  61. data/test/contract_old_api.rb +0 -77
  62. data/test/errors_old_api.rb +0 -230
  63. data/test/fixtures/dry_new_api_error_messages.yml +0 -104
  64. data/test/form_old_api.rb +0 -57
  65. data/test/form_option_old_api.rb +0 -24
  66. data/test/inherit_old_api.rb +0 -105
  67. data/test/module_old_api.rb +0 -146
  68. data/test/populate_old_api.rb +0 -304
  69. data/test/reform_old_api.rb +0 -202
  70. data/test/save_old_api.rb +0 -101
  71. data/test/skip_if_old_api.rb +0 -92
  72. data/test/validate_old_api.rb +0 -410
  73. data/test/validation/dry_validation_old_api.rb +0 -772
@@ -1,77 +0,0 @@
1
- require "test_helper"
2
-
3
- class ContractTest < MiniTest::Spec
4
- Song = Struct.new(:title, :album, :composer)
5
- Album = Struct.new(:name, :duration, :songs, :artist)
6
- Artist = Struct.new(:name)
7
-
8
- class ArtistForm < TestForm
9
- property :name
10
- end
11
-
12
- class AlbumForm < TestContract
13
- property :name
14
-
15
- properties :duration
16
- properties :year, :style, readable: false
17
-
18
- validation do
19
- required(:name).filled
20
- end
21
-
22
- collection :songs do
23
- property :title
24
- validation do
25
- required(:title).filled
26
- end
27
-
28
- property :composer do
29
- property :name
30
- validation do
31
- required(:name).filled
32
- end
33
- end
34
- end
35
-
36
- property :artist, form: ArtistForm
37
- end
38
-
39
- let(:song) { Song.new("Broken") }
40
- let(:song_with_composer) { Song.new("Resist Stance", nil, composer) }
41
- let(:composer) { Artist.new("Greg Graffin") }
42
- let(:artist) { Artist.new("Bad Religion") }
43
- let(:album) { Album.new("The Dissent Of Man", 123, [song, song_with_composer], artist) }
44
-
45
- let(:form) { AlbumForm.new(album) }
46
-
47
- # accept `property form: SongForm`.
48
- it do
49
- form.artist.must_be_instance_of ArtistForm
50
- end
51
-
52
- describe ".properties" do
53
- it "defines a property when called with one argument" do
54
- form.must_respond_to :duration
55
- end
56
-
57
- it "defines several properties when called with multiple arguments" do
58
- form.must_respond_to :year
59
- form.must_respond_to :style
60
- end
61
-
62
- it "passes options to each property when options are provided" do
63
- readable = AlbumForm.new(album).options_for(:style)[:readable]
64
- readable.must_equal false
65
- end
66
-
67
- it "returns the list of defined properties" do
68
- returned_value = AlbumForm.properties(:hello, :world, virtual: true)
69
- returned_value.must_equal %i[hello world]
70
- end
71
- end
72
-
73
- describe "#options_for" do
74
- it { AlbumForm.options_for(:name).extend(Declarative::Inspect).inspect.must_equal "#<Disposable::Twin::Definition: @options={:private_name=>:name, :name=>\"name\"}>" }
75
- it { AlbumForm.new(album).options_for(:name).extend(Declarative::Inspect).inspect.must_equal "#<Disposable::Twin::Definition: @options={:private_name=>:name, :name=>\"name\"}>" }
76
- end
77
- end
@@ -1,230 +0,0 @@
1
- require "test_helper"
2
-
3
- class ErrorsTest < MiniTest::Spec
4
- class AlbumForm < TestForm
5
- property :title
6
- validation do
7
- required(:title).filled
8
- end
9
-
10
- property :artists, default: []
11
- property :producer do
12
- property :name
13
- end
14
-
15
- property :hit do
16
- property :title
17
- validation do
18
- required(:title).filled
19
- end
20
- end
21
-
22
- collection :songs do
23
- property :title
24
- validation do
25
- required(:title).filled
26
- end
27
- end
28
-
29
- property :band do # yepp, people do crazy stuff like that.
30
- property :name
31
- property :label do
32
- property :name
33
- validation do
34
- required(:name).filled
35
- end
36
- end
37
- # TODO: make band a required object.
38
-
39
- validation do
40
- configure do
41
- config.messages_file = "test/fixtures/dry_error_messages.yml"
42
-
43
- def good_musical_taste?(value)
44
- value != "Nickelback"
45
- end
46
- end
47
-
48
- required(:name).filled(:good_musical_taste?)
49
- end
50
- end
51
-
52
- validation do
53
- required(:title).filled
54
- required(:artists).each(:str?)
55
- required(:producer).schema do
56
- required(:name).filled
57
- end
58
- end
59
- end
60
-
61
- let(:album_title) { "Blackhawks Over Los Angeles" }
62
- let(:album) do
63
- OpenStruct.new(
64
- title: album_title,
65
- hit: song,
66
- songs: songs, # TODO: document this requirement,
67
- band: Struct.new(:name, :label).new("Epitaph", OpenStruct.new),
68
- producer: Struct.new(:name).new("Sun Records")
69
- )
70
- end
71
- let(:song) { OpenStruct.new(title: "Downtown") }
72
- let(:songs) { [song = OpenStruct.new(title: "Calling"), song] }
73
- let(:form) { AlbumForm.new(album) }
74
-
75
- describe "#validate with invalid array property" do
76
- it do
77
- form.validate(
78
- title: "Swimming Pool - EP",
79
- band: {
80
- name: "Marie Madeleine",
81
- label: {name: "Ekler'o'shocK"}
82
- },
83
- artists: [42, "Good Charlotte", 43]
84
- ).must_equal false
85
- form.errors.messages.must_equal(artists: {0 => ["must be a string"], 2 => ["must be a string"]})
86
- form.errors.size.must_equal(1)
87
- end
88
- end
89
-
90
- describe "#errors without #validate" do
91
- it do
92
- form.errors.size.must_equal 0
93
- end
94
- end
95
-
96
- describe "blank everywhere" do
97
- before do
98
- form.validate(
99
- "hit" => {"title" => ""},
100
- "title" => "",
101
- "songs" => [{"title" => ""}, {"title" => ""}],
102
- "producer" => {"name" => ""}
103
- )
104
- end
105
-
106
- it do
107
- form.errors.messages.must_equal(
108
- title: ["must be filled"],
109
- "hit.title": ["must be filled"],
110
- "songs.title": ["must be filled"],
111
- "band.label.name": ["must be filled"],
112
- "producer.name": ["must be filled"]
113
- )
114
- end
115
-
116
- # it do
117
- # form.errors.must_equal({:title => ["must be filled"]})
118
- # TODO: this should only contain local errors?
119
- # end
120
-
121
- # nested forms keep their own Errors:
122
- it { form.producer.errors.messages.must_equal(name: ["must be filled"]) }
123
- it { form.hit.errors.messages.must_equal(title: ["must be filled"]) }
124
- it { form.songs[0].errors.messages.must_equal(title: ["must be filled"]) }
125
-
126
- it do
127
- form.errors.messages.must_equal(
128
- title: ["must be filled"],
129
- "hit.title": ["must be filled"],
130
- "songs.title": ["must be filled"],
131
- "band.label.name": ["must be filled"],
132
- "producer.name": ["must be filled"]
133
- )
134
- form.errors.size.must_equal(5)
135
- end
136
- end
137
-
138
- describe "#validate with main form invalid" do
139
- it do
140
- form.validate("title" => "", "band" => {"label" => {name: "Fat Wreck"}}, "producer" => nil).must_equal false
141
- form.errors.messages.must_equal(title: ["must be filled"], producer: ["must be a hash"])
142
- form.errors.size.must_equal(2)
143
- end
144
- end
145
-
146
- describe "#validate with middle nested form invalid" do
147
- before { @result = form.validate("hit" => {"title" => ""}, "band" => {"label" => {name: "Fat Wreck"}}) }
148
-
149
- it { @result.must_equal false }
150
- it { form.errors.messages.must_equal("hit.title": ["must be filled"]) }
151
- it { form.errors.size.must_equal(1) }
152
- end
153
-
154
- describe "#validate with collection form invalid" do
155
- before { @result = form.validate("songs" => [{"title" => ""}], "band" => {"label" => {name: "Fat Wreck"}}) }
156
-
157
- it { @result.must_equal false }
158
- it { form.errors.messages.must_equal("songs.title": ["must be filled"]) }
159
- it { form.errors.size.must_equal(1) }
160
- end
161
-
162
- describe "#validate with collection and 2-level-nested invalid" do
163
- before { @result = form.validate("songs" => [{"title" => ""}], "band" => {"label" => {}}) }
164
-
165
- it { @result.must_equal false }
166
- it { form.errors.messages.must_equal("songs.title": ["must be filled"], "band.label.name": ["must be filled"]) }
167
- it { form.errors.size.must_equal(2) }
168
- end
169
-
170
- describe "#validate with nested form using :base invalid" do
171
- it do
172
- result = form.validate("songs" => [{"title" => "Someday"}], "band" => {"name" => "Nickelback", "label" => {"name" => "Roadrunner Records"}})
173
- result.must_equal false
174
- form.errors.messages.must_equal("band.name": ["you're a bad person"])
175
- form.errors.size.must_equal(1)
176
- end
177
- end
178
-
179
- describe "#add" do
180
- let(:album_title) { nil }
181
- it do
182
- form.errors.add(:before, "validate")
183
- form.errors.add(:before, "validate 2")
184
- form.errors.add(:title, "before validate")
185
- result = form.validate("songs" => [{"title" => "Someday"}], "band" => {"name" => "Nickelback", "label" => {"name" => "Roadrunner Records"}})
186
- result.must_equal false
187
- form.errors.messages.must_equal(before: ["validate", "validate 2"], title: ["before validate", "must be filled"], "band.name": ["you're a bad person"])
188
- # add a new custom error
189
- form.errors.add(:policy, "error_text")
190
- form.errors.messages.must_equal(before: ["validate", "validate 2"], title: ["before validate", "must be filled"], "band.name": ["you're a bad person"], policy: ["error_text"])
191
- # does not duplicate errors
192
- form.errors.add(:title, "must be filled")
193
- form.errors.messages.must_equal(before: ["validate", "validate 2"], title: ["before validate", "must be filled"], "band.name": ["you're a bad person"], policy: ["error_text"])
194
- # merge existing errors
195
- form.errors.add(:policy, "another error")
196
- form.errors.messages.must_equal(before: ["validate", "validate 2"], title: ["before validate", "must be filled"], "band.name": ["you're a bad person"], policy: ["error_text", "another error"])
197
- end
198
- end
199
-
200
- describe "correct #validate" do
201
- before do
202
- @result = form.validate(
203
- "hit" => {"title" => "Sacrifice"},
204
- "title" => "Second Heat",
205
- "songs" => [{"title" => "Heart Of A Lion"}],
206
- "band" => {"label" => {name: "Fat Wreck"}}
207
- )
208
- end
209
-
210
- it { @result.must_equal true }
211
- it { form.hit.title.must_equal "Sacrifice" }
212
- it { form.title.must_equal "Second Heat" }
213
- it { form.songs.first.title.must_equal "Heart Of A Lion" }
214
- it do
215
- skip "WE DON'T NEED COUNT AND EMPTY? ON THE CORE ERRORS OBJECT"
216
- form.errors.size.must_equal(0)
217
- form.errors.empty?.must_equal(true)
218
- end
219
- end
220
-
221
- describe "Errors#to_s" do
222
- before { form.validate("songs" => [{"title" => ""}], "band" => {"label" => {}}) }
223
-
224
- # to_s is aliased to messages
225
- it {
226
- skip "why do we need Errors#to_s ?"
227
- form.errors.to_s.must_equal "{:\"songs.title\"=>[\"must be filled\"], :\"band.label.name\"=>[\"must be filled\"]}"
228
- }
229
- end
230
- end
@@ -1,104 +0,0 @@
1
- en:
2
- dry_validation:
3
- errors:
4
- array?: "must be an array"
5
-
6
- empty?: "must be empty"
7
-
8
- excludes?: "must not include %{value}"
9
-
10
- excluded_from?:
11
- arg:
12
- default: "must not be one of: %{list}"
13
- range: "must not be one of: %{list_left} - %{list_right}"
14
-
15
- eql?: "must be equal to %{left}"
16
-
17
- not_eql?: "must not be equal to %{left}"
18
-
19
- filled?: "must be filled"
20
-
21
- format?: "is in invalid format"
22
-
23
- number?: "must be a number"
24
-
25
- odd?: "must be odd"
26
-
27
- even?: "must be even"
28
-
29
- gt?: "must be greater than %{num}"
30
-
31
- gteq?: "must be greater than or equal to %{num}"
32
-
33
- hash?: "must be a hash"
34
-
35
- included_in?:
36
- arg:
37
- default: "must be one of: %{list}"
38
- range: "must be one of: %{list_left} - %{list_right}"
39
-
40
- includes?: "must include %{value}"
41
-
42
- bool?: "must be boolean"
43
-
44
- true?: "must be true"
45
-
46
- false?: "must be false"
47
-
48
- int?: "must be an integer"
49
-
50
- float?: "must be a float"
51
-
52
- decimal?: "must be a decimal"
53
-
54
- date?: "must be a date"
55
-
56
- date_time?: "must be a date time"
57
-
58
- time?: "must be a time"
59
-
60
- key?: "is missing"
61
-
62
- attr?: "is missing"
63
-
64
- lt?: "must be less than %{num}"
65
-
66
- lteq?: "must be less than or equal to %{num}"
67
-
68
- max_size?: "size cannot be greater than %{num}"
69
-
70
- min_size?: "size cannot be less than %{num}"
71
-
72
- none?: "cannot be defined"
73
-
74
- str?: "must be a string"
75
-
76
- type?: "must be %{type}"
77
-
78
- size?:
79
- arg:
80
- default: "size must be %{size}"
81
- range: "size must be within %{size_left} - %{size_right}"
82
-
83
- value:
84
- string:
85
- arg:
86
- default: "length must be %{size}"
87
- range: "length must be within %{size_left} - %{size_right}"
88
-
89
-
90
-
91
- rules:
92
- name:
93
- good_musical_taste?: "you're a bad person"
94
- title:
95
- good_musical_taste?: "you're a bad person"
96
- songs:
97
- a_song?: "must have at least one enabled song"
98
- artist:
99
- with_last_name?: "must have last name"
100
-
101
- de:
102
- dry_validation:
103
- errors:
104
- filled?: "muss abgefüllt sein"
data/test/form_old_api.rb DELETED
@@ -1,57 +0,0 @@
1
- require "test_helper"
2
-
3
- class FormTest < MiniTest::Spec
4
- Artist = Struct.new(:name)
5
-
6
- class AlbumForm < TestForm
7
- property :title
8
-
9
- property :hit do
10
- property :title
11
- end
12
-
13
- collection :songs do
14
- property :title
15
- end
16
-
17
- property :band do # yepp, people do crazy stuff like that.
18
- property :label do
19
- property :name
20
- end
21
- end
22
- end
23
-
24
- describe "::dup" do
25
- let(:cloned) { AlbumForm.clone }
26
-
27
- # #dup is called in Op.inheritable_attr(:contract_class), it must be subclass of the original one.
28
- it { cloned.wont_equal AlbumForm }
29
- it { AlbumForm.definitions.wont_equal cloned.definitions }
30
-
31
- it do
32
- # currently, forms need a name for validation, even without AM.
33
- cloned.singleton_class.class_eval do
34
- def name
35
- "Album"
36
- end
37
- end
38
-
39
- cloned.validation do
40
- required(:title).filled
41
- end
42
-
43
- cloned.new(OpenStruct.new).validate({})
44
- end
45
- end
46
-
47
- describe "#initialize" do
48
- class ArtistForm < TestForm
49
- property :name
50
- property :current_user, virtual: true
51
- end
52
-
53
- it "allows injecting :virtual options" do
54
- ArtistForm.new(Artist.new, current_user: Object).current_user.must_equal Object
55
- end
56
- end
57
- end
@@ -1,24 +0,0 @@
1
- require "test_helper"
2
-
3
- class FormOptionTest < MiniTest::Spec
4
- Song = Struct.new(:title)
5
- Album = Struct.new(:song)
6
-
7
- class SongForm < TestForm
8
- property :title
9
- validation do
10
- required(:title).filled
11
- end
12
- end
13
-
14
- class AlbumForm < TestForm
15
- property :song, form: SongForm
16
- end
17
-
18
- it do
19
- form = AlbumForm.new(Album.new(Song.new("When It Comes To You")))
20
- form.song.title.must_equal "When It Comes To You"
21
-
22
- form.validate(song: {title: "Run For Cover"})
23
- end
24
- end
@@ -1,105 +0,0 @@
1
- require "test_helper"
2
- require "representable/json"
3
-
4
- class InheritTest < BaseTest
5
- Populator = Reform::Form::Populator
6
-
7
- class SkipParse
8
- include Uber::Callable
9
- def call(*_args)
10
- false
11
- end
12
- end
13
-
14
- class AlbumForm < TestForm
15
- property :title, deserializer: {instance: "Instance"}, skip_if: "skip_if in AlbumForm" # allow direct configuration of :deserializer.
16
-
17
- property :hit, populate_if_empty: ->(*) { Song.new } do
18
- property :title
19
- validation do
20
- required(:title).filled
21
- end
22
- end
23
-
24
- collection :songs, populate_if_empty: -> {}, skip_if: :all_blank do
25
- property :title
26
- end
27
-
28
- property :band, populate_if_empty: -> {} do
29
- def band_id
30
- 1
31
- end
32
- end
33
- end
34
-
35
- class CompilationForm < AlbumForm
36
- property :title, inherit: true, skip_if: "skip_if from CompilationForm"
37
- property :hit, inherit: true, populate_if_empty: ->(*) { Song.new }, skip_if: SkipParse.new do
38
- property :rating
39
- validation do
40
- required(:rating).filled
41
- end
42
- end
43
-
44
- # NO collection here, this is entirely inherited.
45
-
46
- property :band, inherit: true do # inherit everything, but explicitely.
47
- end
48
- end
49
-
50
- let(:album) { Album.new(nil, Song.new, [], Band.new) }
51
- subject { CompilationForm.new(album) }
52
-
53
- it do
54
- subject.validate("hit" => {"title" => "LA Drone", "rating" => 10})
55
- subject.hit.title.must_equal "LA Drone"
56
- subject.hit.rating.must_equal 10
57
- subject.errors.messages.must_equal({})
58
- end
59
-
60
- it do
61
- subject.validate({})
62
- assert_nil subject.model.hit.title
63
- assert_nil subject.model.hit.rating
64
- subject.errors.messages.must_equal("hit.title": ["must be filled"], "hit.rating": ["must be filled"])
65
- end
66
-
67
- it "xxx" do
68
- # sub hashes like :deserializer must be properly cloned when inheriting.
69
- AlbumForm.options_for(:title)[:deserializer].object_id.wont_equal CompilationForm.options_for(:title)[:deserializer].object_id
70
-
71
- # don't overwrite direct deserializer: {} configuration.
72
- AlbumForm.options_for(:title)[:internal_populator].must_be_instance_of Reform::Form::Populator::Sync
73
- AlbumForm.options_for(:title)[:deserializer][:skip_parse].must_equal "skip_if in AlbumForm"
74
-
75
- # AlbumForm.options_for(:hit)[:internal_populator].inspect.must_match /Reform::Form::Populator:.+ @user_proc="Populator"/
76
- # AlbumForm.options_for(:hit)[:deserializer][:instance].inspect.must_be_instance_with Reform::Form::Populator, user_proc: "Populator"
77
-
78
- AlbumForm.options_for(:songs)[:internal_populator].must_be_instance_of Reform::Form::Populator::IfEmpty
79
- AlbumForm.options_for(:songs)[:deserializer][:skip_parse].must_be_instance_of Reform::Form::Validate::Skip::AllBlank
80
-
81
- AlbumForm.options_for(:band)[:internal_populator].must_be_instance_of Reform::Form::Populator::IfEmpty
82
-
83
- CompilationForm.options_for(:title)[:deserializer][:skip_parse].must_equal "skip_if from CompilationForm"
84
- # pp CompilationForm.options_for(:songs)
85
- CompilationForm.options_for(:songs)[:internal_populator].must_be_instance_of Reform::Form::Populator::IfEmpty
86
-
87
- CompilationForm.options_for(:band)[:internal_populator].must_be_instance_of Reform::Form::Populator::IfEmpty
88
-
89
- # completely overwrite inherited.
90
- CompilationForm.options_for(:hit)[:deserializer][:skip_parse].must_be_instance_of SkipParse
91
-
92
- # inherit: true with block will still inherit the original class.
93
- AlbumForm.new(OpenStruct.new(band: OpenStruct.new)).band.band_id.must_equal 1
94
- CompilationForm.new(OpenStruct.new(band: OpenStruct.new)).band.band_id.must_equal 1
95
- end
96
-
97
- class CDForm < AlbumForm
98
- # override :band's original populate_if_empty but with :inherit.
99
- property :band, inherit: true, populator: "CD Populator" do
100
-
101
- end
102
- end
103
-
104
- it { CDForm.options_for(:band)[:internal_populator].instance_variable_get(:@user_proc).must_equal "CD Populator" }
105
- end