odata4 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (114) hide show
  1. checksums.yaml +7 -0
  2. data/.autotest +2 -0
  3. data/.gitignore +24 -0
  4. data/.rspec +2 -0
  5. data/.ruby-gemset +1 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +75 -0
  8. data/CHANGELOG.md +120 -0
  9. data/Gemfile +4 -0
  10. data/LICENSE.txt +23 -0
  11. data/README.md +287 -0
  12. data/Rakefile +7 -0
  13. data/TODO.md +55 -0
  14. data/lib/odata4.rb +37 -0
  15. data/lib/odata4/complex_type.rb +76 -0
  16. data/lib/odata4/complex_type/property.rb +114 -0
  17. data/lib/odata4/entity.rb +319 -0
  18. data/lib/odata4/entity_set.rb +162 -0
  19. data/lib/odata4/enum_type.rb +95 -0
  20. data/lib/odata4/enum_type/property.rb +62 -0
  21. data/lib/odata4/navigation_property.rb +29 -0
  22. data/lib/odata4/navigation_property/proxy.rb +76 -0
  23. data/lib/odata4/properties.rb +25 -0
  24. data/lib/odata4/properties/binary.rb +50 -0
  25. data/lib/odata4/properties/boolean.rb +37 -0
  26. data/lib/odata4/properties/date.rb +27 -0
  27. data/lib/odata4/properties/date_time.rb +83 -0
  28. data/lib/odata4/properties/date_time_offset.rb +17 -0
  29. data/lib/odata4/properties/decimal.rb +50 -0
  30. data/lib/odata4/properties/float.rb +67 -0
  31. data/lib/odata4/properties/geography.rb +13 -0
  32. data/lib/odata4/properties/geography/base.rb +162 -0
  33. data/lib/odata4/properties/geography/line_string.rb +33 -0
  34. data/lib/odata4/properties/geography/point.rb +31 -0
  35. data/lib/odata4/properties/geography/polygon.rb +38 -0
  36. data/lib/odata4/properties/guid.rb +17 -0
  37. data/lib/odata4/properties/integer.rb +107 -0
  38. data/lib/odata4/properties/number.rb +14 -0
  39. data/lib/odata4/properties/string.rb +72 -0
  40. data/lib/odata4/properties/time.rb +40 -0
  41. data/lib/odata4/properties/time_of_day.rb +27 -0
  42. data/lib/odata4/property.rb +118 -0
  43. data/lib/odata4/property_registry.rb +41 -0
  44. data/lib/odata4/query.rb +231 -0
  45. data/lib/odata4/query/criteria.rb +92 -0
  46. data/lib/odata4/query/criteria/comparison_operators.rb +49 -0
  47. data/lib/odata4/query/criteria/date_functions.rb +61 -0
  48. data/lib/odata4/query/criteria/geography_functions.rb +21 -0
  49. data/lib/odata4/query/criteria/lambda_operators.rb +27 -0
  50. data/lib/odata4/query/criteria/string_functions.rb +40 -0
  51. data/lib/odata4/query/in_batches.rb +58 -0
  52. data/lib/odata4/query/result.rb +84 -0
  53. data/lib/odata4/query/result/atom.rb +41 -0
  54. data/lib/odata4/query/result/json.rb +42 -0
  55. data/lib/odata4/railtie.rb +19 -0
  56. data/lib/odata4/service.rb +344 -0
  57. data/lib/odata4/service_registry.rb +52 -0
  58. data/lib/odata4/version.rb +3 -0
  59. data/odata4.gemspec +34 -0
  60. data/spec/fixtures/files/entity_to_xml.xml +17 -0
  61. data/spec/fixtures/files/metadata.xml +150 -0
  62. data/spec/fixtures/files/product_0.json +10 -0
  63. data/spec/fixtures/files/product_0.xml +28 -0
  64. data/spec/fixtures/files/products.json +106 -0
  65. data/spec/fixtures/files/products.xml +308 -0
  66. data/spec/fixtures/files/supplier_0.json +26 -0
  67. data/spec/fixtures/files/supplier_0.xml +32 -0
  68. data/spec/fixtures/vcr_cassettes/complex_type_specs.yml +127 -0
  69. data/spec/fixtures/vcr_cassettes/entity_set_specs.yml +1348 -0
  70. data/spec/fixtures/vcr_cassettes/entity_set_specs/bad_entry.yml +183 -0
  71. data/spec/fixtures/vcr_cassettes/entity_set_specs/existing_entry.yml +256 -0
  72. data/spec/fixtures/vcr_cassettes/entity_set_specs/new_entry.yml +185 -0
  73. data/spec/fixtures/vcr_cassettes/entity_specs.yml +285 -0
  74. data/spec/fixtures/vcr_cassettes/navigation_property_proxy_specs.yml +346 -0
  75. data/spec/fixtures/vcr_cassettes/query/result_specs.yml +189 -0
  76. data/spec/fixtures/vcr_cassettes/query_specs.yml +663 -0
  77. data/spec/fixtures/vcr_cassettes/service_registry_specs.yml +129 -0
  78. data/spec/fixtures/vcr_cassettes/service_specs.yml +127 -0
  79. data/spec/fixtures/vcr_cassettes/usage_example_specs.yml +749 -0
  80. data/spec/odata4/complex_type_spec.rb +116 -0
  81. data/spec/odata4/entity/shared_examples.rb +82 -0
  82. data/spec/odata4/entity_set_spec.rb +168 -0
  83. data/spec/odata4/entity_spec.rb +151 -0
  84. data/spec/odata4/enum_type_spec.rb +134 -0
  85. data/spec/odata4/navigation_property/proxy_spec.rb +44 -0
  86. data/spec/odata4/navigation_property_spec.rb +55 -0
  87. data/spec/odata4/properties/binary_spec.rb +50 -0
  88. data/spec/odata4/properties/boolean_spec.rb +72 -0
  89. data/spec/odata4/properties/date_spec.rb +23 -0
  90. data/spec/odata4/properties/date_time_offset_spec.rb +30 -0
  91. data/spec/odata4/properties/date_time_spec.rb +23 -0
  92. data/spec/odata4/properties/decimal_spec.rb +24 -0
  93. data/spec/odata4/properties/float_spec.rb +45 -0
  94. data/spec/odata4/properties/geography/line_string_spec.rb +33 -0
  95. data/spec/odata4/properties/geography/point_spec.rb +29 -0
  96. data/spec/odata4/properties/geography/polygon_spec.rb +55 -0
  97. data/spec/odata4/properties/geography/shared_examples.rb +72 -0
  98. data/spec/odata4/properties/guid_spec.rb +17 -0
  99. data/spec/odata4/properties/integer_spec.rb +58 -0
  100. data/spec/odata4/properties/string_spec.rb +46 -0
  101. data/spec/odata4/properties/time_of_day_spec.rb +23 -0
  102. data/spec/odata4/properties/time_spec.rb +15 -0
  103. data/spec/odata4/property_registry_spec.rb +16 -0
  104. data/spec/odata4/property_spec.rb +32 -0
  105. data/spec/odata4/query/criteria_spec.rb +229 -0
  106. data/spec/odata4/query/result_spec.rb +53 -0
  107. data/spec/odata4/query_spec.rb +196 -0
  108. data/spec/odata4/service_registry_spec.rb +18 -0
  109. data/spec/odata4/service_spec.rb +80 -0
  110. data/spec/odata4/usage_example_spec.rb +176 -0
  111. data/spec/spec_helper.rb +32 -0
  112. data/spec/support/coverage.rb +2 -0
  113. data/spec/support/vcr.rb +9 -0
  114. metadata +380 -0
@@ -0,0 +1,7 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
7
+
data/TODO.md ADDED
@@ -0,0 +1,55 @@
1
+ # OData V4 To-Do
2
+
3
+ This is a non-complete list of things that need to be done in order to achieve OData V4 compatibility. It will be updated regularly to keep track with current development.
4
+
5
+ ## Tasks
6
+
7
+ [x] `DataServiceVersion` headers changes to `OData-Version`
8
+ [x] Atom: update namespace URIs
9
+ [x] Implement JSON data format
10
+ [x] with batch processing
11
+ [ ] Implement missing/new OData V4 types
12
+ [x] `Edm.Date` (V4/RESO)
13
+ [ ] `Edm.Duration` (V4)
14
+ [x] `Edm.TimeOfDay` (V4/RESO)
15
+ [x] `Edm.EnumType` (V4/RESO)
16
+ [ ] `Edm.Geography` subtypes (RESO)
17
+ [x] `Edm.GeographyPoint`
18
+ [ ] `Edm.GeographyMultiPoint`
19
+ [x] `Edm.GeographyLineString`
20
+ [ ] `Edm.GeographyMultiLineString`
21
+ [x] `Edm.GeographyPolygon` (see note below)
22
+ [ ] Support for holes
23
+ [ ] Support for other serialization formats
24
+ [ ] `Edm.GeopgrahyMultiPolygon`
25
+
26
+ ##### NOTE
27
+
28
+ Due to the lack of library support for GeoXML/GML in Ruby, Geography support is somewhat limited. For instance, [there are more than 3 different ways to represent a polygon in GML][gml-madness], all of which are equivalent and interchangeable. However, due to the lack of GML libraries, we currently only support a single serialization format (`<gml:LinearRing>` with `<gml:pos>` elements, see [polygon_spec.rb][polygon_spec]).
29
+
30
+ [gml-madness]: http://erouault.blogspot.com/2014/04/gml-madness.html
31
+ [polygon_spec]: spec/odata/v4/properties/geography/polygon_spec.rb
32
+
33
+ [x] Changes to `NavigationProperty`
34
+ [x] No more associations (but we probably still need a proxy class)
35
+ [x] New `Type` property
36
+ [x] New `Nullable` property
37
+ [x] New `Partner` property
38
+ [ ] New `ContainsTarget` property
39
+
40
+ [ ] Changes to querying
41
+ [x] `$count=true` replaces `$inlinecount=allpages`
42
+ [x] New `$search` param for fulltext search
43
+ [x] String functions
44
+ [x] Date/time functions
45
+ [x] Geospatial functions
46
+ [x] [Lambda operators][1]
47
+
48
+ [ ] Logging
49
+
50
+ [1]: http://docs.oasis-open.org/odata/odata/v4.0/errata02/os/complete/part2-url-conventions/odata-v4.0-errata02-os-part2-url-conventions-complete.html#_Toc406398149
51
+
52
+ ## Questions / Thoughts
53
+
54
+ [ ] Use standard JSON parser or OJ (or offer choice?)
55
+ [x] Continue to support XML data format (JSON is recommended for V4)? -> We'll support both, ATOM first, JSON to be added later.
@@ -0,0 +1,37 @@
1
+ require 'uri'
2
+ require 'date'
3
+ require 'time'
4
+ require 'bigdecimal'
5
+ require 'nokogiri'
6
+ require 'typhoeus'
7
+ require 'andand'
8
+ require 'json'
9
+
10
+ # require 'active_support'
11
+ # require 'active_support/core_ext'
12
+ # require 'active_support/concern'
13
+
14
+ require 'odata4/version'
15
+ require 'odata4/property_registry'
16
+ require 'odata4/property'
17
+ require 'odata4/properties'
18
+ require 'odata4/navigation_property'
19
+ require 'odata4/complex_type'
20
+ require 'odata4/enum_type'
21
+ require 'odata4/entity'
22
+ require 'odata4/entity_set'
23
+ require 'odata4/query/criteria'
24
+ require 'odata4/query/result'
25
+ require 'odata4/query/in_batches'
26
+ require 'odata4/query'
27
+ require 'odata4/service'
28
+ require 'odata4/service_registry'
29
+
30
+ require 'odata4/railtie' if defined?(::Rails)
31
+
32
+ # The OData4 gem provides a convenient way to interact with OData4 services from
33
+ # Ruby. Please look to the {file:README.md README} for how to get started using
34
+ # the OData4 gem.
35
+ module OData4
36
+ # Your code goes here...
37
+ end
@@ -0,0 +1,76 @@
1
+ require 'odata4/complex_type/property'
2
+
3
+ module OData4
4
+ # ComplexTypes are used in OData4 to either encapsulate richer data types for
5
+ # use as Entity properties. ComplexTypes are composed of properties the same
6
+ # way that Entities are and, so, the interface for working with the various
7
+ # properties of a ComplexType mimics that of Entities.
8
+ class ComplexType
9
+ # Creates a new ComplexType based on the supplied options.
10
+ # @param type_xml [Nokogiri::XML::Element]
11
+ # @param service [OData4::Service]
12
+ # @return [self]
13
+ def initialize(type_definition, service)
14
+ @type_definition = type_definition
15
+ @service = service
16
+ end
17
+
18
+ # The name of the ComplexType
19
+ # @return [String]
20
+ def name
21
+ @name ||= type_definition.attributes['Name'].value
22
+ end
23
+
24
+ # Returns the namespaced type for the ComplexType.
25
+ # @return [String]
26
+ def type
27
+ "#{namespace}.#{name}"
28
+ end
29
+
30
+ # Returns the namespace this ComplexType belongs to.
31
+ # @return [String]
32
+ def namespace
33
+ @namespace ||= service.namespace
34
+ end
35
+
36
+ # Returns this ComplexType's properties.
37
+ # @return [Hash<String, OData4::Property>]
38
+ def properties
39
+ @properties ||= collect_properties
40
+ end
41
+
42
+ # Returns a list of this ComplexType's property names.
43
+ # @return [Array<String>]
44
+ def property_names
45
+ @property_names ||= properties.keys
46
+ end
47
+
48
+ # Returns the property class that implements this `ComplexType`.
49
+ # @return [Class < OData4::ComplexType::Property]
50
+ def property_class
51
+ @property_class ||= lambda { |type, complex_type|
52
+ klass = Class.new ::OData4::ComplexType::Property
53
+ klass.send(:define_method, :type) { type }
54
+ klass.send(:define_method, :complex_type) { complex_type }
55
+ klass
56
+ }.call(type, self)
57
+ end
58
+
59
+ private
60
+
61
+ def service
62
+ @service
63
+ end
64
+
65
+ def type_definition
66
+ @type_definition
67
+ end
68
+
69
+ def collect_properties
70
+ Hash[type_definition.xpath('./Property').map do |property_xml|
71
+ property_name, property = service.send(:process_property_from_xml,property_xml)
72
+ [property_name, property]
73
+ end]
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,114 @@
1
+ module OData4
2
+ class ComplexType
3
+ # Abstract base class for OData4 ComplexTypes
4
+ # @see [OData4::ComplexType]
5
+ class Property < OData4::Property
6
+ def initialize(name, value, options = {})
7
+ super(name, value, options)
8
+ init_properties
9
+ end
10
+
11
+ # Returns the property value, properly typecast
12
+ # @return [Hash, nil]
13
+ def value
14
+ if allows_nil? && properties.values.all?(&:nil?)
15
+ nil
16
+ else
17
+ Hash[properties.map { |key, value| [key, value.value] }]
18
+ end
19
+ end
20
+
21
+ # Sets the property value
22
+ # @params new_value [Hash]
23
+ def value=(new_value)
24
+ validate(new_value)
25
+ if new_value.nil?
26
+ property_names.each { |name| self[name] = nil }
27
+ else
28
+ property_names.each { |name| self[name] = new_value[name] }
29
+ end
30
+ end
31
+
32
+ # Returns a list of this ComplexType's property names.
33
+ # @return [Array<String>]
34
+ def property_names
35
+ @property_names ||= properties.keys
36
+ end
37
+
38
+ # Returns the value of the requested property.
39
+ # @param property_name [to_s]
40
+ # @return [*]
41
+ def [](property_name)
42
+ properties[property_name.to_s].value
43
+ end
44
+
45
+ # Sets the value of the named property.
46
+ # @param property_name [to_s]
47
+ # @param value [*]
48
+ # @return [*]
49
+ def []=(property_name, value)
50
+ properties[property_name.to_s].value = value
51
+ end
52
+
53
+ # Returns the XML representation of the property to the supplied XML
54
+ # builder.
55
+ # @param xml_builder [Nokogiri::XML::Builder]
56
+ def to_xml(xml_builder)
57
+ attributes = {
58
+ 'metadata:type' => type,
59
+ }
60
+
61
+ xml_builder['data'].send(name.to_sym, attributes) do
62
+ properties.each do |name, property|
63
+ property.to_xml(xml_builder)
64
+ end
65
+ end
66
+ end
67
+
68
+ # Creates a new property instance from an XML element
69
+ # @param property_xml [Nokogiri::XML::Element]
70
+ # @param options [Hash]
71
+ # @return [OData4::Property]
72
+ def self.from_xml(property_xml, options = {})
73
+ nodes = property_xml.element_children
74
+ props = Hash[nodes.map { |el| [el.name, el.content] }]
75
+ new(property_xml.name, props.to_json, options)
76
+ end
77
+
78
+ private
79
+
80
+ def complex_type
81
+ raise NotImplementedError, 'Subclass must override'
82
+ end
83
+
84
+ def properties
85
+ @properties
86
+ end
87
+
88
+ def init_properties
89
+ @properties = complex_type.send(:collect_properties)
90
+ set_properties(JSON.parse(@value)) unless @value.nil?
91
+ end
92
+
93
+ def set_properties(new_properties)
94
+ property_names.each do |prop_name|
95
+ self[prop_name] = new_properties[prop_name]
96
+ end
97
+ end
98
+
99
+ def validate(value)
100
+ return if value.nil? && allows_nil?
101
+ raise ArgumentError, 'Value must be a Hash' unless value.is_a?(Hash)
102
+ value.keys.each do |name|
103
+ unless property_names.include?(name) || name =~ /@odata/
104
+ raise ArgumentError, "Invalid property #{name}"
105
+ end
106
+ end
107
+ end
108
+
109
+ def validate_options(options)
110
+ raise ArgumentError, 'Type is required' unless options[:type]
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,319 @@
1
+ module OData4
2
+ # An OData4::Entity represents a single record returned by the service. All
3
+ # Entities have a type and belong to a specific namespace. They are written
4
+ # back to the service via the EntitySet they came from. OData4::Entity
5
+ # instances should not be instantiated directly; instead, they should either
6
+ # be read or instantiated from their respective OData4::EntitySet.
7
+ class Entity
8
+ # The Entity type name
9
+ attr_reader :type
10
+ # The OData4::Service's namespace
11
+ attr_reader :namespace
12
+ # The OData4::Service's identifying name
13
+ attr_reader :service_name
14
+ # The entity set this entity belongs to
15
+ attr_reader :entity_set
16
+ # List of errors on entity
17
+ attr_reader :errors
18
+
19
+ PROPERTY_NOT_LOADED = :not_loaded
20
+
21
+ XML_NAMESPACES = {
22
+ 'xmlns' => 'http://www.w3.org/2005/Atom',
23
+ 'xmlns:data' => 'http://docs.oasis-open.org/odata/ns/data',
24
+ 'xmlns:metadata' => 'http://docs.oasis-open.org/odata/ns/metadata',
25
+ 'xmlns:georss' => 'http://www.georss.org/georss',
26
+ 'xmlns:gml' => 'http://www.opengis.net/gml',
27
+ }.freeze
28
+
29
+ # Initializes a bare Entity
30
+ # @param options [Hash]
31
+ def initialize(options = {})
32
+ @id = options[:id]
33
+ @type = options[:type]
34
+ @namespace = options[:namespace]
35
+ @service_name = options[:service_name]
36
+ @entity_set = options[:entity_set]
37
+ @context = options[:context]
38
+ @links = options[:links]
39
+ @errors = []
40
+ end
41
+
42
+ # Returns name of Entity from Service specified type.
43
+ # @return [String]
44
+ def name
45
+ @name ||= type.gsub(/#{namespace}\./, '')
46
+ end
47
+
48
+ # Returns context URL for this entity
49
+ # @return [String]
50
+ def context
51
+ @context ||= context_url
52
+ end
53
+
54
+ # Get property value
55
+ # @param property_name [to_s]
56
+ # @return [*]
57
+ def [](property_name)
58
+ if get_property(property_name).is_a?(::OData4::ComplexType::Property)
59
+ get_property(property_name)
60
+ else
61
+ get_property(property_name).value
62
+ end
63
+ end
64
+
65
+ # Set property value
66
+ # @param property_name [to_s]
67
+ # @param value [*]
68
+ def []=(property_name, value)
69
+ get_property(property_name).value = value
70
+ end
71
+
72
+ def get_property(property_name)
73
+ prop_name = property_name.to_s
74
+ # Property is lazy loaded
75
+ if properties_xml_value.has_key?(prop_name)
76
+ property = instantiate_property(prop_name, properties_xml_value[prop_name])
77
+ set_property(prop_name, property.dup)
78
+ properties_xml_value.delete(prop_name)
79
+ end
80
+
81
+ if properties.has_key? prop_name
82
+ properties[prop_name]
83
+ elsif navigation_properties.has_key? prop_name
84
+ navigation_properties[prop_name]
85
+ else
86
+ raise ArgumentError, "Unknown property: #{property_name}"
87
+ end
88
+ end
89
+
90
+ def property_names
91
+ [
92
+ @properties_xml_value.andand.keys,
93
+ @properties.andand.keys
94
+ ].compact.flatten
95
+ end
96
+
97
+ def navigation_property_names
98
+ navigation_properties.keys
99
+ end
100
+
101
+ def navigation_properties
102
+ @navigation_properties ||= links.keys.map do |nav_name|
103
+ [
104
+ nav_name,
105
+ OData4::NavigationProperty::Proxy.new(self, nav_name)
106
+ ]
107
+ end.to_h
108
+ end
109
+
110
+ # Links to other OData4 entitites
111
+ # @return [Hash]
112
+ def links
113
+ @links ||= service.navigation_properties[name].map do |nav_name, details|
114
+ [
115
+ nav_name,
116
+ { type: details.nav_type, href: "#{id}/#{nav_name}" }
117
+ ]
118
+ end.to_h
119
+ end
120
+
121
+ # Create Entity with provided properties and options.
122
+ # @param new_properties [Hash]
123
+ # @param options [Hash]
124
+ # @param [OData4::Entity]
125
+ def self.with_properties(new_properties = {}, options = {})
126
+ entity = OData4::Entity.new(options)
127
+ entity.instance_eval do
128
+ service.properties_for_entity(name).each do |property_name, instance|
129
+ set_property(property_name, instance)
130
+ end
131
+
132
+ new_properties.each do |property_name, property_value|
133
+ self[property_name] = property_value
134
+ end
135
+ end
136
+ entity
137
+ end
138
+
139
+ # Create Entity from JSON document with provided options.
140
+ # @param json [Hash|to_s]
141
+ # @param options [Hash]
142
+ # @return [OData4::Entity]
143
+ def self.from_json(json, options = {})
144
+ return nil if json.nil?
145
+ json = JSON.parse(json.to_s) unless json.is_a?(Hash)
146
+ metadata = extract_metadata(json)
147
+ options.merge!(context: metadata['@odata.context'])
148
+ entity = with_properties(json, options)
149
+ process_metadata(entity, metadata)
150
+ entity
151
+ end
152
+
153
+ # Create Entity from XML document with provided options.
154
+ # @param xml_doc [Nokogiri::XML]
155
+ # @param options [Hash]
156
+ # @return [OData4::Entity]
157
+ def self.from_xml(xml_doc, options = {})
158
+ return nil if xml_doc.nil?
159
+ entity = OData4::Entity.new(options)
160
+ process_properties(entity, xml_doc)
161
+ process_links(entity, xml_doc)
162
+ entity
163
+ end
164
+
165
+ # Converts Entity to its XML representation.
166
+ # @return [String]
167
+ def to_xml
168
+ namespaces = XML_NAMESPACES.merge('xml:base' => service.service_url)
169
+ builder = Nokogiri::XML::Builder.new do |xml|
170
+ xml.entry(namespaces) do
171
+ xml.category(term: "#{namespace}.#{type}",
172
+ scheme: 'http://docs.oasis-open.org/odata/ns/scheme')
173
+ xml.author { xml.name }
174
+
175
+ xml.content(type: 'application/xml') do
176
+ xml['metadata'].properties do
177
+ property_names.each do |name|
178
+ next if name == primary_key
179
+ get_property(name).to_xml(xml)
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
185
+ builder.to_xml
186
+ end
187
+
188
+ # Converts Entity to its JSON representation.
189
+ # @return [String]
190
+ def to_json
191
+ # TODO: add @odata.context
192
+ to_hash.to_json
193
+ end
194
+
195
+ # Converts Entity to a hash.
196
+ # @return [Hash]
197
+ def to_hash
198
+ property_names.map do |name|
199
+ [name, get_property(name).json_value]
200
+ end.to_h
201
+ end
202
+
203
+ # Returns the canonical URL for this entity
204
+ # @return [String]
205
+ def id
206
+ @id ||= lambda {
207
+ entity_set = self.entity_set.andand.name
208
+ entity_set ||= context.split('#').last.split('/').first
209
+ "#{entity_set}(#{self[primary_key]})"
210
+ }.call
211
+ end
212
+
213
+ # Returns the primary key for the Entity.
214
+ # @return [String]
215
+ def primary_key
216
+ service.primary_key_for(name)
217
+ end
218
+
219
+ def is_new?
220
+ self[primary_key].nil?
221
+ end
222
+
223
+ def any_errors?
224
+ !errors.empty?
225
+ end
226
+
227
+ def service
228
+ @service ||= OData4::ServiceRegistry[service_name]
229
+ end
230
+
231
+ private
232
+
233
+ def instantiate_property(property_name, value_xml)
234
+ value_type = service.get_property_type(name, property_name)
235
+ klass = ::OData4::PropertyRegistry[value_type]
236
+
237
+ if klass.nil?
238
+ raise RuntimeError, "Unknown property type: #{value_type}"
239
+ else
240
+ klass.from_xml(value_xml)
241
+ end
242
+ end
243
+
244
+ def properties
245
+ @properties ||= {}
246
+ end
247
+
248
+ def properties_xml_value
249
+ @properties_xml_value ||= {}
250
+ end
251
+
252
+ # Computes the entity's canonical context URL
253
+ def context_url
254
+ "#{service.service_url}/$metadata##{entity_set.name}/$entity"
255
+ end
256
+
257
+ def set_property(name, property)
258
+ properties[name.to_s] = property
259
+ end
260
+
261
+ # Instantiating properties takes time, so we can lazy load properties by passing xml_value and lookup when needed
262
+ def set_property_lazy_load(name, xml_value )
263
+ properties_xml_value[name.to_s] = xml_value
264
+ end
265
+
266
+ def self.process_properties(entity, xml_doc)
267
+ entity.instance_eval do
268
+ unless instance_variable_get(:@context)
269
+ context = xml_doc.xpath('/entry').first.andand['context']
270
+ instance_variable_set(:@context, context)
271
+ end
272
+
273
+ xml_doc.xpath('./content/properties/*').each do |property_xml|
274
+ # Doing lazy loading here because instantiating each object takes a long time
275
+ set_property_lazy_load(property_xml.name, property_xml)
276
+ end
277
+ end
278
+ end
279
+
280
+ def self.process_links(entity, xml_doc)
281
+ entity.instance_eval do
282
+ new_links = instance_variable_get(:@links) || {}
283
+ service.navigation_properties[name].each do |nav_name, details|
284
+ xml_doc.xpath("./link[@title='#{nav_name}']").each do |node|
285
+ next if node.attributes['type'].nil?
286
+ next unless node.attributes['type'].value =~ /^application\/atom\+xml;type=(feed|entry)$/i
287
+ link_type = node.attributes['type'].value =~ /type=entry$/i ? :entity : :collection
288
+ new_links[nav_name] = {
289
+ type: link_type,
290
+ href: node.attributes['href'].value
291
+ }
292
+ end
293
+ end
294
+ instance_variable_set(:@links, new_links)
295
+ end
296
+ end
297
+
298
+ def self.extract_metadata(json)
299
+ metadata = json.select { |key, val| key =~ /@odata/ }
300
+ json.delete_if { |key, val| key =~ /@odata/ }
301
+ metadata
302
+ end
303
+
304
+ def self.process_metadata(entity, metadata)
305
+ entity.instance_eval do
306
+ new_links = instance_variable_get(:@links) || {}
307
+ service.navigation_properties[name].each do |nav_name, details|
308
+ href = metadata["#{nav_name}@odata.navigationLink"]
309
+ next if href.nil?
310
+ new_links[nav_name] = {
311
+ type: details.nav_type,
312
+ href: href
313
+ }
314
+ end
315
+ instance_variable_set(:@links, new_links) unless new_links.empty?
316
+ end
317
+ end
318
+ end
319
+ end