Empact-roxml 2.4.3 → 2.5.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.
- data/History.txt +65 -0
- data/Manifest.txt +11 -6
- data/README.rdoc +48 -26
- data/Rakefile +5 -2
- data/TODO +30 -31
- data/examples/active_record.rb +70 -0
- data/examples/amazon.rb +1 -1
- data/examples/current_weather.rb +1 -1
- data/examples/library.rb +40 -0
- data/examples/posts.rb +8 -8
- data/examples/twitter.rb +2 -2
- data/examples/xml/active_record.xml +70 -0
- data/lib/roxml.rb +174 -174
- data/lib/roxml/definition.rb +165 -89
- data/lib/roxml/extensions/deprecation.rb +5 -0
- data/lib/roxml/extensions/string/conversions.rb +2 -3
- data/lib/roxml/hash_definition.rb +26 -25
- data/lib/roxml/xml.rb +15 -6
- data/lib/roxml/xml/parsers/libxml.rb +14 -6
- data/lib/roxml/xml/parsers/rexml.rb +16 -5
- data/lib/roxml/xml/references.rb +14 -17
- data/roxml.gemspec +14 -5
- data/spec/definition_spec.rb +563 -0
- data/spec/examples/active_record_spec.rb +40 -0
- data/spec/examples/library_spec.rb +41 -0
- data/spec/roxml_spec.rb +372 -0
- data/spec/shared_specs.rb +15 -0
- data/spec/spec_helper.rb +21 -4
- data/spec/string_spec.rb +15 -0
- data/spec/xml/parser_spec.rb +47 -0
- data/test/fixtures/book_valid.xml +1 -1
- data/test/fixtures/person_with_guarded_mothers.xml +3 -3
- data/test/mocks/mocks.rb +57 -45
- data/test/test_helper.rb +1 -1
- data/test/unit/definition_test.rb +161 -12
- data/test/unit/deprecations_test.rb +97 -0
- data/test/unit/to_xml_test.rb +30 -1
- data/test/unit/xml_bool_test.rb +15 -3
- data/test/unit/xml_construct_test.rb +6 -6
- data/test/unit/xml_hash_test.rb +18 -0
- data/test/unit/xml_initialize_test.rb +6 -3
- data/test/unit/xml_namespace_test.rb +1 -0
- data/test/unit/xml_object_test.rb +66 -5
- data/test/unit/xml_text_test.rb +3 -0
- metadata +57 -24
- data/test/unit/array_test.rb +0 -16
- data/test/unit/freeze_test.rb +0 -71
- data/test/unit/inheritance_test.rb +0 -63
- data/test/unit/overriden_output_test.rb +0 -33
- data/test/unit/roxml_test.rb +0 -60
- data/test/unit/string_test.rb +0 -11
data/History.txt
CHANGED
@@ -1,3 +1,68 @@
|
|
1
|
+
== 2.5.1 (March 2, 2009)
|
2
|
+
|
3
|
+
* minor enhancements
|
4
|
+
|
5
|
+
* Add Document#save to REXML support, complete with XMLDecl output
|
6
|
+
|
7
|
+
* bug fixes
|
8
|
+
|
9
|
+
* rexml support has been fixed
|
10
|
+
* the first example in the readme was broken and has been fixed
|
11
|
+
|
12
|
+
== 2.5.0 (February 24, 2009)
|
13
|
+
|
14
|
+
* major enhancements
|
15
|
+
|
16
|
+
* support for mapping ActiveRecord classes. See examples/active_record.rb.
|
17
|
+
* .from_xml will now use the setter for the declared variable, if one is available,
|
18
|
+
rather than directly setting the instance variable
|
19
|
+
* All declaration type arguments are now supported via the :as parameter, e.g. :as => [MyType]. Other uses are deprecated.
|
20
|
+
* All xml source path arguments are now supported via the :from and :in parameters, e.g. :from => :attr or :from => '@MyAttr'. Other uses are deprecated.
|
21
|
+
* All other options are presented separately, e.g. :cdata => true rather than :as => :cdata. Other uses are deprecated.
|
22
|
+
|
23
|
+
* minor enhancements
|
24
|
+
|
25
|
+
* .xml_attr declaration declares neither a reader nor a writer. With it you're left to your own devices.
|
26
|
+
* You can use literal [] for the [:text] object type declaration,
|
27
|
+
though they should be used in the :as parameter: :as => []
|
28
|
+
* You can use [] with your :as declarations. e.g. :as => [Float] is
|
29
|
+
equivalent to the old :as => [Float, :array]
|
30
|
+
* Show the actual call point of a deprecation, rather than some internal path
|
31
|
+
* Add support for BigDecimal and Fixnum as block shorthands [James Healy]
|
32
|
+
* Update libxml support to 0.9.6, and add it as a dependency, to ensure correct versioning, and
|
33
|
+
as it's an order of magnitude faster than rexml
|
34
|
+
|
35
|
+
* breaking changes
|
36
|
+
|
37
|
+
* :else option only applies to instances created via .from_xml
|
38
|
+
* On .from_xml, #initialize is now called with the *initialization_args before extracting attributes from the xml.
|
39
|
+
* #xml_initialize has been replaced with the #after_parse callback, which takes no arguments.
|
40
|
+
* .xml_accessor will overwrite the setter for this variable if it has already been defined. Use .xml_reader or .xml_attr,
|
41
|
+
or define your writer later, if this is not the behavior you want.
|
42
|
+
|
43
|
+
* deprecations
|
44
|
+
|
45
|
+
* Use :cdata => true rather than :as => :cdata
|
46
|
+
* Use literal [] around your regular object type, rather than :as => :array
|
47
|
+
* Use :from => :content rather than the :content object declaration type
|
48
|
+
* Specifying an unknown symbol or Class for :as will raise in 3.0
|
49
|
+
* Specifying :as with anything other than a type argument e.g. :bool, Float, [Date],
|
50
|
+
will not be supported in 3.0
|
51
|
+
* Use :from => :attr or :from => '@attribute_name' rather than the :attr
|
52
|
+
object declaration type
|
53
|
+
* Passing any type declaration outside the :as parameter is deprecated
|
54
|
+
* In 3.0, attributes ending in _on and _at will default to :as => Date and DateTime, respectively,
|
55
|
+
rather than :text
|
56
|
+
* Deprecated hash :attrs declaration syntax in favor of {:key => '@attr1', :value => '@attr2'}
|
57
|
+
* Deprecated hash {Type => 'name'} declaration syntax in favor of {:as => Type, :from => 'name}
|
58
|
+
* Deprecated String#to_utf and #to_latin.
|
59
|
+
|
60
|
+
* bug fixes
|
61
|
+
|
62
|
+
* xml_accessor now properly handles punctuation, such that the writer appears without '?' for boolean attributes
|
63
|
+
* text node contents are no longer truncated when '&' are present in the contents
|
64
|
+
* When using :as => Integer or Float, don't raise on missing element [James Healy]
|
65
|
+
|
1
66
|
== 2.4.3 (February 1, 2009)
|
2
67
|
|
3
68
|
* 1 bug fix
|
data/Manifest.txt
CHANGED
@@ -5,11 +5,14 @@ README.rdoc
|
|
5
5
|
Rakefile
|
6
6
|
TODO
|
7
7
|
config/website.yml
|
8
|
+
examples/active_record.rb
|
8
9
|
examples/amazon.rb
|
9
10
|
examples/current_weather.rb
|
10
11
|
examples/dashed_elements.rb
|
12
|
+
examples/library.rb
|
11
13
|
examples/posts.rb
|
12
14
|
examples/twitter.rb
|
15
|
+
examples/xml/active_record.xml
|
13
16
|
examples/xml/amazon.xml
|
14
17
|
examples/xml/current_weather.xml
|
15
18
|
examples/xml/dashed_elements.xml
|
@@ -31,13 +34,20 @@ lib/roxml/xml/parsers/libxml.rb
|
|
31
34
|
lib/roxml/xml/parsers/rexml.rb
|
32
35
|
lib/roxml/xml/references.rb
|
33
36
|
roxml.gemspec
|
37
|
+
spec/definition_spec.rb
|
38
|
+
spec/examples/active_record_spec.rb
|
34
39
|
spec/examples/amazon_spec.rb
|
35
40
|
spec/examples/current_weather_spec.rb
|
36
41
|
spec/examples/dashed_elements_spec.rb
|
42
|
+
spec/examples/library_spec.rb
|
37
43
|
spec/examples/post_spec.rb
|
38
44
|
spec/examples/twitter_spec.rb
|
45
|
+
spec/roxml_spec.rb
|
46
|
+
spec/shared_specs.rb
|
39
47
|
spec/spec.opts
|
40
48
|
spec/spec_helper.rb
|
49
|
+
spec/string_spec.rb
|
50
|
+
spec/xml/parser_spec.rb
|
41
51
|
tasks/rspec.rake
|
42
52
|
tasks/test.rake
|
43
53
|
test/bugs/rexml_bugs.rb
|
@@ -75,13 +85,8 @@ test/mocks/dictionaries.rb
|
|
75
85
|
test/mocks/mocks.rb
|
76
86
|
test/release/dependencies_test.rb
|
77
87
|
test/test_helper.rb
|
78
|
-
test/unit/array_test.rb
|
79
88
|
test/unit/definition_test.rb
|
80
|
-
test/unit/
|
81
|
-
test/unit/inheritance_test.rb
|
82
|
-
test/unit/overriden_output_test.rb
|
83
|
-
test/unit/roxml_test.rb
|
84
|
-
test/unit/string_test.rb
|
89
|
+
test/unit/deprecations_test.rb
|
85
90
|
test/unit/to_xml_test.rb
|
86
91
|
test/unit/xml_attribute_test.rb
|
87
92
|
test/unit/xml_block_test.rb
|
data/README.rdoc
CHANGED
@@ -14,36 +14,36 @@ ROXML, you can annotate the Ruby classes as follows:
|
|
14
14
|
class Book
|
15
15
|
include ROXML
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
17
|
+
xml_accessor :isbn, :from => "@ISBN" # attribute with name 'ISBN'
|
18
|
+
xml_accessor :title
|
19
|
+
xml_accessor :description, :cdata => true # text node with cdata protection
|
20
|
+
xml_accessor :author
|
21
21
|
end
|
22
22
|
|
23
23
|
class Library
|
24
24
|
include ROXML
|
25
25
|
|
26
|
-
xml_accessor :name, :from => "NAME", :
|
27
|
-
xml_accessor :books, [Book],
|
26
|
+
xml_accessor :name, :from => "NAME", :cdata => true
|
27
|
+
xml_accessor :books, :as => [Book] # by default roxml searches for books for in <book> child nodes, then, if none are present, in ./books/book children
|
28
28
|
end
|
29
29
|
|
30
30
|
To create a library and put a number of books in it we could run the following code:
|
31
31
|
|
32
|
-
book = Book.new
|
32
|
+
book = Book.new
|
33
33
|
book.isbn = "0201710897"
|
34
34
|
book.title = "The PickAxe"
|
35
35
|
book.description = "Best Ruby book out there!"
|
36
36
|
book.author = "David Thomas, Andrew Hunt, Dave Thomas"
|
37
37
|
|
38
|
-
lib = Library.new
|
38
|
+
lib = Library.new
|
39
39
|
lib.name = "Favorite Books"
|
40
|
-
lib
|
40
|
+
lib.books = [book]
|
41
41
|
|
42
42
|
To save this information to an XML file:
|
43
43
|
|
44
|
-
|
45
|
-
|
46
|
-
|
44
|
+
doc = ROXML::XML::Document.new
|
45
|
+
doc.root = lib.to_xml
|
46
|
+
doc.save("library.xml")
|
47
47
|
|
48
48
|
To later populate the library object from the XML file:
|
49
49
|
|
@@ -62,11 +62,22 @@ you would add a reference to another ROXML class. For example:
|
|
62
62
|
|
63
63
|
can be mapped using the following code:
|
64
64
|
|
65
|
+
class Publisher
|
66
|
+
include ROXML
|
67
|
+
|
68
|
+
xml_accessor :name
|
69
|
+
|
70
|
+
# other important functionality
|
71
|
+
end
|
72
|
+
|
65
73
|
class BookWithPublisher
|
66
74
|
include ROXML
|
67
75
|
|
68
|
-
xml_name
|
69
|
-
xml_reader :publisher, Publisher
|
76
|
+
xml_name 'book'
|
77
|
+
xml_reader :publisher, :as => Publisher
|
78
|
+
|
79
|
+
# or, alternatively, if no class is needed to hang functionality on:
|
80
|
+
# xml_reader :publisher, :from => 'name', :in => 'publisher'
|
70
81
|
end
|
71
82
|
|
72
83
|
Note: In the above example, _xml_name_ annotation tells ROXML to set the element
|
@@ -78,34 +89,45 @@ in this case.
|
|
78
89
|
Extending the above examples, say you want to parse a book's page count and have it available as an Integer.
|
79
90
|
In such a case, you can extend any object with a block to manipulate it's value at parse time. For example:
|
80
91
|
|
81
|
-
class
|
92
|
+
class Dog
|
82
93
|
include ROXML
|
83
94
|
|
84
|
-
xml_reader
|
85
|
-
Integer(val)
|
86
|
-
end
|
95
|
+
xml_reader(:age, :from => '@human_years', :as => Integer) {|years| years * 7 }
|
87
96
|
end
|
88
97
|
|
89
98
|
The result of the block above is stored, rather than the actual value parsed from the document.
|
90
99
|
|
91
100
|
== Construction
|
92
101
|
|
93
|
-
|
94
|
-
|
95
|
-
|
102
|
+
Object life-cycle is as follows: .from_xml is called with a first argument representing the xml
|
103
|
+
in file, string, or path form, and with optional initialization_args following.
|
104
|
+
|
105
|
+
Firt .new and thus #initialize, is called with those same initialization_args, or no args if none
|
106
|
+
are present. Then the object is populated with the attribute values from xml. Then the
|
107
|
+
#after_parse callback is called, with no arguments.
|
108
|
+
|
109
|
+
In #after_parse you can ensure that your object initialization is complete, including initialization which
|
110
|
+
requires more than one variable in concert.
|
111
|
+
|
112
|
+
E.g.:
|
96
113
|
|
97
114
|
class Measurement
|
98
115
|
include ROXML
|
99
116
|
|
100
|
-
xml_reader :units, :attr
|
101
|
-
xml_reader :value, :content
|
117
|
+
xml_reader :units, :from => :attr
|
118
|
+
xml_reader :value, :from => :content
|
119
|
+
|
120
|
+
def initialize(value = 0, units = 'meters')
|
121
|
+
to_metric
|
122
|
+
end
|
102
123
|
|
103
|
-
|
124
|
+
private
|
125
|
+
def after_parse
|
104
126
|
# xml attributes of self are already valid
|
105
|
-
|
127
|
+
to_metric
|
106
128
|
end
|
107
129
|
|
108
|
-
def
|
130
|
+
def to_metric
|
109
131
|
# translate units & value into metric, for example
|
110
132
|
end
|
111
133
|
end
|
data/Rakefile
CHANGED
@@ -16,10 +16,13 @@ $hoe = Hoe.new('roxml', ROXML::VERSION) do |p|
|
|
16
16
|
p.changes = File.read("History.txt").split(/^==/)[1].strip
|
17
17
|
p.rubyforge_name = p.name
|
18
18
|
p.extra_deps = [
|
19
|
-
['activesupport','>= 2.1.0']
|
19
|
+
['activesupport','>= 2.1.0'],
|
20
|
+
['libxml-ruby', '>= 0.8.6']
|
20
21
|
]
|
21
22
|
p.extra_dev_deps = [
|
22
|
-
['newgem', ">= #{::Newgem::VERSION}"]
|
23
|
+
['newgem', ">= #{::Newgem::VERSION}"],
|
24
|
+
['sqlite3-ruby', '>= 1.2.4' ],
|
25
|
+
['activerecord', '>= 2.2.2' ]
|
23
26
|
]
|
24
27
|
|
25
28
|
p.summary = "Ruby Object to XML mapping library"
|
data/TODO
CHANGED
@@ -1,37 +1,10 @@
|
|
1
1
|
Planned:
|
2
2
|
|
3
|
-
v 2.5
|
4
|
-
* Test out compatibility with ActiveRecord such that it's easy
|
5
|
-
to have both xml & database-sourced objects
|
6
|
-
|
7
|
-
* provide an overridable extension point for the #to_xml-ing of
|
8
|
-
values. This would enable easy accommodation of non-standard formats.
|
9
|
-
For example, formatting a number with leading zeros.
|
10
|
-
|
11
|
-
* Ensure (perhaps optionally) that references are unambiguous. That is error/warn
|
12
|
-
a singular specification has multiple possible node references
|
13
|
-
|
14
|
-
* Automatically use Date or DateTime for accessors ending in '_on' and '_at', respectively
|
15
|
-
|
16
|
-
* Add xml_in helper to share :in declarations between several attributes. E.g.:
|
17
|
-
|
18
|
-
xml_reader :count, :in => 'Attributes', :as => Integer
|
19
|
-
xml_reader :something_else, :in => 'Attributes', :as => Date
|
20
|
-
|
21
|
-
becomes:
|
22
|
-
|
23
|
-
xml_in 'Attributes' do |xml|
|
24
|
-
xml.reader :count, :as => Integer
|
25
|
-
xml.reader :something_else, :as => Date
|
26
|
-
end
|
27
|
-
|
28
|
-
* :self => true for sending method_missing to this attribute?
|
29
|
-
|
30
|
-
* :attributes extensions ala HappyMapper?
|
31
|
-
|
32
3
|
v 3.0
|
33
4
|
* remove deprecated functionality
|
34
5
|
|
6
|
+
* Automatically use Date or DateTime for accessors ending in '_on' and '_at', respectively
|
7
|
+
|
35
8
|
* Clarify API via seperation of concerns. :as for Type, :from and :in for location,
|
36
9
|
All other options as named options, e.g. :cdata => true rather than :as => :cdata.
|
37
10
|
A few not so great examples...:
|
@@ -52,12 +25,38 @@ v 3.0
|
|
52
25
|
|
53
26
|
xml_reader :count, :size, :number, :as => Integer, :from => :attr
|
54
27
|
|
28
|
+
* Consider class_inheritable_attribute rather than superclass.try stuff.
|
29
|
+
|
30
|
+
* Do some benchmarking
|
31
|
+
|
32
|
+
v 3.1
|
33
|
+
|
34
|
+
* Back with http://xml-object.rubyforge.org/doc/ to minimize need for specifications?
|
35
|
+
|
55
36
|
* Commandeer #parse to use opposite #from_xml, but in an unrooted, collection-friendly fashion,
|
56
37
|
ala HappyMapper's parse
|
57
38
|
|
58
|
-
|
39
|
+
v 3.x
|
40
|
+
|
41
|
+
* :self => true for sending method_missing to this attribute?
|
42
|
+
|
43
|
+
* :attributes extensions ala HappyMapper?
|
44
|
+
|
45
|
+
* Add xml_attrs helper to share :in declarations between several attributes. E.g.:
|
46
|
+
|
47
|
+
xml_reader :count, :in => 'Attributes', :as => Integer
|
48
|
+
xml_reader :something_else, :in => 'Attributes', :as => Date
|
49
|
+
|
50
|
+
becomes:
|
51
|
+
|
52
|
+
xml_attrs :in => 'Attributes' do |xml|
|
53
|
+
xml.reader :count, :as => Integer
|
54
|
+
xml.reader :something_else, :as => Date
|
55
|
+
end
|
56
|
+
|
57
|
+
* Ensure (perhaps optionally) that references are unambiguous. That is error/warn
|
58
|
+
a singular specification has multiple possible node references
|
59
59
|
|
60
60
|
* Use lazy evaluation to minimize parsing time for large files
|
61
61
|
|
62
|
-
* Consider class_inheritable_attribute rather than superclass.try stuff.
|
63
62
|
|
@@ -0,0 +1,70 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec/spec_helper')
|
3
|
+
require 'sqlite3'
|
4
|
+
require 'activerecord'
|
5
|
+
|
6
|
+
DB_PATH = File.join(File.dirname(__FILE__), 'active_record.sqlite3')
|
7
|
+
ActiveRecord::Base.establish_connection(
|
8
|
+
:adapter => 'sqlite3',
|
9
|
+
:database => DB_PATH
|
10
|
+
)
|
11
|
+
|
12
|
+
class Waypoint < ActiveRecord::Base
|
13
|
+
include ROXML
|
14
|
+
|
15
|
+
belongs_to :route
|
16
|
+
|
17
|
+
xml_attr :isLeg
|
18
|
+
xml_attr :lonlatx
|
19
|
+
xml_attr :lonlaty
|
20
|
+
xml_attr :gridReference
|
21
|
+
xml_attr :ascent
|
22
|
+
xml_attr :descent
|
23
|
+
xml_attr :distance
|
24
|
+
xml_attr :bearing
|
25
|
+
xml_attr :timemins
|
26
|
+
end
|
27
|
+
|
28
|
+
class Route < ActiveRecord::Base
|
29
|
+
include ROXML
|
30
|
+
|
31
|
+
has_many :waypoints
|
32
|
+
|
33
|
+
xml_attr :title
|
34
|
+
xml_attr :totalDist
|
35
|
+
xml_attr :totalMins
|
36
|
+
xml_attr :totalHg
|
37
|
+
xml_attr :lonlatx
|
38
|
+
xml_attr :lonlaty
|
39
|
+
xml_attr :grcenter
|
40
|
+
|
41
|
+
xml_attr :waypoints, :as => [Waypoint], :in => "waypoints"
|
42
|
+
end
|
43
|
+
|
44
|
+
# do a quick pseudo migration. This should only get executed on the first run
|
45
|
+
if !Waypoint.table_exists?
|
46
|
+
ActiveRecord::Base.connection.create_table(:waypoints) do |t|
|
47
|
+
t.column :route_id, :integer
|
48
|
+
t.column :isLeg, :string
|
49
|
+
t.column :lonlatx, :string
|
50
|
+
t.column :lonlaty, :string
|
51
|
+
t.column :gridReference, :string
|
52
|
+
t.column :ascent, :string
|
53
|
+
t.column :descent, :string
|
54
|
+
t.column :distance, :string
|
55
|
+
t.column :bearing, :string
|
56
|
+
t.column :timeMins, :string
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
if !Route.table_exists?
|
61
|
+
ActiveRecord::Base.connection.create_table(:routes) do |t|
|
62
|
+
t.column :title, :string
|
63
|
+
t.column :totalDist, :string
|
64
|
+
t.column :totalMins, :string
|
65
|
+
t.column :totalHg, :string
|
66
|
+
t.column :lonlatx, :string
|
67
|
+
t.column :lonlaty, :string
|
68
|
+
t.column :grcenter, :string
|
69
|
+
end
|
70
|
+
end
|
data/examples/amazon.rb
CHANGED
data/examples/current_weather.rb
CHANGED
data/examples/library.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
class Publisher
|
2
|
+
include ROXML
|
3
|
+
|
4
|
+
xml_accessor :name
|
5
|
+
|
6
|
+
def initialize(name = nil)
|
7
|
+
@name = name
|
8
|
+
end
|
9
|
+
|
10
|
+
def ==(other)
|
11
|
+
name == other.name
|
12
|
+
end
|
13
|
+
|
14
|
+
# other important functionality
|
15
|
+
end
|
16
|
+
|
17
|
+
class Novel
|
18
|
+
include ROXML
|
19
|
+
|
20
|
+
xml_accessor :isbn, :from => "@ISBN" # attribute with name 'ISBN'
|
21
|
+
xml_accessor :title
|
22
|
+
xml_accessor :description, :cdata => true # text node with cdata protection
|
23
|
+
xml_accessor :author
|
24
|
+
xml_accessor :publisher, :as => Publisher # singular object reference for illustrative purposes.
|
25
|
+
|
26
|
+
def ==(other)
|
27
|
+
self.class.roxml_attrs.map(&:accessor).all? {|attr| send(attr) == other.send(attr) }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Library
|
32
|
+
include ROXML
|
33
|
+
|
34
|
+
xml_accessor :name, :from => "NAME", :cdata => true
|
35
|
+
xml_accessor :novels, :as => [Novel] # by default roxml searches for books for in <novel> children, then, if none are present, in ./novels/novel children
|
36
|
+
|
37
|
+
def ==(other)
|
38
|
+
name == other.name && novels == other.novels
|
39
|
+
end
|
40
|
+
end
|