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