representable 0.0.1.alpha1

Sign up to get free protection for your applications and to get access to all the features.
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