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.
- checksums.yaml +7 -0
- data/.autotest +2 -0
- data/.gitignore +24 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +75 -0
- data/CHANGELOG.md +120 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +23 -0
- data/README.md +287 -0
- data/Rakefile +7 -0
- data/TODO.md +55 -0
- data/lib/odata4.rb +37 -0
- data/lib/odata4/complex_type.rb +76 -0
- data/lib/odata4/complex_type/property.rb +114 -0
- data/lib/odata4/entity.rb +319 -0
- data/lib/odata4/entity_set.rb +162 -0
- data/lib/odata4/enum_type.rb +95 -0
- data/lib/odata4/enum_type/property.rb +62 -0
- data/lib/odata4/navigation_property.rb +29 -0
- data/lib/odata4/navigation_property/proxy.rb +76 -0
- data/lib/odata4/properties.rb +25 -0
- data/lib/odata4/properties/binary.rb +50 -0
- data/lib/odata4/properties/boolean.rb +37 -0
- data/lib/odata4/properties/date.rb +27 -0
- data/lib/odata4/properties/date_time.rb +83 -0
- data/lib/odata4/properties/date_time_offset.rb +17 -0
- data/lib/odata4/properties/decimal.rb +50 -0
- data/lib/odata4/properties/float.rb +67 -0
- data/lib/odata4/properties/geography.rb +13 -0
- data/lib/odata4/properties/geography/base.rb +162 -0
- data/lib/odata4/properties/geography/line_string.rb +33 -0
- data/lib/odata4/properties/geography/point.rb +31 -0
- data/lib/odata4/properties/geography/polygon.rb +38 -0
- data/lib/odata4/properties/guid.rb +17 -0
- data/lib/odata4/properties/integer.rb +107 -0
- data/lib/odata4/properties/number.rb +14 -0
- data/lib/odata4/properties/string.rb +72 -0
- data/lib/odata4/properties/time.rb +40 -0
- data/lib/odata4/properties/time_of_day.rb +27 -0
- data/lib/odata4/property.rb +118 -0
- data/lib/odata4/property_registry.rb +41 -0
- data/lib/odata4/query.rb +231 -0
- data/lib/odata4/query/criteria.rb +92 -0
- data/lib/odata4/query/criteria/comparison_operators.rb +49 -0
- data/lib/odata4/query/criteria/date_functions.rb +61 -0
- data/lib/odata4/query/criteria/geography_functions.rb +21 -0
- data/lib/odata4/query/criteria/lambda_operators.rb +27 -0
- data/lib/odata4/query/criteria/string_functions.rb +40 -0
- data/lib/odata4/query/in_batches.rb +58 -0
- data/lib/odata4/query/result.rb +84 -0
- data/lib/odata4/query/result/atom.rb +41 -0
- data/lib/odata4/query/result/json.rb +42 -0
- data/lib/odata4/railtie.rb +19 -0
- data/lib/odata4/service.rb +344 -0
- data/lib/odata4/service_registry.rb +52 -0
- data/lib/odata4/version.rb +3 -0
- data/odata4.gemspec +34 -0
- data/spec/fixtures/files/entity_to_xml.xml +17 -0
- data/spec/fixtures/files/metadata.xml +150 -0
- data/spec/fixtures/files/product_0.json +10 -0
- data/spec/fixtures/files/product_0.xml +28 -0
- data/spec/fixtures/files/products.json +106 -0
- data/spec/fixtures/files/products.xml +308 -0
- data/spec/fixtures/files/supplier_0.json +26 -0
- data/spec/fixtures/files/supplier_0.xml +32 -0
- data/spec/fixtures/vcr_cassettes/complex_type_specs.yml +127 -0
- data/spec/fixtures/vcr_cassettes/entity_set_specs.yml +1348 -0
- data/spec/fixtures/vcr_cassettes/entity_set_specs/bad_entry.yml +183 -0
- data/spec/fixtures/vcr_cassettes/entity_set_specs/existing_entry.yml +256 -0
- data/spec/fixtures/vcr_cassettes/entity_set_specs/new_entry.yml +185 -0
- data/spec/fixtures/vcr_cassettes/entity_specs.yml +285 -0
- data/spec/fixtures/vcr_cassettes/navigation_property_proxy_specs.yml +346 -0
- data/spec/fixtures/vcr_cassettes/query/result_specs.yml +189 -0
- data/spec/fixtures/vcr_cassettes/query_specs.yml +663 -0
- data/spec/fixtures/vcr_cassettes/service_registry_specs.yml +129 -0
- data/spec/fixtures/vcr_cassettes/service_specs.yml +127 -0
- data/spec/fixtures/vcr_cassettes/usage_example_specs.yml +749 -0
- data/spec/odata4/complex_type_spec.rb +116 -0
- data/spec/odata4/entity/shared_examples.rb +82 -0
- data/spec/odata4/entity_set_spec.rb +168 -0
- data/spec/odata4/entity_spec.rb +151 -0
- data/spec/odata4/enum_type_spec.rb +134 -0
- data/spec/odata4/navigation_property/proxy_spec.rb +44 -0
- data/spec/odata4/navigation_property_spec.rb +55 -0
- data/spec/odata4/properties/binary_spec.rb +50 -0
- data/spec/odata4/properties/boolean_spec.rb +72 -0
- data/spec/odata4/properties/date_spec.rb +23 -0
- data/spec/odata4/properties/date_time_offset_spec.rb +30 -0
- data/spec/odata4/properties/date_time_spec.rb +23 -0
- data/spec/odata4/properties/decimal_spec.rb +24 -0
- data/spec/odata4/properties/float_spec.rb +45 -0
- data/spec/odata4/properties/geography/line_string_spec.rb +33 -0
- data/spec/odata4/properties/geography/point_spec.rb +29 -0
- data/spec/odata4/properties/geography/polygon_spec.rb +55 -0
- data/spec/odata4/properties/geography/shared_examples.rb +72 -0
- data/spec/odata4/properties/guid_spec.rb +17 -0
- data/spec/odata4/properties/integer_spec.rb +58 -0
- data/spec/odata4/properties/string_spec.rb +46 -0
- data/spec/odata4/properties/time_of_day_spec.rb +23 -0
- data/spec/odata4/properties/time_spec.rb +15 -0
- data/spec/odata4/property_registry_spec.rb +16 -0
- data/spec/odata4/property_spec.rb +32 -0
- data/spec/odata4/query/criteria_spec.rb +229 -0
- data/spec/odata4/query/result_spec.rb +53 -0
- data/spec/odata4/query_spec.rb +196 -0
- data/spec/odata4/service_registry_spec.rb +18 -0
- data/spec/odata4/service_spec.rb +80 -0
- data/spec/odata4/usage_example_spec.rb +176 -0
- data/spec/spec_helper.rb +32 -0
- data/spec/support/coverage.rb +2 -0
- data/spec/support/vcr.rb +9 -0
- metadata +380 -0
data/Rakefile
ADDED
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.
|
data/lib/odata4.rb
ADDED
|
@@ -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
|