frodata 0.9.1
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 +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
|