yob-roxml 3.1.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. data/.gitignore +7 -0
  2. data/.gitmodules +3 -0
  3. data/History.txt +354 -0
  4. data/LICENSE +20 -0
  5. data/README.rdoc +195 -0
  6. data/Rakefile +117 -0
  7. data/TODO +37 -0
  8. data/VERSION +1 -0
  9. data/examples/amazon.rb +35 -0
  10. data/examples/current_weather.rb +27 -0
  11. data/examples/dashed_elements.rb +20 -0
  12. data/examples/library.rb +40 -0
  13. data/examples/posts.rb +27 -0
  14. data/examples/rails.rb +70 -0
  15. data/examples/twitter.rb +37 -0
  16. data/examples/xml/active_record.xml +70 -0
  17. data/examples/xml/amazon.xml +133 -0
  18. data/examples/xml/current_weather.xml +89 -0
  19. data/examples/xml/dashed_elements.xml +52 -0
  20. data/examples/xml/posts.xml +23 -0
  21. data/examples/xml/twitter.xml +422 -0
  22. data/lib/roxml.rb +556 -0
  23. data/lib/roxml/definition.rb +238 -0
  24. data/lib/roxml/hash_definition.rb +25 -0
  25. data/lib/roxml/xml.rb +40 -0
  26. data/lib/roxml/xml/parsers/libxml.rb +85 -0
  27. data/lib/roxml/xml/parsers/nokogiri.rb +82 -0
  28. data/lib/roxml/xml/references.rb +322 -0
  29. data/roxml.gemspec +206 -0
  30. data/spec/definition_spec.rb +494 -0
  31. data/spec/examples/active_record_spec.rb +40 -0
  32. data/spec/examples/amazon_spec.rb +54 -0
  33. data/spec/examples/current_weather_spec.rb +37 -0
  34. data/spec/examples/dashed_elements_spec.rb +20 -0
  35. data/spec/examples/library_spec.rb +46 -0
  36. data/spec/examples/post_spec.rb +24 -0
  37. data/spec/examples/twitter_spec.rb +32 -0
  38. data/spec/roxml_spec.rb +372 -0
  39. data/spec/shared_specs.rb +15 -0
  40. data/spec/spec.opts +1 -0
  41. data/spec/spec_helper.rb +14 -0
  42. data/spec/support/libxml.rb +3 -0
  43. data/spec/support/nokogiri.rb +3 -0
  44. data/spec/xml/array_spec.rb +36 -0
  45. data/spec/xml/attributes_spec.rb +71 -0
  46. data/spec/xml/encoding_spec.rb +52 -0
  47. data/spec/xml/namespace_spec.rb +270 -0
  48. data/spec/xml/namespaces_spec.rb +67 -0
  49. data/spec/xml/object_spec.rb +82 -0
  50. data/spec/xml/parser_spec.rb +21 -0
  51. data/spec/xml/text_spec.rb +71 -0
  52. data/test/fixtures/book_malformed.xml +5 -0
  53. data/test/fixtures/book_pair.xml +8 -0
  54. data/test/fixtures/book_text_with_attribute.xml +5 -0
  55. data/test/fixtures/book_valid.xml +5 -0
  56. data/test/fixtures/book_with_authors.xml +7 -0
  57. data/test/fixtures/book_with_contributions.xml +9 -0
  58. data/test/fixtures/book_with_contributors.xml +7 -0
  59. data/test/fixtures/book_with_contributors_attrs.xml +7 -0
  60. data/test/fixtures/book_with_default_namespace.xml +9 -0
  61. data/test/fixtures/book_with_depth.xml +6 -0
  62. data/test/fixtures/book_with_octal_pages.xml +4 -0
  63. data/test/fixtures/book_with_publisher.xml +7 -0
  64. data/test/fixtures/book_with_wrapped_attr.xml +3 -0
  65. data/test/fixtures/dictionary_of_attr_name_clashes.xml +8 -0
  66. data/test/fixtures/dictionary_of_attrs.xml +6 -0
  67. data/test/fixtures/dictionary_of_guarded_names.xml +6 -0
  68. data/test/fixtures/dictionary_of_mixeds.xml +4 -0
  69. data/test/fixtures/dictionary_of_name_clashes.xml +10 -0
  70. data/test/fixtures/dictionary_of_names.xml +4 -0
  71. data/test/fixtures/dictionary_of_texts.xml +10 -0
  72. data/test/fixtures/library.xml +30 -0
  73. data/test/fixtures/library_uppercase.xml +30 -0
  74. data/test/fixtures/muffins.xml +3 -0
  75. data/test/fixtures/nameless_ageless_youth.xml +2 -0
  76. data/test/fixtures/node_with_attr_name_conflicts.xml +1 -0
  77. data/test/fixtures/node_with_name_conflicts.xml +4 -0
  78. data/test/fixtures/numerology.xml +4 -0
  79. data/test/fixtures/person.xml +1 -0
  80. data/test/fixtures/person_with_guarded_mothers.xml +13 -0
  81. data/test/fixtures/person_with_mothers.xml +10 -0
  82. data/test/load_test.rb +6 -0
  83. data/test/mocks/dictionaries.rb +57 -0
  84. data/test/mocks/mocks.rb +279 -0
  85. data/test/support/fixtures.rb +11 -0
  86. data/test/test_helper.rb +34 -0
  87. data/test/unit/definition_test.rb +235 -0
  88. data/test/unit/deprecations_test.rb +24 -0
  89. data/test/unit/to_xml_test.rb +81 -0
  90. data/test/unit/xml_attribute_test.rb +39 -0
  91. data/test/unit/xml_block_test.rb +81 -0
  92. data/test/unit/xml_bool_test.rb +122 -0
  93. data/test/unit/xml_convention_test.rb +150 -0
  94. data/test/unit/xml_hash_test.rb +115 -0
  95. data/test/unit/xml_initialize_test.rb +49 -0
  96. data/test/unit/xml_name_test.rb +141 -0
  97. data/test/unit/xml_namespace_test.rb +31 -0
  98. data/test/unit/xml_object_test.rb +205 -0
  99. data/test/unit/xml_required_test.rb +94 -0
  100. data/test/unit/xml_text_test.rb +71 -0
  101. data/website/index.html +98 -0
  102. metadata +300 -0
@@ -0,0 +1,494 @@
1
+ require 'spec/spec_helper'
2
+
3
+ describe ROXML::Definition do
4
+ describe "#name_explicit?" do
5
+ it "should indicate whether from option is present" do
6
+ ROXML::Definition.new(:element, :from => 'somewhere').name_explicit?.should be_true
7
+ ROXML::Definition.new(:element).name_explicit?.should be_false
8
+ end
9
+
10
+ it "should not consider name proxies as explicit" do
11
+ ROXML::Definition.new(:element, :from => :attr).name_explicit?.should be_false
12
+ ROXML::Definition.new(:element, :from => :content).name_explicit?.should be_false
13
+ end
14
+ end
15
+
16
+ describe "DateTime reference", :shared => true do
17
+ it "should return nil on empty string" do
18
+ @subject.blocks.first.call(" ").should be_nil
19
+ end
20
+
21
+ it "should return a time version of the string" do
22
+ @subject.blocks.first.call("12:05pm, September 3rd, 1970").to_s == "1970-09-03T12:05:00+00:00"
23
+ end
24
+
25
+ context "when passed an array of values" do
26
+ it "should timify all of them" do
27
+ @subject.blocks.first.call(["12:05pm, September 3rd, 1970", "3:00pm, May 22, 1700"]).map(&:to_s).should == ["1970-09-03T12:05:00+00:00", "1700-05-22T15:00:00+00:00"]
28
+ end
29
+ end
30
+ end
31
+
32
+ describe "Date reference", :shared => true do
33
+ it "should return nil on empty string" do
34
+ @subject.blocks.first.call(" ").should be_nil
35
+ end
36
+
37
+ it "should return a time version of the string" do
38
+ @subject.blocks.first.call("September 3rd, 1970").to_s == "1970-09-03"
39
+ end
40
+
41
+ context "when passed an array of values" do
42
+ it "should timify all of them" do
43
+ @subject.blocks.first.call(["September 3rd, 1970", "1776-07-04"]).map(&:to_s).should == ["1970-09-03", "1776-07-04"]
44
+ end
45
+ end
46
+ end
47
+
48
+ it "should unescape xml entities" do
49
+ ROXML::Definition.new(:questions, :as => []).to_ref(RoxmlObject.new).value_in(%{
50
+ <xml>
51
+ <question>&quot;Wickard &amp; Filburn&quot; &gt;</question>
52
+ <question> &lt; McCulloch &amp; Maryland?</question>
53
+ </xml>
54
+ }).should == ["\"Wickard & Filburn\" >", " < McCulloch & Maryland?"]
55
+ end
56
+
57
+ it "should unescape utf characters in xml" do
58
+ ROXML::Definition.new(:questions, :as => []).to_ref(RoxmlObject.new).value_in(%{
59
+ <xml>
60
+ <question>ROXML\342\204\242</question>
61
+ </xml>
62
+ }).should == ["ROXML™"]
63
+ end
64
+
65
+ describe "attr name" do
66
+ context "when ending with '_at'" do
67
+ context "and without an :as argument" do
68
+ before(:all) do
69
+ @subject = ROXML::Definition.new(:time_at)
70
+ end
71
+ it_should_behave_like "DateTime reference"
72
+ end
73
+ end
74
+
75
+ context "when ending with '_on'" do
76
+ context "and without an :as argument" do
77
+ before(:all) do
78
+ @subject = ROXML::Definition.new(:created_on)
79
+ end
80
+ it_should_behave_like "Date reference"
81
+ end
82
+ end
83
+ end
84
+
85
+ describe ":as" do
86
+ describe "=> []" do
87
+ it "should means array of texts" do
88
+ opts = ROXML::Definition.new(:authors, :as => [])
89
+ opts.array?.should be_true
90
+ opts.sought_type.should == :text
91
+ end
92
+ end
93
+
94
+ describe "=> RoxmlClass" do
95
+ class RoxmlClass
96
+ include ROXML
97
+ end
98
+
99
+ it "should store type" do
100
+ opts = ROXML::Definition.new(:name, :as => RoxmlClass)
101
+ opts.sought_type.should == RoxmlClass
102
+ end
103
+ end
104
+
105
+ describe "=> NonRoxmlClassWithFromXmlDefined" do
106
+ class OctalInteger
107
+ def self.from_xml(val)
108
+ new(Integer(val.content))
109
+ end
110
+ end
111
+
112
+ it "should accept type" do
113
+ opts = ROXML::Definition.new(:name, :as => OctalInteger)
114
+ opts.sought_type.should == OctalInteger
115
+ end
116
+ end
117
+
118
+ describe "=> NonRoxmlClass" do
119
+ it "should fail with a warning" do
120
+ proc { ROXML::Definition.new(:authors, :as => Module) }.should raise_error(ArgumentError)
121
+ end
122
+ end
123
+
124
+ describe "=> [NonRoxmlClass]" do
125
+ it "should raise" do
126
+ proc { ROXML::Definition.new(:authors, :as => [Module]) }.should raise_error(ArgumentError)
127
+ end
128
+ end
129
+
130
+ describe "=> {}" do
131
+ describe "hash options declaration", :shared => true do
132
+ it "should represent a hash" do
133
+ @opts.hash?.should be_true
134
+ end
135
+
136
+ it "should have hash definition" do
137
+ {@opts.hash.key.sought_type => @opts.hash.key.name}.should == @hash_args[:key]
138
+ {@opts.hash.value.sought_type => @opts.hash.value.name}.should == @hash_args[:value]
139
+ end
140
+
141
+ it "should not represent an array" do
142
+ @opts.array?.should be_false
143
+ end
144
+ end
145
+
146
+ describe "hash with attr key and text val" do
147
+ before do
148
+ @opts = ROXML::Definition.new(:attributes, :as => {:key => '@name',
149
+ :value => 'value'})
150
+ @hash_args = {:key => {:attr => 'name'},
151
+ :value => {:text => 'value'}}
152
+ end
153
+
154
+ it_should_behave_like "hash options declaration"
155
+ end
156
+
157
+ describe "hash with String class for type" do
158
+ before do
159
+ @opts = ROXML::Definition.new(:attributes, :as => {:key => 'name',
160
+ :value => 'value'})
161
+ @hash_args = {:key => {:text => 'name'}, :value => {:text => 'value'}}
162
+ end
163
+
164
+ it_should_behave_like "hash options declaration"
165
+ end
166
+
167
+ describe "hash with attr key and content val" do
168
+ before do
169
+ @opts = ROXML::Definition.new(:attributes, :as => {:key => '@name',
170
+ :value => :content})
171
+ @hash_args = {:key => {:attr => 'name'}, :value => {:text => '.'}}
172
+ end
173
+
174
+ it_should_behave_like "hash options declaration"
175
+ end
176
+
177
+ describe "hash with names as keys and content vals" do
178
+ before do
179
+ @opts = ROXML::Definition.new(:attributes, :as => {:key => :name,
180
+ :value => :content})
181
+ @hash_args = {:key => {:text => '*'}, :value => {:text => '.'}}
182
+ end
183
+
184
+ it_should_behave_like "hash options declaration"
185
+ end
186
+ end
187
+
188
+ describe "for block shorthand" do
189
+ describe "in literal array" do
190
+ before do
191
+ @opts = ROXML::Definition.new(:intarray, :as => [Integer])
192
+ end
193
+
194
+ it "should be detected as array reference" do
195
+ @opts.array?.should be_true
196
+ end
197
+
198
+ it "should be normal otherwise" do
199
+ @opts.sought_type.should == :text
200
+ @opts.blocks.size.should == 1
201
+ end
202
+ end
203
+
204
+ it "should have no blocks without a shorthand" do
205
+ ROXML::Definition.new(:count).blocks.should be_empty
206
+ end
207
+
208
+ it "should raise on unknown :as" do
209
+ proc { ROXML::Definition.new(:count, :as => :bogus) }.should raise_error(ArgumentError)
210
+ proc { ROXML::Definition.new(:count, :as => :foat) }.should raise_error(ArgumentError)
211
+ end
212
+
213
+ describe "block shorthand type declaration", :shared => true do
214
+ it "should translate nil to nil" do
215
+ @definition.blocks.first.call(nil).should be_nil
216
+ end
217
+
218
+ it "should translate empty strings to nil" do
219
+ @definition.blocks.first.call("").should be_nil
220
+ @definition.blocks.first.call(" ").should be_nil
221
+ end
222
+ end
223
+
224
+ describe "Integer" do
225
+ before do
226
+ @definition = ROXML::Definition.new(:intvalue, :as => Integer)
227
+ end
228
+
229
+ it_should_behave_like "block shorthand type declaration"
230
+
231
+ it "should translate text to integers" do
232
+ @definition.blocks.first['3'].should == 3
233
+ @definition.blocks.first['792'].should == 792
234
+ end
235
+
236
+ it "should raise on non-integer values" do
237
+ proc { @definition.blocks.first['08'] }.should raise_error(ArgumentError)
238
+ proc { @definition.blocks.first['793.12'] }.should raise_error(ArgumentError)
239
+ proc { @definition.blocks.first['junk 11'] }.should raise_error(ArgumentError)
240
+ proc { @definition.blocks.first['11sttf'] }.should raise_error(ArgumentError)
241
+ end
242
+
243
+ context "when passed an array" do
244
+ it "should translate the array elements to integer" do
245
+ @definition.blocks.first.call(["792", "12", "328"]).should == [792, 12, 328]
246
+ end
247
+ end
248
+ end
249
+
250
+ describe "Float" do
251
+ before do
252
+ @definition = ROXML::Definition.new(:floatvalue, :as => Float)
253
+ end
254
+
255
+ it_should_behave_like "block shorthand type declaration"
256
+
257
+ it "should translate text to float" do
258
+ @definition.blocks.first['3'].should == 3.0
259
+ @definition.blocks.first['12.7'].should == 12.7
260
+ end
261
+
262
+ it "should raise on non-float values" do
263
+ proc { @definition.blocks.first['junk 11.3'] }.should raise_error(ArgumentError)
264
+ proc { @definition.blocks.first['11.1sttf'] }.should raise_error(ArgumentError)
265
+ end
266
+
267
+ context "when passed an array" do
268
+ it "should translate the array elements to integer" do
269
+ @definition.blocks.first.call(["792.13", "240", "3.14"]).should == [792.13, 240.0, 3.14]
270
+ end
271
+ end
272
+ end
273
+
274
+ describe "BigDecimal" do
275
+ before do
276
+ @definition = ROXML::Definition.new(:decimalvalue, :as => BigDecimal)
277
+ end
278
+
279
+ it_should_behave_like "block shorthand type declaration"
280
+
281
+ it "should translate text to decimal numbers" do
282
+ @definition.blocks.first['3'].should == BigDecimal.new("3.0")
283
+ @definition.blocks.first['0.3'].should == BigDecimal.new("0.3")
284
+ end
285
+
286
+ it "should extract what it can, and fall back to 0" do
287
+ @definition.blocks.first['junk 11'].should eql(BigDecimal.new("0"))
288
+ @definition.blocks.first['11sttf'].should eql(BigDecimal.new("11.0"))
289
+ end
290
+
291
+ context "when passed an array" do
292
+ it "should translate the array elements to integer" do
293
+ @definition.blocks.first.call(["12.1", "328.2"]).should == [BigDecimal.new("12.1"), BigDecimal.new("328.2")]
294
+ end
295
+ end
296
+ end
297
+
298
+ describe "Fixnum" do
299
+ before do
300
+ @definition = ROXML::Definition.new(:fixnumvalue, :as => Fixnum)
301
+ end
302
+
303
+ it_should_behave_like "block shorthand type declaration"
304
+
305
+ it "should translate text to integers" do
306
+ @definition.blocks.first['3'].should == 3
307
+ @definition.blocks.first['792'].should == 792
308
+ @definition.blocks.first['08'].should == 8
309
+ @definition.blocks.first['279.23'].should == 279
310
+ end
311
+
312
+ it "should extract whatever is possible and fall back to 0" do
313
+ @definition.blocks.first['junk 11'].should eql(0)
314
+ @definition.blocks.first['.?sttf'].should eql(0)
315
+ @definition.blocks.first['11sttf'].should eql(11)
316
+ end
317
+
318
+ context "when passed an array" do
319
+ it "should translate the array elements to integer" do
320
+ @definition.blocks.first.call(["792", "12", "328"]).should == [792, 12, 328]
321
+ end
322
+ end
323
+ end
324
+
325
+ describe ":bool" do
326
+ it "should boolify individual values" do
327
+ ROXML::Definition.new(:floatvalue, :as => :bool).blocks.first.call("1").should be_true
328
+ ROXML::Definition.new(:floatvalue, :as => :bool).blocks.first.call("True").should be_true
329
+ ROXML::Definition.new(:floatvalue, :as => :bool).blocks.first.call("Yes").should be_true
330
+ end
331
+
332
+ context "when an array is passed in" do
333
+ it "should boolify arrays of values" do
334
+ ROXML::Definition.new(:floatvalue, :as => :bool).blocks.first.call("0").should be_false
335
+ ROXML::Definition.new(:floatvalue, :as => :bool).blocks.first.call("false").should be_false
336
+ ROXML::Definition.new(:floatvalue, :as => :bool).blocks.first.call("nO").should be_false
337
+ end
338
+ end
339
+
340
+ context "when no value is detected" do
341
+ it "should return nil" do
342
+ ROXML::Definition.new(:floatvalue, :as => :bool).blocks.first.call("junk").should be_nil
343
+ end
344
+
345
+ context "when a literal block is available" do
346
+ it "should pass the value itself to the block"
347
+ end
348
+ end
349
+ end
350
+
351
+ describe "Time" do
352
+ it "should return nil on empty string" do
353
+ ROXML::Definition.new(:floatvalue, :as => Time).blocks.first.call(" ").should be_nil
354
+ end
355
+
356
+ it "should return a time version of the string" do
357
+ ROXML::Definition.new(:datevalue, :as => Time).blocks.first.call("12:31am").min.should == 31
358
+ end
359
+
360
+ context "when passed an array of values" do
361
+ it "should timify all of them" do
362
+ ROXML::Definition.new(:datevalue, :as => Time).blocks.first.call(["12:31am", "3:00pm", "11:59pm"]).map(&:min).should == [31, 0, 59]
363
+ end
364
+ end
365
+ end
366
+
367
+ describe "Date" do
368
+ before do
369
+ @subject = ROXML::Definition.new(:datevalue, :as => Date)
370
+ end
371
+ it_should_behave_like "Date reference"
372
+ end
373
+
374
+ describe "DateTime" do
375
+ before do
376
+ @subject = ROXML::Definition.new(:datevalue, :as => DateTime)
377
+ end
378
+ it_should_behave_like "DateTime reference"
379
+ end
380
+
381
+ it "should prohibit multiple shorthands" do
382
+ proc { ROXML::Definition.new(:count, :as => [Float, Integer]) }.should raise_error(ArgumentError)
383
+ end
384
+
385
+ it "should stack block shorthands with explicit blocks" do
386
+ ROXML::Definition.new(:count, :as => Integer) {|val| val.to_i }.blocks.size.should == 2
387
+ ROXML::Definition.new(:count, :as => Float) {|val| val.object_id }.blocks.size.should == 2
388
+ end
389
+ end
390
+ end
391
+
392
+ describe ":from" do
393
+ describe "attribute reference", :shared => true do
394
+ it "should be interpreted as :attr" do
395
+ @opts.sought_type.should == :attr
396
+ end
397
+
398
+ it "should strip '@' from name" do
399
+ @opts.name.should == 'attr_name'
400
+ end
401
+
402
+ it "should unescape xml entities" do
403
+ @opts.to_ref(RoxmlObject.new).value_in(%{
404
+ <question attr_name="&quot;Wickard &amp; Filburn&quot; &gt; / &lt; McCulloch &amp; Marryland?" />
405
+ }).should == "\"Wickard & Filburn\" > / < McCulloch & Marryland?"
406
+ end
407
+ end
408
+
409
+ context ":attr" do
410
+ before do
411
+ @opts = ROXML::Definition.new(:attr_name, :from => :attr)
412
+ end
413
+
414
+ it_should_behave_like "attribute reference"
415
+ end
416
+
417
+ context "@attribute_name" do
418
+ before do
419
+ @opts = ROXML::Definition.new(:attr_name, :from => '@attr_name')
420
+ end
421
+
422
+ it_should_behave_like "attribute reference"
423
+ end
424
+
425
+ describe ":content" do
426
+ it "should be recognized" do
427
+ ROXML::Definition.new(:author).content?.should be_false
428
+ ROXML::Definition.new(:author, :from => :content).content?.should == true
429
+ end
430
+
431
+ it "should be equivalent to :from => '.'" do
432
+ ROXML::Definition.new(:author, :from => '.').content?.should == true
433
+ end
434
+ end
435
+ end
436
+
437
+ describe ":in" do
438
+ context "as xpath" do
439
+ it "should pass through as wrapper" do
440
+ ROXML::Definition.new(:manufacturer, :in => './').wrapper.should == './'
441
+ end
442
+ end
443
+
444
+ context "as xpath" do
445
+ it "should pass through as wrapper" do
446
+ ROXML::Definition.new(:manufacturer, :in => 'wrapper').wrapper.should == 'wrapper'
447
+ end
448
+ end
449
+ end
450
+
451
+ describe "options" do
452
+
453
+ describe "boolean option", :shared => true do
454
+ it "should be recognized" do
455
+ ROXML::Definition.new(:author, :from => :content, @option => true).respond_to?(:"#{@option}?")
456
+ ROXML::Definition.new(:author, :from => :content, @option => true).send(:"#{@option}?").should be_true
457
+ ROXML::Definition.new(:author, :from => :content, @option => false).send(:"#{@option}?").should be_false
458
+ end
459
+
460
+ it "should default to false" do
461
+ ROXML::Definition.new(:author, :from => :content).send(:"#{@option}?").should be_false
462
+ end
463
+ end
464
+
465
+ describe ":required" do
466
+ before do
467
+ @option = :required
468
+ end
469
+
470
+ it_should_behave_like "boolean option"
471
+
472
+ it "should not be allowed together with :else" do
473
+ proc { ROXML::Definition.new(:author, :from => :content, :required => true, :else => 'Johnny') }.should raise_error(ArgumentError)
474
+ proc { ROXML::Definition.new(:author, :from => :content, :required => false, :else => 'Johnny') }.should_not raise_error
475
+ end
476
+ end
477
+
478
+ describe ":frozen" do
479
+ before do
480
+ @option = :frozen
481
+ end
482
+
483
+ it_should_behave_like "boolean option"
484
+ end
485
+
486
+ describe ":cdata" do
487
+ before do
488
+ @option = :cdata
489
+ end
490
+
491
+ it_should_behave_like "boolean option"
492
+ end
493
+ end
494
+ end