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