frodata 0.9.1
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 +150 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +23 -0
- data/README.md +427 -0
- data/Rakefile +7 -0
- data/TODO.md +55 -0
- data/frodata.gemspec +34 -0
- data/lib/frodata.rb +36 -0
- data/lib/frodata/entity.rb +332 -0
- data/lib/frodata/entity_container.rb +75 -0
- data/lib/frodata/entity_set.rb +161 -0
- data/lib/frodata/errors.rb +68 -0
- data/lib/frodata/navigation_property.rb +29 -0
- data/lib/frodata/navigation_property/proxy.rb +80 -0
- data/lib/frodata/properties.rb +32 -0
- data/lib/frodata/properties/binary.rb +50 -0
- data/lib/frodata/properties/boolean.rb +37 -0
- data/lib/frodata/properties/collection.rb +50 -0
- data/lib/frodata/properties/complex.rb +114 -0
- data/lib/frodata/properties/date.rb +27 -0
- data/lib/frodata/properties/date_time.rb +83 -0
- data/lib/frodata/properties/date_time_offset.rb +17 -0
- data/lib/frodata/properties/decimal.rb +50 -0
- data/lib/frodata/properties/enum.rb +62 -0
- data/lib/frodata/properties/float.rb +67 -0
- data/lib/frodata/properties/geography.rb +13 -0
- data/lib/frodata/properties/geography/base.rb +162 -0
- data/lib/frodata/properties/geography/line_string.rb +33 -0
- data/lib/frodata/properties/geography/point.rb +31 -0
- data/lib/frodata/properties/geography/polygon.rb +38 -0
- data/lib/frodata/properties/guid.rb +17 -0
- data/lib/frodata/properties/integer.rb +107 -0
- data/lib/frodata/properties/number.rb +14 -0
- data/lib/frodata/properties/string.rb +72 -0
- data/lib/frodata/properties/time.rb +40 -0
- data/lib/frodata/properties/time_of_day.rb +27 -0
- data/lib/frodata/property.rb +139 -0
- data/lib/frodata/property_registry.rb +41 -0
- data/lib/frodata/query.rb +233 -0
- data/lib/frodata/query/criteria.rb +92 -0
- data/lib/frodata/query/criteria/comparison_operators.rb +49 -0
- data/lib/frodata/query/criteria/date_functions.rb +61 -0
- data/lib/frodata/query/criteria/geography_functions.rb +21 -0
- data/lib/frodata/query/criteria/lambda_operators.rb +27 -0
- data/lib/frodata/query/criteria/string_functions.rb +40 -0
- data/lib/frodata/query/in_batches.rb +58 -0
- data/lib/frodata/railtie.rb +19 -0
- data/lib/frodata/schema.rb +155 -0
- data/lib/frodata/schema/complex_type.rb +79 -0
- data/lib/frodata/schema/enum_type.rb +95 -0
- data/lib/frodata/service.rb +254 -0
- data/lib/frodata/service/request.rb +85 -0
- data/lib/frodata/service/response.rb +162 -0
- data/lib/frodata/service/response/atom.rb +40 -0
- data/lib/frodata/service/response/json.rb +41 -0
- data/lib/frodata/service/response/plain.rb +36 -0
- data/lib/frodata/service/response/xml.rb +40 -0
- data/lib/frodata/service_registry.rb +52 -0
- data/lib/frodata/version.rb +3 -0
- data/spec/fixtures/files/entity_to_xml.xml +17 -0
- data/spec/fixtures/files/error.xml +5 -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/entity_set_specs.yml +1635 -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 +1060 -0
- data/spec/fixtures/vcr_cassettes/schema/complex_type_specs.yml +127 -0
- data/spec/fixtures/vcr_cassettes/service/request_specs.yml +193 -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 +1330 -0
- data/spec/frodata/entity/shared_examples.rb +82 -0
- data/spec/frodata/entity_container_spec.rb +38 -0
- data/spec/frodata/entity_set_spec.rb +168 -0
- data/spec/frodata/entity_spec.rb +151 -0
- data/spec/frodata/errors_spec.rb +48 -0
- data/spec/frodata/navigation_property/proxy_spec.rb +44 -0
- data/spec/frodata/navigation_property_spec.rb +55 -0
- data/spec/frodata/properties/binary_spec.rb +50 -0
- data/spec/frodata/properties/boolean_spec.rb +72 -0
- data/spec/frodata/properties/collection_spec.rb +44 -0
- data/spec/frodata/properties/date_spec.rb +23 -0
- data/spec/frodata/properties/date_time_offset_spec.rb +30 -0
- data/spec/frodata/properties/date_time_spec.rb +23 -0
- data/spec/frodata/properties/decimal_spec.rb +51 -0
- data/spec/frodata/properties/float_spec.rb +45 -0
- data/spec/frodata/properties/geography/line_string_spec.rb +33 -0
- data/spec/frodata/properties/geography/point_spec.rb +29 -0
- data/spec/frodata/properties/geography/polygon_spec.rb +55 -0
- data/spec/frodata/properties/geography/shared_examples.rb +72 -0
- data/spec/frodata/properties/guid_spec.rb +17 -0
- data/spec/frodata/properties/integer_spec.rb +58 -0
- data/spec/frodata/properties/string_spec.rb +46 -0
- data/spec/frodata/properties/time_of_day_spec.rb +23 -0
- data/spec/frodata/properties/time_spec.rb +15 -0
- data/spec/frodata/property_registry_spec.rb +16 -0
- data/spec/frodata/property_spec.rb +71 -0
- data/spec/frodata/query/criteria_spec.rb +229 -0
- data/spec/frodata/query_spec.rb +199 -0
- data/spec/frodata/schema/complex_type_spec.rb +96 -0
- data/spec/frodata/schema/enum_type_spec.rb +112 -0
- data/spec/frodata/schema_spec.rb +97 -0
- data/spec/frodata/service/request_spec.rb +49 -0
- data/spec/frodata/service/response_spec.rb +85 -0
- data/spec/frodata/service_registry_spec.rb +18 -0
- data/spec/frodata/service_spec.rb +191 -0
- data/spec/frodata/usage_example_spec.rb +188 -0
- data/spec/spec_helper.rb +32 -0
- data/spec/support/coverage.rb +2 -0
- data/spec/support/vcr.rb +9 -0
- metadata +401 -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/frodata.gemspec
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'frodata/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'frodata'
|
8
|
+
spec.version = FrOData::VERSION
|
9
|
+
spec.authors = ['Christoph Wagner', 'James Thompson']
|
10
|
+
spec.email = %w{christoph@wrstudios.com james@plainprograms.com}
|
11
|
+
spec.summary = %q{Simple OData library}
|
12
|
+
spec.description = %q{Provides a simple interface for working with OData V4 APIs.}
|
13
|
+
spec.homepage = 'https://github.com/wrstudios/frodata'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = %w{lib}
|
20
|
+
|
21
|
+
spec.add_development_dependency 'bundler', '~> 1.6'
|
22
|
+
spec.add_development_dependency 'rake', '~> 0'
|
23
|
+
spec.add_development_dependency 'simplecov', '~> 0.15'
|
24
|
+
spec.add_development_dependency 'rspec', '~> 3.7'
|
25
|
+
spec.add_development_dependency 'rspec-autotest', '~> 1.0'
|
26
|
+
spec.add_development_dependency 'autotest', '~> 4.4'
|
27
|
+
spec.add_development_dependency 'vcr', '~> 4.0'
|
28
|
+
spec.add_development_dependency 'timecop', '~> 0.9'
|
29
|
+
spec.add_development_dependency 'equivalent-xml', '~> 0.6'
|
30
|
+
|
31
|
+
spec.add_dependency 'nokogiri', '~> 1.8'
|
32
|
+
spec.add_dependency 'faraday', '~> 0.15'
|
33
|
+
spec.add_dependency 'andand', '~> 1.3'
|
34
|
+
end
|
data/lib/frodata.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'date'
|
3
|
+
require 'time'
|
4
|
+
require 'bigdecimal'
|
5
|
+
require 'nokogiri'
|
6
|
+
require 'faraday'
|
7
|
+
require 'logger'
|
8
|
+
require 'andand'
|
9
|
+
require 'json'
|
10
|
+
|
11
|
+
# require 'active_support'
|
12
|
+
# require 'active_support/core_ext'
|
13
|
+
# require 'active_support/concern'
|
14
|
+
|
15
|
+
require 'frodata/version'
|
16
|
+
require 'frodata/errors'
|
17
|
+
require 'frodata/property_registry'
|
18
|
+
require 'frodata/property'
|
19
|
+
require 'frodata/properties'
|
20
|
+
require 'frodata/navigation_property'
|
21
|
+
require 'frodata/entity'
|
22
|
+
require 'frodata/entity_container'
|
23
|
+
require 'frodata/entity_set'
|
24
|
+
require 'frodata/query'
|
25
|
+
require 'frodata/schema'
|
26
|
+
require 'frodata/service'
|
27
|
+
require 'frodata/service_registry'
|
28
|
+
|
29
|
+
require 'frodata/railtie' if defined?(::Rails)
|
30
|
+
|
31
|
+
# The FrOData gem provides a convenient way to interact with OData V4 services from
|
32
|
+
# Ruby. Please look to the {file:README.md README} for how to get started using
|
33
|
+
# the FrOData gem.
|
34
|
+
module FrOData
|
35
|
+
# Your code goes here...
|
36
|
+
end
|
@@ -0,0 +1,332 @@
|
|
1
|
+
module FrOData
|
2
|
+
# An FrOData::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. FrOData::Entity
|
5
|
+
# instances should not be instantiated directly; instead, they should either
|
6
|
+
# be read or instantiated from their respective FrOData::EntitySet.
|
7
|
+
class Entity
|
8
|
+
# The Entity type name
|
9
|
+
attr_reader :type
|
10
|
+
# The FrOData::Service's identifying name
|
11
|
+
attr_reader :service_name
|
12
|
+
# The entity set this entity belongs to
|
13
|
+
attr_reader :entity_set
|
14
|
+
# List of errors on entity
|
15
|
+
attr_reader :errors
|
16
|
+
|
17
|
+
PROPERTY_NOT_LOADED = :not_loaded
|
18
|
+
|
19
|
+
XML_NAMESPACES = {
|
20
|
+
'xmlns' => 'http://www.w3.org/2005/Atom',
|
21
|
+
'xmlns:data' => 'http://docs.oasis-open.org/odata/ns/data',
|
22
|
+
'xmlns:metadata' => 'http://docs.oasis-open.org/odata/ns/metadata',
|
23
|
+
'xmlns:georss' => 'http://www.georss.org/georss',
|
24
|
+
'xmlns:gml' => 'http://www.opengis.net/gml',
|
25
|
+
}.freeze
|
26
|
+
|
27
|
+
# Initializes a bare Entity
|
28
|
+
# @param options [Hash]
|
29
|
+
def initialize(options = {})
|
30
|
+
@id = options[:id]
|
31
|
+
@type = options[:type]
|
32
|
+
@service_name = options[:service_name]
|
33
|
+
@entity_set = options[:entity_set]
|
34
|
+
@context = options[:context]
|
35
|
+
@links = options[:links]
|
36
|
+
@errors = []
|
37
|
+
end
|
38
|
+
|
39
|
+
def namespace
|
40
|
+
@namespace ||= type.rpartition('.').first
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns name of Entity from Service specified type.
|
44
|
+
# @return [String]
|
45
|
+
def name
|
46
|
+
@name ||= type.split('.').last
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns context URL for this entity
|
50
|
+
# @return [String]
|
51
|
+
def context
|
52
|
+
@context ||= context_url
|
53
|
+
end
|
54
|
+
|
55
|
+
# Get property value
|
56
|
+
# @param property_name [to_s]
|
57
|
+
# @return [*]
|
58
|
+
def [](property_name)
|
59
|
+
if get_property(property_name).is_a?(::FrOData::Properties::Complex)
|
60
|
+
get_property(property_name)
|
61
|
+
else
|
62
|
+
get_property(property_name).value
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Set property value
|
67
|
+
# @param property_name [to_s]
|
68
|
+
# @param value [*]
|
69
|
+
def []=(property_name, value)
|
70
|
+
get_property(property_name).value = value
|
71
|
+
end
|
72
|
+
|
73
|
+
def get_property(property_name)
|
74
|
+
prop_name = property_name.to_s
|
75
|
+
# Property is lazy loaded
|
76
|
+
if properties_xml_value.has_key?(prop_name)
|
77
|
+
property = instantiate_property(prop_name, properties_xml_value[prop_name])
|
78
|
+
set_property(prop_name, property.dup)
|
79
|
+
properties_xml_value.delete(prop_name)
|
80
|
+
end
|
81
|
+
|
82
|
+
if properties.has_key? prop_name
|
83
|
+
properties[prop_name]
|
84
|
+
elsif navigation_properties.has_key? prop_name
|
85
|
+
navigation_properties[prop_name]
|
86
|
+
else
|
87
|
+
raise ArgumentError, "Unknown property: #{property_name}"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def property_names
|
92
|
+
[
|
93
|
+
@properties_xml_value.andand.keys,
|
94
|
+
@properties.andand.keys
|
95
|
+
].compact.flatten
|
96
|
+
end
|
97
|
+
|
98
|
+
def navigation_property_names
|
99
|
+
navigation_properties.keys
|
100
|
+
end
|
101
|
+
|
102
|
+
def navigation_properties
|
103
|
+
@navigation_properties ||= links.keys.map do |nav_name|
|
104
|
+
[
|
105
|
+
nav_name,
|
106
|
+
FrOData::NavigationProperty::Proxy.new(self, nav_name)
|
107
|
+
]
|
108
|
+
end.to_h
|
109
|
+
end
|
110
|
+
|
111
|
+
# Links to other FrOData entitites
|
112
|
+
# @return [Hash]
|
113
|
+
def links
|
114
|
+
@links ||= schema.navigation_properties[name].map do |nav_name, details|
|
115
|
+
[
|
116
|
+
nav_name,
|
117
|
+
{ type: details.nav_type, href: "#{id}/#{nav_name}" }
|
118
|
+
]
|
119
|
+
end.to_h
|
120
|
+
end
|
121
|
+
|
122
|
+
# Create Entity with provided properties and options.
|
123
|
+
# @param new_properties [Hash]
|
124
|
+
# @param options [Hash]
|
125
|
+
# @param [FrOData::Entity]
|
126
|
+
def self.with_properties(new_properties = {}, options = {})
|
127
|
+
entity = FrOData::Entity.new(options)
|
128
|
+
entity.instance_eval do
|
129
|
+
service.properties_for_entity(type).each do |property_name, instance|
|
130
|
+
set_property(property_name, instance)
|
131
|
+
end
|
132
|
+
|
133
|
+
new_properties.each do |property_name, property_value|
|
134
|
+
self[property_name] = property_value
|
135
|
+
end
|
136
|
+
end
|
137
|
+
entity
|
138
|
+
end
|
139
|
+
|
140
|
+
# Create Entity from JSON document with provided options.
|
141
|
+
# @param json [Hash|to_s]
|
142
|
+
# @param options [Hash]
|
143
|
+
# @return [FrOData::Entity]
|
144
|
+
def self.from_json(json, options = {})
|
145
|
+
return nil if json.nil?
|
146
|
+
json = JSON.parse(json.to_s) unless json.is_a?(Hash)
|
147
|
+
metadata = extract_metadata(json)
|
148
|
+
options.merge!(context: metadata['@odata.context'])
|
149
|
+
entity = with_properties(json, options)
|
150
|
+
process_metadata(entity, metadata)
|
151
|
+
entity
|
152
|
+
end
|
153
|
+
|
154
|
+
# Create Entity from XML document with provided options.
|
155
|
+
# @param xml_doc [Nokogiri::XML]
|
156
|
+
# @param options [Hash]
|
157
|
+
# @return [FrOData::Entity]
|
158
|
+
def self.from_xml(xml_doc, options = {})
|
159
|
+
return nil if xml_doc.nil?
|
160
|
+
entity = FrOData::Entity.new(options)
|
161
|
+
process_properties(entity, xml_doc)
|
162
|
+
process_links(entity, xml_doc)
|
163
|
+
entity
|
164
|
+
end
|
165
|
+
|
166
|
+
# Converts Entity to its XML representation.
|
167
|
+
# @return [String]
|
168
|
+
def to_xml
|
169
|
+
namespaces = XML_NAMESPACES.merge('xml:base' => service.service_url)
|
170
|
+
builder = Nokogiri::XML::Builder.new do |xml|
|
171
|
+
xml.entry(namespaces) do
|
172
|
+
xml.category(term: type,
|
173
|
+
scheme: 'http://docs.oasis-open.org/odata/ns/scheme')
|
174
|
+
xml.author { xml.name }
|
175
|
+
|
176
|
+
xml.content(type: 'application/xml') do
|
177
|
+
xml['metadata'].properties do
|
178
|
+
property_names.each do |name|
|
179
|
+
next if name == primary_key
|
180
|
+
get_property(name).to_xml(xml)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
builder.to_xml
|
187
|
+
end
|
188
|
+
|
189
|
+
# Converts Entity to its JSON representation.
|
190
|
+
# @return [String]
|
191
|
+
def to_json
|
192
|
+
# TODO: add @odata.context
|
193
|
+
to_hash.to_json
|
194
|
+
end
|
195
|
+
|
196
|
+
# Converts Entity to a hash.
|
197
|
+
# @return [Hash]
|
198
|
+
def to_hash
|
199
|
+
property_names.map do |name|
|
200
|
+
[name, get_property(name).json_value]
|
201
|
+
end.to_h
|
202
|
+
end
|
203
|
+
|
204
|
+
# Returns the canonical URL for this entity
|
205
|
+
# @return [String]
|
206
|
+
def id
|
207
|
+
@id ||= lambda {
|
208
|
+
entity_set = self.entity_set.andand.name
|
209
|
+
entity_set ||= context.split('#').last.split('/').first
|
210
|
+
"#{entity_set}(#{self[primary_key]})"
|
211
|
+
}.call
|
212
|
+
end
|
213
|
+
|
214
|
+
# Returns the primary key for the Entity.
|
215
|
+
# @return [String]
|
216
|
+
def primary_key
|
217
|
+
schema.primary_key_for(name)
|
218
|
+
end
|
219
|
+
|
220
|
+
def is_new?
|
221
|
+
self[primary_key].nil?
|
222
|
+
end
|
223
|
+
|
224
|
+
def any_errors?
|
225
|
+
!errors.empty?
|
226
|
+
end
|
227
|
+
|
228
|
+
def service
|
229
|
+
@service ||= FrOData::ServiceRegistry[service_name]
|
230
|
+
end
|
231
|
+
|
232
|
+
def schema
|
233
|
+
@schema ||= service.schemas[namespace]
|
234
|
+
end
|
235
|
+
|
236
|
+
private
|
237
|
+
|
238
|
+
def instantiate_property(property_name, value_xml)
|
239
|
+
prop_type = schema.get_property_type(name, property_name)
|
240
|
+
prop_type, value_type = prop_type.split(/\(|\)/)
|
241
|
+
|
242
|
+
if prop_type == 'Collection'
|
243
|
+
klass = ::FrOData::Properties::Collection
|
244
|
+
options = { value_type: value_type }
|
245
|
+
else
|
246
|
+
klass = ::FrOData::PropertyRegistry[prop_type]
|
247
|
+
options = {}
|
248
|
+
end
|
249
|
+
|
250
|
+
if klass.nil?
|
251
|
+
raise RuntimeError, "Unknown property type: #{prop_type}"
|
252
|
+
else
|
253
|
+
klass.from_xml(value_xml, options.merge(service: service))
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
def properties
|
258
|
+
@properties ||= {}
|
259
|
+
end
|
260
|
+
|
261
|
+
def properties_xml_value
|
262
|
+
@properties_xml_value ||= {}
|
263
|
+
end
|
264
|
+
|
265
|
+
# Computes the entity's canonical context URL
|
266
|
+
def context_url
|
267
|
+
"#{service.service_url}/$metadata##{entity_set.name}/$entity"
|
268
|
+
end
|
269
|
+
|
270
|
+
def set_property(name, property)
|
271
|
+
properties[name.to_s] = property
|
272
|
+
end
|
273
|
+
|
274
|
+
# Instantiating properties takes time, so we can lazy load properties by passing xml_value and lookup when needed
|
275
|
+
def set_property_lazy_load(name, xml_value )
|
276
|
+
properties_xml_value[name.to_s] = xml_value
|
277
|
+
end
|
278
|
+
|
279
|
+
def self.process_properties(entity, xml_doc)
|
280
|
+
entity.instance_eval do
|
281
|
+
unless instance_variable_get(:@context)
|
282
|
+
context = xml_doc.xpath('/entry').first.andand['context']
|
283
|
+
instance_variable_set(:@context, context)
|
284
|
+
end
|
285
|
+
|
286
|
+
xml_doc.xpath('./content/properties/*').each do |property_xml|
|
287
|
+
# Doing lazy loading here because instantiating each object takes a long time
|
288
|
+
set_property_lazy_load(property_xml.name, property_xml)
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
def self.process_links(entity, xml_doc)
|
294
|
+
entity.instance_eval do
|
295
|
+
new_links = instance_variable_get(:@links) || {}
|
296
|
+
schema.navigation_properties[name].each do |nav_name, details|
|
297
|
+
xml_doc.xpath("./link[@title='#{nav_name}']").each do |node|
|
298
|
+
next if node.attributes['type'].nil?
|
299
|
+
next unless node.attributes['type'].value =~ /^application\/atom\+xml;type=(feed|entry)$/i
|
300
|
+
link_type = node.attributes['type'].value =~ /type=entry$/i ? :entity : :collection
|
301
|
+
new_links[nav_name] = {
|
302
|
+
type: link_type,
|
303
|
+
href: node.attributes['href'].value
|
304
|
+
}
|
305
|
+
end
|
306
|
+
end
|
307
|
+
instance_variable_set(:@links, new_links)
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
def self.extract_metadata(json)
|
312
|
+
metadata = json.select { |key, val| key =~ /@odata/ }
|
313
|
+
json.delete_if { |key, val| key =~ /@odata/ }
|
314
|
+
metadata
|
315
|
+
end
|
316
|
+
|
317
|
+
def self.process_metadata(entity, metadata)
|
318
|
+
entity.instance_eval do
|
319
|
+
new_links = instance_variable_get(:@links) || {}
|
320
|
+
schema.navigation_properties[name].each do |nav_name, details|
|
321
|
+
href = metadata["#{nav_name}@odata.navigationLink"]
|
322
|
+
next if href.nil?
|
323
|
+
new_links[nav_name] = {
|
324
|
+
type: details.nav_type,
|
325
|
+
href: href
|
326
|
+
}
|
327
|
+
end
|
328
|
+
instance_variable_set(:@links, new_links) unless new_links.empty?
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|