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
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module OData4
|
|
2
|
+
class Query
|
|
3
|
+
class Criteria
|
|
4
|
+
module GeographyFunctions
|
|
5
|
+
# Applies the `geo.distance` function.
|
|
6
|
+
# @param to [to_s]
|
|
7
|
+
# @return [self]
|
|
8
|
+
def distance(to)
|
|
9
|
+
set_function_and_argument(:'geo.distance', to)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Applies the `geo.intersects` function.
|
|
13
|
+
# @param what [to_s]
|
|
14
|
+
# @return [self]
|
|
15
|
+
def intersects(what)
|
|
16
|
+
set_function_and_argument(:'geo.intersects', what)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module OData4
|
|
2
|
+
class Query
|
|
3
|
+
class Criteria
|
|
4
|
+
module LambdaOperators
|
|
5
|
+
# Applies the `any` lambda operator to the given property
|
|
6
|
+
# @param property [to_s]
|
|
7
|
+
# @return [self]
|
|
8
|
+
def any(property)
|
|
9
|
+
set_function_and_argument(:any, property)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Applies the `any` lambda operator to the given property
|
|
13
|
+
# @param property [to_s]
|
|
14
|
+
# @return [self]
|
|
15
|
+
def all(property)
|
|
16
|
+
set_function_and_argument(:all, property)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def lambda_operator?
|
|
22
|
+
[:any, :all].include?(function)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module OData4
|
|
2
|
+
class Query
|
|
3
|
+
class Criteria
|
|
4
|
+
module StringFunctions
|
|
5
|
+
# Sets up a `contains` function criterium.
|
|
6
|
+
# @param str [to_s]
|
|
7
|
+
# @return [self]
|
|
8
|
+
def contains(str)
|
|
9
|
+
set_function_and_argument(:contains, str)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Sets up a `startswith` function criterium.
|
|
13
|
+
# @param str [to_s]
|
|
14
|
+
# @return [self]
|
|
15
|
+
def startswith(str)
|
|
16
|
+
set_function_and_argument(:startswith, str)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Sets up a `endswith` function criterium.
|
|
20
|
+
# @param str [to_s]
|
|
21
|
+
# @return [self]
|
|
22
|
+
def endswith(str)
|
|
23
|
+
set_function_and_argument(:endswith, str)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Applies the `tolower` function to the property.
|
|
27
|
+
# @return [self]
|
|
28
|
+
def tolower
|
|
29
|
+
set_function_and_argument(:tolower, nil)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Applies the `toupper` function to the property.
|
|
33
|
+
# @return [self]
|
|
34
|
+
def toupper
|
|
35
|
+
set_function_and_argument(:toupper, nil)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module OData4
|
|
2
|
+
class Query
|
|
3
|
+
module InBatches
|
|
4
|
+
DEFAULT_BATCH_SIZE = 10
|
|
5
|
+
|
|
6
|
+
# Process results in batches.
|
|
7
|
+
#
|
|
8
|
+
# When a block is given, yields `OData4::Query::Result`
|
|
9
|
+
# objects of specified batch size to the block.
|
|
10
|
+
#
|
|
11
|
+
# service['Products'].query.in_batches(of: 10) do |batch|
|
|
12
|
+
# batch.count # batch size (10 except for last batch)
|
|
13
|
+
# batch.is_a? OData4::Query::Result # true
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# Returns an Enumerator to process results individually.
|
|
17
|
+
#
|
|
18
|
+
# service['Products'].query.in_batches.each do |entity|
|
|
19
|
+
# entity.is_a? OData4::Entity # true
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# @param of: [int] batch size
|
|
23
|
+
# @return [Enumerator]
|
|
24
|
+
def in_batches(of: DEFAULT_BATCH_SIZE, &block)
|
|
25
|
+
per_page = of
|
|
26
|
+
|
|
27
|
+
if block_given?
|
|
28
|
+
each_batch(of, &block)
|
|
29
|
+
else
|
|
30
|
+
Enumerator.new do |result|
|
|
31
|
+
each_batch(of) do |batch|
|
|
32
|
+
batch.each { |entity| result << entity }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def each_batch(per_page, &block)
|
|
41
|
+
page = 0
|
|
42
|
+
|
|
43
|
+
loop do
|
|
44
|
+
batch = get_paginated_entities(per_page, page)
|
|
45
|
+
break if batch.empty?
|
|
46
|
+
|
|
47
|
+
yield batch
|
|
48
|
+
|
|
49
|
+
page += 1
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def get_paginated_entities(per_page, page)
|
|
54
|
+
skip(per_page * page).limit(per_page).execute
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
module OData4
|
|
2
|
+
class Query
|
|
3
|
+
# Represents the results of executing a OData4::Query.
|
|
4
|
+
# @api private
|
|
5
|
+
class Result
|
|
6
|
+
include Enumerable
|
|
7
|
+
|
|
8
|
+
attr_reader :query
|
|
9
|
+
|
|
10
|
+
# Initialize a result with the query and the result.
|
|
11
|
+
# @param query [OData4::Query]
|
|
12
|
+
# @param result [Typhoeus::Result]
|
|
13
|
+
def initialize(query, result)
|
|
14
|
+
@query = query
|
|
15
|
+
@result = result
|
|
16
|
+
check_result_type
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Provided for Enumerable functionality
|
|
20
|
+
# @param block [block] a block to evaluate
|
|
21
|
+
# @return [OData4::Entity] each entity in turn for the query result
|
|
22
|
+
def each(&block)
|
|
23
|
+
unless empty?
|
|
24
|
+
process_results(&block)
|
|
25
|
+
until next_page.nil?
|
|
26
|
+
# ensure query gets executed with the same options
|
|
27
|
+
result = query.execute(next_page_url)
|
|
28
|
+
process_results(&block)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Checks whether the result set contains any results
|
|
34
|
+
# @return [Boolean]
|
|
35
|
+
def empty?
|
|
36
|
+
@empty_result ||= find_entities.empty?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
attr_accessor :result
|
|
42
|
+
|
|
43
|
+
def check_result_type
|
|
44
|
+
# Dynamically extend instance with methods for
|
|
45
|
+
# processing the current result type
|
|
46
|
+
if is_atom_result?
|
|
47
|
+
extend OData4::Query::Result::Atom
|
|
48
|
+
elsif is_json_result?
|
|
49
|
+
extend OData4::Query::Result::JSON
|
|
50
|
+
elsif result.body.empty?
|
|
51
|
+
# Some services (*cough* Microsoft *cough*) return
|
|
52
|
+
# an empty response with no `Content-Type` header set.
|
|
53
|
+
# We catch that here and bypass content type detection.
|
|
54
|
+
@empty_result = true
|
|
55
|
+
else
|
|
56
|
+
raise ArgumentError, "Invalid result type '#{content_type}'"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def is_atom_result?
|
|
61
|
+
content_type =~ /#{Regexp.escape OData4::Service::MIME_TYPES[:atom]}/
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def is_json_result?
|
|
65
|
+
content_type =~ /#{Regexp.escape OData4::Service::MIME_TYPES[:json]}/
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def content_type
|
|
69
|
+
result.headers['Content-Type'] || ''
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def service
|
|
73
|
+
query.entity_set.service
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def entity_options
|
|
77
|
+
query.entity_set.entity_options
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
require 'odata4/query/result/atom'
|
|
84
|
+
require 'odata4/query/result/json'
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module OData4
|
|
2
|
+
class Query
|
|
3
|
+
class Result
|
|
4
|
+
# Represents the results of executing a OData4::Query.
|
|
5
|
+
# @api private
|
|
6
|
+
module Atom
|
|
7
|
+
def process_results(&block)
|
|
8
|
+
find_entities.each do |entity_xml|
|
|
9
|
+
entity = OData4::Entity.from_xml(entity_xml, entity_options)
|
|
10
|
+
block_given? ? block.call(entity) : yield(entity)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def next_page
|
|
15
|
+
result_xml.xpath("/feed/link[@rel='next']").first
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def next_page_url
|
|
19
|
+
next_page.attributes['href'].value.gsub(service.service_url, '')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def error_message
|
|
23
|
+
result_xml.xpath('//error/message').first.andand.text
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def result_xml
|
|
29
|
+
@result_xml ||= ::Nokogiri::XML(result.body).remove_namespaces!
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Find entity entries in a result set
|
|
33
|
+
#
|
|
34
|
+
# @return [Nokogiri::XML::NodeSet]
|
|
35
|
+
def find_entities
|
|
36
|
+
result_xml.xpath('//entry')
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module OData4
|
|
2
|
+
class Query
|
|
3
|
+
class Result
|
|
4
|
+
# Represents the results of executing a OData4::Query.
|
|
5
|
+
# @api private
|
|
6
|
+
module JSON
|
|
7
|
+
def process_results(&block)
|
|
8
|
+
find_entities.each do |entity_json|
|
|
9
|
+
entity = OData4::Entity.from_json(entity_json, entity_options)
|
|
10
|
+
block_given? ? block.call(entity) : yield(entity)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def next_page
|
|
15
|
+
result_json['@odata.nextLink']
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def next_page_url
|
|
19
|
+
next_page.gsub(service.service_url, '')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def error_message
|
|
23
|
+
result_json['error'].andand['message']
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def result_json
|
|
29
|
+
@result_json ||= ::JSON.parse(result.body)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def single_entity?
|
|
33
|
+
result_json['@odata.context'] =~ /\$entity$/
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def find_entities
|
|
37
|
+
single_entity? ? [result_json] : result_json['value']
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module OData4
|
|
2
|
+
class Railtie < Rails::Railtie
|
|
3
|
+
config.before_initialize do
|
|
4
|
+
::OData4::Railtie.load_configuration!
|
|
5
|
+
::OData4::Railtie.setup_service_registry!
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
# Looks for config/odata.yml and loads the configuration.
|
|
9
|
+
def self.load_configuration!
|
|
10
|
+
# TODO Implement Rails configuration loading
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Examines the loaded configuration and populates the
|
|
14
|
+
# OData4::ServiceRegistry accordingly.
|
|
15
|
+
def self.setup_service_registry!
|
|
16
|
+
# TODO Populate OData4::ServiceRegistry based on configuration
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
module OData4
|
|
2
|
+
# Encapsulates the basic details and functionality needed to interact with an
|
|
3
|
+
# OData4 service.
|
|
4
|
+
class Service
|
|
5
|
+
# The OData4 Service's URL
|
|
6
|
+
attr_reader :service_url
|
|
7
|
+
# Options to pass around
|
|
8
|
+
attr_reader :options
|
|
9
|
+
|
|
10
|
+
HTTP_TIMEOUT = 20
|
|
11
|
+
|
|
12
|
+
METADATA_TIMEOUTS = [20, 60]
|
|
13
|
+
|
|
14
|
+
MIME_TYPES = {
|
|
15
|
+
atom: 'application/atom+xml',
|
|
16
|
+
json: 'application/json',
|
|
17
|
+
plain: 'text/plain'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
# Opens the service based on the requested URL and adds the service to
|
|
21
|
+
# {OData4::Registry}
|
|
22
|
+
#
|
|
23
|
+
# @param service_url [String] the URL to the desired OData4 service
|
|
24
|
+
# @param options [Hash] options to pass to the service
|
|
25
|
+
# @return [OData4::Service] an instance of the service
|
|
26
|
+
def initialize(service_url, options = {})
|
|
27
|
+
@service_url = service_url
|
|
28
|
+
@options = default_options.merge(options)
|
|
29
|
+
OData4::ServiceRegistry.add(self)
|
|
30
|
+
register_custom_types
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Opens the service based on the requested URL and adds the service to
|
|
34
|
+
# {OData4::Registry}
|
|
35
|
+
#
|
|
36
|
+
# @param service_url [String] the URL to the desired OData4 service
|
|
37
|
+
# @param options [Hash] options to pass to the service
|
|
38
|
+
# @return [OData4::Service] an instance of the service
|
|
39
|
+
def self.open(service_url, options = {})
|
|
40
|
+
Service.new(service_url, options)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Returns user supplied name for service, or its URL
|
|
44
|
+
# @return [String]
|
|
45
|
+
def name
|
|
46
|
+
@name ||= options[:name] || service_url
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Returns the service's metadata URL.
|
|
50
|
+
# @return [String]
|
|
51
|
+
def metadata_url
|
|
52
|
+
"#{service_url}/$metadata"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Returns a list of entities exposed by the service
|
|
56
|
+
def entity_types
|
|
57
|
+
@entity_types ||= metadata.xpath('//EntityType').collect {|entity| entity.attributes['Name'].value}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Returns a hash of EntitySet names keyed to their respective EntityType name
|
|
61
|
+
def entity_sets
|
|
62
|
+
@entity_sets ||= metadata.xpath('//EntityContainer/EntitySet').collect {|entity|
|
|
63
|
+
[
|
|
64
|
+
entity.attributes['EntityType'].value.gsub("#{namespace}.", ''),
|
|
65
|
+
entity.attributes['Name'].value
|
|
66
|
+
]
|
|
67
|
+
}.to_h
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Returns a list of ComplexTypes used by the service
|
|
71
|
+
# @return [Hash<String, OData4::ComplexType>]
|
|
72
|
+
def complex_types
|
|
73
|
+
@complex_types ||= metadata.xpath('//ComplexType').map do |entity|
|
|
74
|
+
[
|
|
75
|
+
entity.attributes['Name'].value,
|
|
76
|
+
::OData4::ComplexType.new(entity, self)
|
|
77
|
+
]
|
|
78
|
+
end.to_h
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Returns a list of EnumTypes used by the service
|
|
82
|
+
# @return [Hash<String, OData4::EnumType>]
|
|
83
|
+
def enum_types
|
|
84
|
+
@enum_types ||= metadata.xpath('//EnumType').map do |entity|
|
|
85
|
+
[
|
|
86
|
+
entity.attributes['Name'].value,
|
|
87
|
+
::OData4::EnumType.new(entity, self)
|
|
88
|
+
]
|
|
89
|
+
end.to_h
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Returns a hash for finding an association through an entity type's defined
|
|
93
|
+
# NavigationProperty elements.
|
|
94
|
+
# @return [Hash<Hash<OData4::Association>>]
|
|
95
|
+
def navigation_properties
|
|
96
|
+
@navigation_properties ||= metadata.xpath('//EntityType').collect do |entity_type_def|
|
|
97
|
+
entity_type_name = entity_type_def.attributes['Name'].value
|
|
98
|
+
[
|
|
99
|
+
entity_type_name,
|
|
100
|
+
entity_type_def.xpath('./NavigationProperty').collect do |nav_property_def|
|
|
101
|
+
[
|
|
102
|
+
nav_property_def.attributes['Name'].value,
|
|
103
|
+
::OData4::NavigationProperty.build(nav_property_def)
|
|
104
|
+
]
|
|
105
|
+
end.to_h
|
|
106
|
+
]
|
|
107
|
+
end.to_h
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Returns the namespace defined on the service's schema
|
|
111
|
+
def namespace
|
|
112
|
+
@namespace ||= metadata.xpath('//Schema').first.attributes['Namespace'].value
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Returns a more compact inspection of the service object
|
|
116
|
+
def inspect
|
|
117
|
+
"#<#{self.class.name}:#{self.object_id} name='#{name}' service_url='#{self.service_url}'>"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Retrieves the EntitySet associated with a specific EntityType by name
|
|
121
|
+
#
|
|
122
|
+
# @param entity_set_name [to_s] the name of the EntitySet desired
|
|
123
|
+
# @return [OData4::EntitySet] an OData4::EntitySet to query
|
|
124
|
+
def [](entity_set_name)
|
|
125
|
+
xpath_query = "//EntityContainer/EntitySet[@Name='#{entity_set_name}']"
|
|
126
|
+
entity_set_node = metadata.xpath(xpath_query).first
|
|
127
|
+
raise ArgumentError, "Unknown Entity Set: #{entity_set_name}" if entity_set_node.nil?
|
|
128
|
+
container_name = entity_set_node.parent.attributes['Name'].value
|
|
129
|
+
entity_type_name = entity_set_node.attributes['EntityType'].value.gsub(/#{namespace}\./, '')
|
|
130
|
+
OData4::EntitySet.new(name: entity_set_name,
|
|
131
|
+
namespace: namespace,
|
|
132
|
+
type: entity_type_name.to_s,
|
|
133
|
+
service_name: name,
|
|
134
|
+
container: container_name)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Execute a request against the service
|
|
138
|
+
#
|
|
139
|
+
# @param url_chunk [to_s] string to append to service url
|
|
140
|
+
# @param additional_options [Hash] options to pass to Typhoeus
|
|
141
|
+
# @return [Typhoeus::Response]
|
|
142
|
+
def execute(url_chunk, additional_options = {})
|
|
143
|
+
accept = content_type(additional_options.delete(:format) || :auto)
|
|
144
|
+
accept_header = {'Accept' => accept }
|
|
145
|
+
|
|
146
|
+
request_options = options[:typhoeus]
|
|
147
|
+
.merge({ method: :get })
|
|
148
|
+
.merge(additional_options)
|
|
149
|
+
|
|
150
|
+
# Don't overwrite Accept header if already present
|
|
151
|
+
unless request_options[:headers]['Accept']
|
|
152
|
+
request_options[:headers] = request_options[:headers].merge(accept_header)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
request = ::Typhoeus::Request.new(
|
|
156
|
+
URI.join("#{service_url}/", URI.escape(url_chunk)),
|
|
157
|
+
request_options
|
|
158
|
+
)
|
|
159
|
+
logger.info "Requesting #{URI.unescape(request.url)}..."
|
|
160
|
+
request.run
|
|
161
|
+
|
|
162
|
+
response = request.response
|
|
163
|
+
logger.debug(response.headers)
|
|
164
|
+
logger.debug(response.body)
|
|
165
|
+
|
|
166
|
+
validate_response(response)
|
|
167
|
+
response
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Find a specific node in the given result set
|
|
171
|
+
#
|
|
172
|
+
# @param results [Typhoeus::Response]
|
|
173
|
+
# @return [Nokogiri::XML::Element]
|
|
174
|
+
def find_node(results, node_name)
|
|
175
|
+
document = ::Nokogiri::XML(results.body)
|
|
176
|
+
document.remove_namespaces!
|
|
177
|
+
document.xpath("//#{node_name}").first
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Find entity entries in a result set
|
|
181
|
+
#
|
|
182
|
+
# @param results [Typhoeus::Response]
|
|
183
|
+
# @return [Nokogiri::XML::NodeSet]
|
|
184
|
+
def find_entities(results)
|
|
185
|
+
document = ::Nokogiri::XML(results.body)
|
|
186
|
+
document.remove_namespaces!
|
|
187
|
+
document.xpath('//entry')
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Get the property type for an entity from metadata.
|
|
191
|
+
#
|
|
192
|
+
# @param entity_name [to_s] the name of the relevant entity
|
|
193
|
+
# @param property_name [to_s] the property name needed
|
|
194
|
+
# @return [String] the name of the property's type
|
|
195
|
+
def get_property_type(entity_name, property_name)
|
|
196
|
+
metadata.xpath("//EntityType[@Name='#{entity_name}']/Property[@Name='#{property_name}']").first.attributes['Type'].value
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Get the primary key for the supplied Entity.
|
|
200
|
+
#
|
|
201
|
+
# @param entity_name [to_s]
|
|
202
|
+
# @return [String]
|
|
203
|
+
def primary_key_for(entity_name)
|
|
204
|
+
metadata.xpath("//EntityType[@Name='#{entity_name}']/Key/PropertyRef").first.attributes['Name'].value
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Get the list of properties and their various options for the supplied
|
|
208
|
+
# Entity name.
|
|
209
|
+
# @param entity_name [to_s]
|
|
210
|
+
# @return [Hash]
|
|
211
|
+
# @api private
|
|
212
|
+
def properties_for_entity(entity_name)
|
|
213
|
+
type_definition = metadata.xpath("//EntityType[@Name='#{entity_name}']").first
|
|
214
|
+
raise ArgumentError, "Unknown EntityType: #{entity_name}" if type_definition.nil?
|
|
215
|
+
properties_to_return = {}
|
|
216
|
+
type_definition.xpath('./Property').each do |property_xml|
|
|
217
|
+
property_name, property = process_property_from_xml(property_xml)
|
|
218
|
+
properties_to_return[property_name] = property
|
|
219
|
+
end
|
|
220
|
+
properties_to_return
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Returns the log level set via initial options, or the
|
|
224
|
+
# default log level (`Logger::WARN`) if none was specified.
|
|
225
|
+
# @see Logger
|
|
226
|
+
# @return [Fixnum|Symbol]
|
|
227
|
+
def log_level
|
|
228
|
+
options[:log_level] || Logger::WARN
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Returns the logger instance used by the service.
|
|
232
|
+
# When Ruby on Rails has been detected, the service will
|
|
233
|
+
# use `Rails.logger`. The log level will NOT be changed.
|
|
234
|
+
#
|
|
235
|
+
# When no Rails has been detected, a default logger will
|
|
236
|
+
# be used that logs to STDOUT with the log level supplied
|
|
237
|
+
# via options, or the default log level if none was given.
|
|
238
|
+
# @see #log_level
|
|
239
|
+
# @return [Logger]
|
|
240
|
+
def logger
|
|
241
|
+
@logger ||= lambda do
|
|
242
|
+
if defined?(Rails)
|
|
243
|
+
Rails.logger
|
|
244
|
+
else
|
|
245
|
+
logger = Logger.new(STDOUT)
|
|
246
|
+
logger.level = log_level
|
|
247
|
+
logger
|
|
248
|
+
end
|
|
249
|
+
end.call
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Allows the logger to be set to a custom `Logger` instance.
|
|
253
|
+
# @param custom_logger [Logger]
|
|
254
|
+
def logger=(custom_logger)
|
|
255
|
+
@logger = custom_logger
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
private
|
|
259
|
+
|
|
260
|
+
def default_options
|
|
261
|
+
{
|
|
262
|
+
typhoeus: {
|
|
263
|
+
headers: { 'OData4-Version' => '4.0' },
|
|
264
|
+
timeout: HTTP_TIMEOUT
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def content_type(format)
|
|
270
|
+
if format == :auto
|
|
271
|
+
MIME_TYPES.values.join(',')
|
|
272
|
+
elsif MIME_TYPES.has_key? format
|
|
273
|
+
MIME_TYPES[format]
|
|
274
|
+
else
|
|
275
|
+
raise ArgumentError, "Unknown format '#{format}'"
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def metadata
|
|
280
|
+
@metadata ||= lambda { read_metadata }.call
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def read_metadata
|
|
284
|
+
response = nil
|
|
285
|
+
# From file, good for debugging
|
|
286
|
+
if options[:metadata_file]
|
|
287
|
+
data = File.read(options[:metadata_file])
|
|
288
|
+
::Nokogiri::XML(data).remove_namespaces!
|
|
289
|
+
else # From a URL
|
|
290
|
+
METADATA_TIMEOUTS.each do |timeout|
|
|
291
|
+
response = ::Typhoeus::Request.get(URI.escape(metadata_url),
|
|
292
|
+
options[:typhoeus].merge(timeout: timeout))
|
|
293
|
+
break unless response.timed_out?
|
|
294
|
+
end
|
|
295
|
+
raise "Metadata Timeout" if response.timed_out?
|
|
296
|
+
validate_response(response)
|
|
297
|
+
::Nokogiri::XML(response.body).remove_namespaces!
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def validate_response(response)
|
|
302
|
+
raise "Bad Request. #{error_message(response)}" if response.code == 400
|
|
303
|
+
raise "Access Denied" if response.code == 401
|
|
304
|
+
raise "Forbidden" if response.code == 403
|
|
305
|
+
raise "Not Found" if [0,404].include?(response.code)
|
|
306
|
+
raise "Method Not Allowed" if response.code == 405
|
|
307
|
+
raise "Not Acceptable" if response.code == 406
|
|
308
|
+
raise "Request Entity Too Large" if response.code == 413
|
|
309
|
+
raise "Internal Server Error" if response.code == 500
|
|
310
|
+
raise "Service Unavailable" if response.code == 503
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def error_message(response)
|
|
314
|
+
OData4::Query::Result.new(nil, response).error_message
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def process_property_from_xml(property_xml)
|
|
318
|
+
property_name = property_xml.attributes['Name'].value
|
|
319
|
+
value_type = property_xml.attributes['Type'].value
|
|
320
|
+
property_options = {}
|
|
321
|
+
|
|
322
|
+
klass = ::OData4::PropertyRegistry[value_type]
|
|
323
|
+
|
|
324
|
+
if klass.nil?
|
|
325
|
+
raise RuntimeError, "Unknown property type: #{value_type}"
|
|
326
|
+
else
|
|
327
|
+
property_options[:allows_nil] = false if property_xml.attributes['Nullable'] == 'false'
|
|
328
|
+
property = klass.new(property_name, nil, property_options)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
return [property_name, property]
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def register_custom_types
|
|
335
|
+
complex_types.each do |name, type|
|
|
336
|
+
::OData4::PropertyRegistry.add(type.type, type.property_class)
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
enum_types.each do |name, type|
|
|
340
|
+
::OData4::PropertyRegistry.add(type.type, type.property_class)
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
end
|