odata4 0.7.0

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 (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