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
@@ -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
|