odata4 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (114) hide show
  1. checksums.yaml +7 -0
  2. data/.autotest +2 -0
  3. data/.gitignore +24 -0
  4. data/.rspec +2 -0
  5. data/.ruby-gemset +1 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +75 -0
  8. data/CHANGELOG.md +120 -0
  9. data/Gemfile +4 -0
  10. data/LICENSE.txt +23 -0
  11. data/README.md +287 -0
  12. data/Rakefile +7 -0
  13. data/TODO.md +55 -0
  14. data/lib/odata4.rb +37 -0
  15. data/lib/odata4/complex_type.rb +76 -0
  16. data/lib/odata4/complex_type/property.rb +114 -0
  17. data/lib/odata4/entity.rb +319 -0
  18. data/lib/odata4/entity_set.rb +162 -0
  19. data/lib/odata4/enum_type.rb +95 -0
  20. data/lib/odata4/enum_type/property.rb +62 -0
  21. data/lib/odata4/navigation_property.rb +29 -0
  22. data/lib/odata4/navigation_property/proxy.rb +76 -0
  23. data/lib/odata4/properties.rb +25 -0
  24. data/lib/odata4/properties/binary.rb +50 -0
  25. data/lib/odata4/properties/boolean.rb +37 -0
  26. data/lib/odata4/properties/date.rb +27 -0
  27. data/lib/odata4/properties/date_time.rb +83 -0
  28. data/lib/odata4/properties/date_time_offset.rb +17 -0
  29. data/lib/odata4/properties/decimal.rb +50 -0
  30. data/lib/odata4/properties/float.rb +67 -0
  31. data/lib/odata4/properties/geography.rb +13 -0
  32. data/lib/odata4/properties/geography/base.rb +162 -0
  33. data/lib/odata4/properties/geography/line_string.rb +33 -0
  34. data/lib/odata4/properties/geography/point.rb +31 -0
  35. data/lib/odata4/properties/geography/polygon.rb +38 -0
  36. data/lib/odata4/properties/guid.rb +17 -0
  37. data/lib/odata4/properties/integer.rb +107 -0
  38. data/lib/odata4/properties/number.rb +14 -0
  39. data/lib/odata4/properties/string.rb +72 -0
  40. data/lib/odata4/properties/time.rb +40 -0
  41. data/lib/odata4/properties/time_of_day.rb +27 -0
  42. data/lib/odata4/property.rb +118 -0
  43. data/lib/odata4/property_registry.rb +41 -0
  44. data/lib/odata4/query.rb +231 -0
  45. data/lib/odata4/query/criteria.rb +92 -0
  46. data/lib/odata4/query/criteria/comparison_operators.rb +49 -0
  47. data/lib/odata4/query/criteria/date_functions.rb +61 -0
  48. data/lib/odata4/query/criteria/geography_functions.rb +21 -0
  49. data/lib/odata4/query/criteria/lambda_operators.rb +27 -0
  50. data/lib/odata4/query/criteria/string_functions.rb +40 -0
  51. data/lib/odata4/query/in_batches.rb +58 -0
  52. data/lib/odata4/query/result.rb +84 -0
  53. data/lib/odata4/query/result/atom.rb +41 -0
  54. data/lib/odata4/query/result/json.rb +42 -0
  55. data/lib/odata4/railtie.rb +19 -0
  56. data/lib/odata4/service.rb +344 -0
  57. data/lib/odata4/service_registry.rb +52 -0
  58. data/lib/odata4/version.rb +3 -0
  59. data/odata4.gemspec +34 -0
  60. data/spec/fixtures/files/entity_to_xml.xml +17 -0
  61. data/spec/fixtures/files/metadata.xml +150 -0
  62. data/spec/fixtures/files/product_0.json +10 -0
  63. data/spec/fixtures/files/product_0.xml +28 -0
  64. data/spec/fixtures/files/products.json +106 -0
  65. data/spec/fixtures/files/products.xml +308 -0
  66. data/spec/fixtures/files/supplier_0.json +26 -0
  67. data/spec/fixtures/files/supplier_0.xml +32 -0
  68. data/spec/fixtures/vcr_cassettes/complex_type_specs.yml +127 -0
  69. data/spec/fixtures/vcr_cassettes/entity_set_specs.yml +1348 -0
  70. data/spec/fixtures/vcr_cassettes/entity_set_specs/bad_entry.yml +183 -0
  71. data/spec/fixtures/vcr_cassettes/entity_set_specs/existing_entry.yml +256 -0
  72. data/spec/fixtures/vcr_cassettes/entity_set_specs/new_entry.yml +185 -0
  73. data/spec/fixtures/vcr_cassettes/entity_specs.yml +285 -0
  74. data/spec/fixtures/vcr_cassettes/navigation_property_proxy_specs.yml +346 -0
  75. data/spec/fixtures/vcr_cassettes/query/result_specs.yml +189 -0
  76. data/spec/fixtures/vcr_cassettes/query_specs.yml +663 -0
  77. data/spec/fixtures/vcr_cassettes/service_registry_specs.yml +129 -0
  78. data/spec/fixtures/vcr_cassettes/service_specs.yml +127 -0
  79. data/spec/fixtures/vcr_cassettes/usage_example_specs.yml +749 -0
  80. data/spec/odata4/complex_type_spec.rb +116 -0
  81. data/spec/odata4/entity/shared_examples.rb +82 -0
  82. data/spec/odata4/entity_set_spec.rb +168 -0
  83. data/spec/odata4/entity_spec.rb +151 -0
  84. data/spec/odata4/enum_type_spec.rb +134 -0
  85. data/spec/odata4/navigation_property/proxy_spec.rb +44 -0
  86. data/spec/odata4/navigation_property_spec.rb +55 -0
  87. data/spec/odata4/properties/binary_spec.rb +50 -0
  88. data/spec/odata4/properties/boolean_spec.rb +72 -0
  89. data/spec/odata4/properties/date_spec.rb +23 -0
  90. data/spec/odata4/properties/date_time_offset_spec.rb +30 -0
  91. data/spec/odata4/properties/date_time_spec.rb +23 -0
  92. data/spec/odata4/properties/decimal_spec.rb +24 -0
  93. data/spec/odata4/properties/float_spec.rb +45 -0
  94. data/spec/odata4/properties/geography/line_string_spec.rb +33 -0
  95. data/spec/odata4/properties/geography/point_spec.rb +29 -0
  96. data/spec/odata4/properties/geography/polygon_spec.rb +55 -0
  97. data/spec/odata4/properties/geography/shared_examples.rb +72 -0
  98. data/spec/odata4/properties/guid_spec.rb +17 -0
  99. data/spec/odata4/properties/integer_spec.rb +58 -0
  100. data/spec/odata4/properties/string_spec.rb +46 -0
  101. data/spec/odata4/properties/time_of_day_spec.rb +23 -0
  102. data/spec/odata4/properties/time_spec.rb +15 -0
  103. data/spec/odata4/property_registry_spec.rb +16 -0
  104. data/spec/odata4/property_spec.rb +32 -0
  105. data/spec/odata4/query/criteria_spec.rb +229 -0
  106. data/spec/odata4/query/result_spec.rb +53 -0
  107. data/spec/odata4/query_spec.rb +196 -0
  108. data/spec/odata4/service_registry_spec.rb +18 -0
  109. data/spec/odata4/service_spec.rb +80 -0
  110. data/spec/odata4/usage_example_spec.rb +176 -0
  111. data/spec/spec_helper.rb +32 -0
  112. data/spec/support/coverage.rb +2 -0
  113. data/spec/support/vcr.rb +9 -0
  114. 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