representable 0.0.1.alpha1

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 (103) hide show
  1. data/.gitignore +7 -0
  2. data/.gitmodules +3 -0
  3. data/.rspec +1 -0
  4. data/Gemfile +3 -0
  5. data/History.txt +354 -0
  6. data/LICENSE +20 -0
  7. data/README.rdoc +186 -0
  8. data/Rakefile +10 -0
  9. data/TODO +37 -0
  10. data/VERSION +1 -0
  11. data/examples/amazon.rb +35 -0
  12. data/examples/current_weather.rb +27 -0
  13. data/examples/dashed_elements.rb +20 -0
  14. data/examples/library.rb +40 -0
  15. data/examples/posts.rb +27 -0
  16. data/examples/rails.rb +70 -0
  17. data/examples/twitter.rb +37 -0
  18. data/examples/xml/active_record.xml +70 -0
  19. data/examples/xml/amazon.xml +133 -0
  20. data/examples/xml/current_weather.xml +89 -0
  21. data/examples/xml/dashed_elements.xml +52 -0
  22. data/examples/xml/posts.xml +23 -0
  23. data/examples/xml/twitter.xml +422 -0
  24. data/lib/representable.rb +257 -0
  25. data/lib/representable/definition.rb +109 -0
  26. data/lib/representable/nokogiri_extensions.rb +19 -0
  27. data/lib/representable/references.rb +153 -0
  28. data/lib/representable/version.rb +3 -0
  29. data/lib/representable/xml.rb +79 -0
  30. data/representable.gemspec +29 -0
  31. data/spec/definition_spec.rb +495 -0
  32. data/spec/examples/active_record_spec.rb +41 -0
  33. data/spec/examples/amazon_spec.rb +54 -0
  34. data/spec/examples/current_weather_spec.rb +37 -0
  35. data/spec/examples/dashed_elements_spec.rb +20 -0
  36. data/spec/examples/library_spec.rb +46 -0
  37. data/spec/examples/post_spec.rb +24 -0
  38. data/spec/examples/twitter_spec.rb +32 -0
  39. data/spec/roxml_integration_test.rb +289 -0
  40. data/spec/roxml_spec.rb +372 -0
  41. data/spec/shared_specs.rb +15 -0
  42. data/spec/spec_helper.rb +5 -0
  43. data/spec/support/libxml.rb +3 -0
  44. data/spec/support/nokogiri.rb +3 -0
  45. data/spec/xml/array_spec.rb +36 -0
  46. data/spec/xml/attributes_spec.rb +71 -0
  47. data/spec/xml/encoding_spec.rb +53 -0
  48. data/spec/xml/namespace_spec.rb +270 -0
  49. data/spec/xml/namespaces_spec.rb +67 -0
  50. data/spec/xml/object_spec.rb +82 -0
  51. data/spec/xml/parser_spec.rb +21 -0
  52. data/spec/xml/text_spec.rb +71 -0
  53. data/test/fixtures/book_malformed.xml +5 -0
  54. data/test/fixtures/book_pair.xml +8 -0
  55. data/test/fixtures/book_text_with_attribute.xml +5 -0
  56. data/test/fixtures/book_valid.xml +5 -0
  57. data/test/fixtures/book_with_authors.xml +7 -0
  58. data/test/fixtures/book_with_contributions.xml +9 -0
  59. data/test/fixtures/book_with_contributors.xml +7 -0
  60. data/test/fixtures/book_with_contributors_attrs.xml +7 -0
  61. data/test/fixtures/book_with_default_namespace.xml +9 -0
  62. data/test/fixtures/book_with_depth.xml +6 -0
  63. data/test/fixtures/book_with_octal_pages.xml +4 -0
  64. data/test/fixtures/book_with_publisher.xml +7 -0
  65. data/test/fixtures/book_with_wrapped_attr.xml +3 -0
  66. data/test/fixtures/dictionary_of_attr_name_clashes.xml +8 -0
  67. data/test/fixtures/dictionary_of_attrs.xml +6 -0
  68. data/test/fixtures/dictionary_of_guarded_names.xml +6 -0
  69. data/test/fixtures/dictionary_of_mixeds.xml +4 -0
  70. data/test/fixtures/dictionary_of_name_clashes.xml +10 -0
  71. data/test/fixtures/dictionary_of_names.xml +4 -0
  72. data/test/fixtures/dictionary_of_texts.xml +10 -0
  73. data/test/fixtures/library.xml +30 -0
  74. data/test/fixtures/library_uppercase.xml +30 -0
  75. data/test/fixtures/muffins.xml +3 -0
  76. data/test/fixtures/nameless_ageless_youth.xml +2 -0
  77. data/test/fixtures/node_with_attr_name_conflicts.xml +1 -0
  78. data/test/fixtures/node_with_name_conflicts.xml +4 -0
  79. data/test/fixtures/numerology.xml +4 -0
  80. data/test/fixtures/person.xml +1 -0
  81. data/test/fixtures/person_with_guarded_mothers.xml +13 -0
  82. data/test/fixtures/person_with_mothers.xml +10 -0
  83. data/test/mocks/dictionaries.rb +57 -0
  84. data/test/mocks/mocks.rb +279 -0
  85. data/test/roxml_test.rb +58 -0
  86. data/test/support/fixtures.rb +11 -0
  87. data/test/test_helper.rb +6 -0
  88. data/test/unit/definition_test.rb +235 -0
  89. data/test/unit/deprecations_test.rb +24 -0
  90. data/test/unit/to_xml_test.rb +81 -0
  91. data/test/unit/xml_attribute_test.rb +39 -0
  92. data/test/unit/xml_block_test.rb +81 -0
  93. data/test/unit/xml_bool_test.rb +122 -0
  94. data/test/unit/xml_convention_test.rb +150 -0
  95. data/test/unit/xml_hash_test.rb +115 -0
  96. data/test/unit/xml_initialize_test.rb +49 -0
  97. data/test/unit/xml_name_test.rb +141 -0
  98. data/test/unit/xml_namespace_test.rb +31 -0
  99. data/test/unit/xml_object_test.rb +206 -0
  100. data/test/unit/xml_required_test.rb +94 -0
  101. data/test/unit/xml_text_test.rb +71 -0
  102. data/website/index.html +98 -0
  103. metadata +248 -0
@@ -0,0 +1,257 @@
1
+ require 'active_support'
2
+ require 'active_support/core_ext/module/delegation'
3
+ require 'active_support/core_ext/array/extract_options'
4
+ require 'active_support/core_ext/string/starts_ends_with'
5
+ require 'active_support/core_ext/string/inflections.rb'
6
+ require 'active_support/core_ext/hash/reverse_merge.rb'
7
+
8
+ require 'hooks/inheritable_attribute'
9
+
10
+
11
+ require 'representable/definition'
12
+ require 'representable/nokogiri_extensions'
13
+ require 'representable/references'
14
+
15
+ require 'representable/xml' # TODO: do that dynamically.
16
+
17
+ module Representable
18
+ VERSION = '3.1.5'
19
+
20
+
21
+ def self.included(base)
22
+ base.class_eval do
23
+ extend ClassMethods::Accessors, ClassMethods::Declarations
24
+
25
+
26
+
27
+ attr_accessor :roxml_references
28
+
29
+ extend Hooks::InheritableAttribute
30
+ inheritable_attr :representable_attrs
31
+ self.representable_attrs = []
32
+
33
+ inheritable_attr :explicit_representation_name # FIXME: move to Accessors.
34
+
35
+
36
+ extend Xml::Declarations # DISCUSS: do that dynamically?
37
+ extend Xml::ClassMethods # DISCUSS: do that dynamically?
38
+ include Xml::InstanceMethods # DISCUSS: do that dynamically?
39
+ end
40
+ end
41
+
42
+ module ClassMethods # :nodoc:
43
+ module Declarations
44
+ def definition_class
45
+ Definition
46
+ end
47
+
48
+
49
+ # Declares a reference to a certain xml element, whether an attribute, a node,
50
+ # or a typed collection of nodes. This method does not add a corresponding accessor
51
+ # to the object. For that behavior see the similar methods: .xml_reader and .xml_accessor.
52
+ #
53
+ # == Sym Option
54
+ # [sym] Symbol representing the name of the accessor.
55
+ #
56
+ # === Default naming
57
+ # This name will be the default node or attribute name searched for,
58
+ # if no other is declared. For example,
59
+ #
60
+ # xml_reader :bob
61
+ # xml_accessor :pony, :from => :attr
62
+ #
63
+ # are equivalent to:
64
+ #
65
+ # xml_reader :bob, :from => 'bob'
66
+ # xml_accessor :pony, :from => '@pony'
67
+ #
68
+ # == Options
69
+ # === :as
70
+ # ==== Basic Types
71
+ # Allows you to specify one of several basic types to return the value as. For example
72
+ #
73
+ # xml_reader :count, :as => Integer
74
+ #
75
+ # is equivalent to:
76
+ #
77
+ # xml_reader(:count) {|val| Integer(val) unless val.empty? }
78
+ #
79
+ # Such block shorthands for Integer, Float, Fixnum, BigDecimal, Date, Time, and DateTime
80
+ # are currently available, but only for non-Hash declarations.
81
+ #
82
+ # To reference many elements, put the desired type in a literal array. e.g.:
83
+ #
84
+ # xml_reader :counts, :as => [Integer]
85
+ #
86
+ # Even an array of text nodes can be specified with :as => []
87
+ #
88
+ # xml_reader :quotes, :as => []
89
+ #
90
+ # === Other ROXML Class
91
+ # Declares an accessor that represents another ROXML class as child XML element
92
+ # (one-to-one or composition) or array of child elements (one-to-many or
93
+ # aggregation) of this type. Default is one-to-one. For one-to-many, simply pass the class
94
+ # as the only element in an array.
95
+ #
96
+ # Composition example:
97
+ # <book>
98
+ # <publisher>
99
+ # <name>Pragmatic Bookshelf</name>
100
+ # </publisher>
101
+ # </book>
102
+ #
103
+ # Can be mapped using the following code:
104
+ # class Book
105
+ # xml_reader :publisher, :as => Publisher
106
+ # end
107
+ #
108
+ # Aggregation example:
109
+ # <library>
110
+ # <books>
111
+ # <book/>
112
+ # <book/>
113
+ # </books>
114
+ # </library>
115
+ #
116
+ # Can be mapped using the following code:
117
+ # class Library
118
+ # xml_reader :books, :as => [Book], :in => "books"
119
+ # end
120
+ #
121
+ # If you don't have the <books> tag to wrap around the list of <book> tags:
122
+ # <library>
123
+ # <name>Ruby books</name>
124
+ # <book/>
125
+ # <book/>
126
+ # </library>
127
+ #
128
+ # You can skip the wrapper argument:
129
+ # xml_reader :books, :as => [Book]
130
+ #
131
+ # === :from
132
+ # The name by which the xml value will be found, either an attribute or tag name in XML.
133
+ # Default is sym, or the singular form of sym, in the case of arrays and hashes.
134
+ #
135
+ # This value may also include XPath notation.
136
+ #
137
+ # ==== :from => :content
138
+ # When :from is set to :content, this refers to the content of the current node,
139
+ # rather than a sub-node. It is equivalent to :from => '.'
140
+ #
141
+ # Example:
142
+ # class Contributor
143
+ # xml_reader :name, :from => :content
144
+ # xml_reader :role, :from => :attr
145
+ # end
146
+ #
147
+ # To map:
148
+ # <contributor role="editor">James Wick</contributor>
149
+ #
150
+ # ==== :from => :attr
151
+ # When :from is set to :attr, this refers to the content of an attribute,
152
+ # rather than a sub-node. It is equivalent to :from => '@attribute_name'
153
+ #
154
+ # Example:
155
+ # class Book
156
+ # xml_reader :isbn, :from => "@ISBN"
157
+ # xml_accessor :title, :from => :attr # :from defaults to '@title'
158
+ # end
159
+ #
160
+ # To map:
161
+ # <book ISBN="0974514055" title="Programming Ruby: the pragmatic programmers' guide" />
162
+ #
163
+ # ==== :from => :text
164
+ # The default source, if none is specified, this means the accessor
165
+ # represents a text node from XML. This is documented for completeness
166
+ # only. You should just leave this option off when you want the default behavior,
167
+ # as in the examples below.
168
+ #
169
+ # :text is equivalent to :from => accessor_name, and you should specify the
170
+ # actual node name (and, optionally, a namespace) if it differs, as in the case of :author below.
171
+ #
172
+ # Example:
173
+ # class Book
174
+ # xml_reader :author, :from => 'Author'
175
+ # xml_accessor :description, :cdata => true
176
+ # xml_reader :title
177
+ # end
178
+ #
179
+ # To map:
180
+ # <book>
181
+ # <title>Programming Ruby: the pragmatic programmers' guide</title>
182
+ # <description><![CDATA[Probably the best Ruby book out there]]></description>
183
+ # <Author>David Thomas</Author>
184
+ # </book>
185
+ #
186
+ # Likewise, a number of :text node values can be collected in an array like so:
187
+ #
188
+ # Example:
189
+ # class Library
190
+ # xml_reader :books, :as => []
191
+ # end
192
+ #
193
+ # To map:
194
+ # <library>
195
+ # <book>To kill a mockingbird</book>
196
+ # <book>House of Leaves</book>
197
+ # <book>Gödel, Escher, Bach</book>
198
+ # </library>
199
+ #
200
+ # === Other Options
201
+ # [:in] An optional name of a wrapping tag for this XML accessor.
202
+ # This can include other xpath values, which will be joined with :from with a '/'
203
+ # [:required] If true, throws RequiredElementMissing when the element isn't present
204
+ # [:cdata] true for values which should be input from or output as cdata elements
205
+ # [:to_xml] this proc is applied to the attributes value outputting the instance via #to_xml
206
+ #
207
+ def representable_attr(*syms, &block)
208
+ opts = syms.extract_options!
209
+ syms.map do |sym|
210
+ definition_class.new(sym, opts, &block).tap do |attr|
211
+ representable_attrs << attr
212
+ end
213
+ end
214
+ end
215
+
216
+ # Declares a read-only xml reference. See xml_attr for details.
217
+ #
218
+ # Note that while xml_reader does not create a setter for this attribute,
219
+ # its value can be modified indirectly via methods. For more complete
220
+ # protection, consider the :frozen option.
221
+ def representable_reader(*syms, &block)
222
+ representable_attr(*syms, &block).each do |attr|
223
+ add_reader(attr)
224
+ end
225
+ end
226
+
227
+ # Declares a writable xml reference. See xml_attr for details.
228
+ #
229
+ # Note that while xml_accessor does create a setter for this attribute,
230
+ # you can use the :frozen option to prevent its value from being
231
+ # modified indirectly via methods.
232
+ def representable_accessor(*syms, &block)
233
+ representable_attr(*syms, &block).each do |attr|
234
+ add_reader(attr)
235
+ attr_writer(attr.accessor)
236
+ end
237
+ end
238
+
239
+ private
240
+ def add_reader(attr)
241
+ define_method(attr.accessor) do
242
+ instance_variable_get(attr.instance_variable_name)
243
+ end
244
+ end
245
+ end
246
+
247
+ module Accessors
248
+ def representation_name=(name)
249
+ self.explicit_representation_name = name
250
+ end
251
+
252
+ def representation_name
253
+ explicit_representation_name or name.split('::').last.underscore
254
+ end
255
+ end
256
+ end
257
+ end
@@ -0,0 +1,109 @@
1
+ class Module
2
+ def bool_attr_reader(*attrs)
3
+ attrs.each do |attr|
4
+ define_method :"#{attr}?" do
5
+ instance_variable_get(:"@#{attr}") || false
6
+ end
7
+ end
8
+ end
9
+ end
10
+
11
+ module Representable
12
+ class ContradictoryNamespaces < StandardError
13
+ end
14
+ class Definition # :nodoc:
15
+ attr_reader :name, :sought_type, :wrapper, :accessor, :namespace
16
+ bool_attr_reader :name_explicit, :array, :cdata
17
+
18
+ def initialize(sym, opts={})
19
+ @accessor = sym.to_s
20
+ @namespace = opts.delete(:namespace)
21
+
22
+
23
+ if opts[:as].is_a?(Array) # DISCUSS: move to ArrayDefinition.
24
+ @array = true
25
+ @name = (opts[:tag] || @accessor).to_s
26
+ else
27
+ @name = accessor
28
+ @name = (opts[:from] || @name).to_s
29
+ end
30
+
31
+ @sought_type = extract_type(opts[:as])
32
+ if @sought_type.respond_to?(:roxml_tag_name)
33
+ opts[:from] ||= @sought_type.roxml_tag_name
34
+ end
35
+
36
+ if opts[:from] == :content
37
+ opts[:from] = '.'
38
+ elsif opts[:from] == :name
39
+ opts[:from] = '*'
40
+ elsif opts[:from] == :attr
41
+ @sought_type = :attr
42
+ opts[:from] = nil
43
+ elsif opts[:from] == :namespace
44
+ opts[:from] = '*'
45
+ @sought_type = :namespace
46
+ elsif opts[:from].to_s.starts_with?('@')
47
+ @sought_type = :attr
48
+ opts[:from].sub!('@', '')
49
+ end
50
+
51
+
52
+ #raise ContradictoryNamespaces if @name.include?(':') && (@namespace.present? || @namespace == false)
53
+
54
+ end
55
+
56
+ def instance_variable_name
57
+ :"@#{accessor}"
58
+ end
59
+
60
+ def setter
61
+ :"#{accessor}="
62
+ end
63
+
64
+ def typed?
65
+ sought_type.is_a?(Class)
66
+ end
67
+
68
+
69
+ def name?
70
+ @name == '*'
71
+ end
72
+
73
+ def content?
74
+ @name == '.'
75
+ end
76
+
77
+ # Applies the block to +value+ which might also be a collection.
78
+ def apply(value)
79
+ return value unless value # DISCUSS: is that ok here?
80
+
81
+ if array?
82
+ value = value.collect do |item|
83
+ yield item
84
+ end
85
+ else
86
+ value = yield value
87
+ end
88
+
89
+ value
90
+ end
91
+
92
+ def to_ref
93
+ case sought_type
94
+ when :attr then XMLAttributeRef
95
+ when :text then XMLTextRef
96
+ when :namespace then XMLNameSpaceRef
97
+ when Symbol then raise ArgumentError, "Invalid type argument #{sought_type}"
98
+ else XMLObjectRef
99
+ end.new(self)
100
+ end
101
+
102
+ private
103
+ def extract_type(as)
104
+ as = as.first if as.is_a?(Array) # TODO: move to ArrayDefinition.
105
+
106
+ as || :text
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,19 @@
1
+ require 'nokogiri'
2
+
3
+ Nokogiri::XML::Node.class_eval do
4
+ def add_node(name)
5
+ add_child Nokogiri::XML::Node.new(name, document)
6
+ end
7
+
8
+ # FIXME: remove switch. where is #from used with nodes?
9
+ def self.from(data)
10
+ case data
11
+ when Nokogiri::XML::Node
12
+ data
13
+ when Nokogiri::XML::Document
14
+ data.root
15
+ else
16
+ Nokogiri::XML(data).root
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,153 @@
1
+ module Representable
2
+ class RequiredElementMissing < Exception # :nodoc:
3
+ end
4
+
5
+ # Internal base class that represents an XML - Class binding.
6
+ class XMLRef
7
+ attr_reader :definition
8
+ delegate :required?, :array?, :accessor, :wrapper, :name, :to => :definition
9
+
10
+ def initialize(definition)
11
+ @definition = definition
12
+ end
13
+
14
+ def value_in(xml)
15
+ xml = Nokogiri::XML::Node.from(xml) or return default
16
+
17
+ value_from_node(xml) or default
18
+ end
19
+
20
+ private
21
+ def default
22
+ ""
23
+ end
24
+
25
+ def xpath
26
+ name
27
+ end
28
+
29
+ def wrap(xml, opts = {:always_create => false})
30
+ wrap_with = @auto_vals ? auto_wrapper : wrapper
31
+
32
+ return xml if !wrap_with || xml.name == wrap_with
33
+ if !opts[:always_create] && (child = xml.children.find {|c| c.name == wrap_with })
34
+ return child
35
+ end
36
+ xml.add_node(wrap_with.to_s)
37
+ end
38
+
39
+ def collect_for(xml)
40
+ nodes = xml.search("./#{xpath}")
41
+ vals = nodes.collect { |node| yield node }
42
+
43
+ array? ? vals : vals.first
44
+ end
45
+ end
46
+
47
+ # Represents a tag attribute.
48
+ class XMLAttributeRef < XMLRef
49
+ # Updates the attribute in the given XML block to
50
+ # the value provided.
51
+ def update_xml(xml, values)
52
+ wrap(xml).tap do |xml|
53
+ xml[name] = values.to_s
54
+ end
55
+ end
56
+
57
+ private
58
+ def value_from_node(xml)
59
+ xml[name]
60
+ end
61
+ end
62
+
63
+ # Represents text content in a tag.
64
+ class XMLTextRef < XMLRef
65
+ delegate :cdata?, :content?, :name?, :to => :definition
66
+
67
+ # Updates the text in the given _xml_ block to
68
+ # the _value_ provided.
69
+ def update_xml(xml, value)
70
+ wrap(xml).tap do |xml|
71
+ if content?
72
+ add(xml, value)
73
+ elsif name?
74
+ xml.name = value
75
+ elsif array?
76
+ value.each do |v|
77
+ add(xml.add_node(name), v)
78
+ end
79
+ else
80
+ add(xml.add_node(name), value)
81
+ end
82
+ end
83
+ end
84
+
85
+ private
86
+ def value_from_node(xml)
87
+ collect_for(xml) do |node|
88
+ node.content
89
+ end
90
+ end
91
+
92
+ def add(dest, value)
93
+ if cdata?
94
+ dest.add_child(Nokogiri::XML::CDATA.new(dest.document, content))
95
+ else
96
+ dest.content = value.to_s
97
+ end
98
+ end
99
+ end
100
+
101
+ class XMLNameSpaceRef < XMLRef
102
+ private
103
+ def value_from_node(xml)
104
+ xml.namespace.prefix
105
+ end
106
+ end
107
+
108
+ # Represents a tag with object binding.
109
+ class XMLObjectRef < XMLTextRef
110
+ delegate :sought_type, :to => :definition
111
+
112
+ # Adds the ref's markup to +xml+.
113
+ def update_xml(xml, value)
114
+ wrap(xml).tap do |xml|
115
+ if array?
116
+ update_xml_for_collection(xml, value)
117
+ else
118
+ update_xml_for_entity(xml, value)
119
+ end
120
+ end
121
+ end
122
+
123
+ private
124
+ def default
125
+ []
126
+ end
127
+
128
+ def serialize(object)
129
+ object.to_xml
130
+ end
131
+
132
+ def deserialize(node_class, xml)
133
+ node_class.from_xml(xml)
134
+ end
135
+
136
+ # Deserializes the ref's element from +xml+.
137
+ def value_from_node(xml)
138
+ collect_for(xml) do |node|
139
+ deserialize(sought_type, node)
140
+ end
141
+ end
142
+
143
+ def update_xml_for_collection(xml, collection)
144
+ collection.each do |item|
145
+ update_xml_for_entity(xml, item)
146
+ end
147
+ end
148
+
149
+ def update_xml_for_entity(xml, entity)
150
+ xml.add_child(serialize(entity))
151
+ end
152
+ end
153
+ end