odata4 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e28c4147df950462c72f8824c2868601941872c5
4
- data.tar.gz: 8044f114aaa10c09719c3d3e5743f5f5c96de9d7
3
+ metadata.gz: 9adfc90966866d69ec4082404f10a60cd8404563
4
+ data.tar.gz: 293c604e274b209c738496e41318f2441d41e159
5
5
  SHA512:
6
- metadata.gz: c10b8c4859b7f2acdbf33d5258f12e43970d24061f558afac558be6212a955e4c5b5118720de78e74ae22231f7bf5479a137591082a0568aee13f5a325350788
7
- data.tar.gz: 8d73fc1f5877df150a21a28ca10871582e58657f0419726fadb1d2c514c62d09517ff1f7eb2d1559bd49d6aea8fb50435e7280bedd60ed174a9528c26d926f05
6
+ metadata.gz: 52bad8a865f20e227aa331f09e680d857caaaa3da8e70df5be873d78477d96b7e4451506e1a70efec9239034c4e98693c0f7b5600f78e5f383fcb56249e6085c
7
+ data.tar.gz: c1868354a432bed72b9b6db7b0e76d6194fd221e05827d3077cdaf9d258687e46bd489a444132154eb95a4835f959ca18e7f64c22dbaf49a41a1299f6d3be382
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 0.8.1
4
+
5
+ * [New Feature] Basic support for `Collection` property type
6
+ * [Refactor] Moved all HTTP-related code into `Service::Request`,
7
+ renamed `Query::Result` to `Service::Response`
8
+ * [Bugfix] Fixed incorrect `OData-Version` header being sent
9
+ * [Bugfix] Fixed duplicate namespace in Atom serialization
10
+
3
11
  ## 0.8.0
4
12
 
5
13
  * [New Feature] Support for multiple schemas
data/README.md CHANGED
@@ -13,6 +13,7 @@ If you need a gem to integration with OData Version 3, you can use James Thompso
13
13
  [![Test Coverage](https://api.codeclimate.com/v1/badges/f151944dc05b2c7268e5/test_coverage)](https://codeclimate.com/github/wrstudios/odata4/test_coverage)
14
14
  [![Dependency Status](https://gemnasium.com/badges/github.com/wrstudios/odata4.svg)](https://gemnasium.com/github.com/wrstudios/odata4)
15
15
  [![Documentation](http://inch-ci.org/github/wrstudios/odata4.png?branch=master)](http://www.rubydoc.info/github/wrstudios/odata4/master)
16
+ [![Gem Version](https://badge.fury.io/rb/odata4.svg)](https://badge.fury.io/rb/odata4)
16
17
 
17
18
  ## Installation
18
19
 
data/lib/odata4/entity.rb CHANGED
@@ -169,7 +169,7 @@ module OData4
169
169
  namespaces = XML_NAMESPACES.merge('xml:base' => service.service_url)
170
170
  builder = Nokogiri::XML::Builder.new do |xml|
171
171
  xml.entry(namespaces) do
172
- xml.category(term: "#{namespace}.#{type}",
172
+ xml.category(term: type,
173
173
  scheme: 'http://docs.oasis-open.org/odata/ns/scheme')
174
174
  xml.author { xml.name }
175
175
 
@@ -236,13 +236,21 @@ module OData4
236
236
  private
237
237
 
238
238
  def instantiate_property(property_name, value_xml)
239
- value_type = schema.get_property_type(name, property_name)
240
- klass = ::OData4::PropertyRegistry[value_type]
239
+ prop_type = schema.get_property_type(name, property_name)
240
+ prop_type, value_type = prop_type.split(/\(|\)/)
241
+
242
+ if prop_type == 'Collection'
243
+ klass = ::OData4::Properties::Collection
244
+ options = { value_type: value_type }
245
+ else
246
+ klass = ::OData4::PropertyRegistry[prop_type]
247
+ options = {}
248
+ end
241
249
 
242
250
  if klass.nil?
243
- raise RuntimeError, "Unknown property type: #{value_type}"
251
+ raise RuntimeError, "Unknown property type: #{prop_type}"
244
252
  else
245
- klass.from_xml(value_xml, service: service)
253
+ klass.from_xml(value_xml, options.merge(service: service))
246
254
  end
247
255
  end
248
256
 
@@ -0,0 +1,50 @@
1
+ module OData4
2
+ module Properties
3
+ # Defines the Collection OData4 type.
4
+ class Collection < OData4::Property
5
+ # Overriding default constructor to avoid converting
6
+ # value to string.
7
+ # TODO: Make this the default for all property types?
8
+ def initialize(name, value, options = {})
9
+ super(name, value, options)
10
+ self.value = value
11
+ end
12
+
13
+ def value
14
+ if @value.nil?
15
+ nil
16
+ else
17
+ @value.map(&:value)
18
+ end
19
+ end
20
+
21
+ def value=(value)
22
+ if value.nil? && allows_nil?
23
+ @value = nil
24
+ elsif value.respond_to?(:map)
25
+ @value = value.map.with_index do |element, index|
26
+ type_class.new("#{name}[#{index}]", element)
27
+ end
28
+ else
29
+ validation_error 'Value must be an array'
30
+ end
31
+ end
32
+
33
+ def url_value
34
+ '[' + @value.map(&:url_value).join(',') + ']'
35
+ end
36
+
37
+ def type
38
+ "Collection(#{value_type})"
39
+ end
40
+
41
+ def value_type
42
+ options[:value_type] || 'Edm.String'
43
+ end
44
+
45
+ def type_class
46
+ OData4::PropertyRegistry[value_type]
47
+ end
48
+ end
49
+ end
50
+ end
@@ -4,6 +4,7 @@ require 'odata4/properties/number'
4
4
  # Implementations
5
5
  require 'odata4/properties/binary'
6
6
  require 'odata4/properties/boolean'
7
+ require 'odata4/properties/collection'
7
8
  require 'odata4/properties/date'
8
9
  require 'odata4/properties/date_time'
9
10
  require 'odata4/properties/date_time_offset'
data/lib/odata4/query.rb CHANGED
@@ -1,3 +1,6 @@
1
+ require 'odata4/query/criteria'
2
+ require 'odata4/query/in_batches'
3
+
1
4
  module OData4
2
5
  # OData4::Query provides the query interface for requesting Entities matching
3
6
  # specific criteria from an OData4::EntitySet. This class should not be
@@ -133,17 +136,16 @@ module OData4
133
136
  end
134
137
 
135
138
  # Execute the query.
136
- # @return [OData4::Query::Result]
137
- def execute(query = self.to_s)
138
- response = service.execute(query, options)
139
- OData4::Query::Result.new(self, response)
139
+ # @return [OData4::Service::Response]
140
+ def execute(url_chunk = self.to_s)
141
+ service.execute(url_chunk, options.merge(query: self))
140
142
  end
141
143
 
142
144
  # Executes the query to get a count of entities.
143
145
  # @return [Integer]
144
146
  def count
145
147
  url_chunk = ["#{entity_set.name}/$count", assemble_criteria].compact.join('?')
146
- response = service.execute(url_chunk)
148
+ response = self.execute(url_chunk)
147
149
  # Some servers (*cough* Microsoft *cough*) seem to
148
150
  # return extraneous characters in the response.
149
151
  response.body.scan(/\d+/).first.to_i
data/lib/odata4/schema.rb CHANGED
@@ -128,10 +128,16 @@ module OData4
128
128
 
129
129
  def process_property_from_xml(property_xml)
130
130
  property_name = property_xml.attributes['Name'].value
131
- value_type = property_xml.attributes['Type'].value
131
+ property_type = property_xml.attributes['Type'].value
132
132
  property_options = { service: service }
133
133
 
134
- klass = ::OData4::PropertyRegistry[value_type]
134
+ property_type, value_type = property_type.split(/\(|\)/)
135
+ if property_type == 'Collection'
136
+ klass = ::OData4::Properties::Collection
137
+ property_options.merge(value_type: value_type)
138
+ else
139
+ klass = ::OData4::PropertyRegistry[property_type]
140
+ end
135
141
 
136
142
  if klass.nil?
137
143
  raise RuntimeError, "Unknown property type: #{value_type}"
@@ -0,0 +1,81 @@
1
+ module OData4
2
+ class Service
3
+ # Encapsulates a single request to an OData service.
4
+ class Request
5
+ # The OData service against which the request is performed
6
+ attr_reader :service
7
+ # The OData4::Query that generated this request (optional)
8
+ attr_reader :query
9
+ # The HTTP method for this request
10
+ attr_accessor :method
11
+ # The request format (`:atom`, `:json`, or `:auto`)
12
+ attr_accessor :format
13
+
14
+ # Create a new request
15
+ # @param service [OData4::Service] Where the request will be sent
16
+ # @param url_chunk [String] Request path, relative to the service URL, including query params
17
+ # @param options [Hash] Additional request options
18
+ def initialize(service, url_chunk, options = {})
19
+ @service = service
20
+ @url_chunk = url_chunk
21
+ @method = options[:method] || :get
22
+ @format = options[:format] || :auto
23
+ @query = options[:query]
24
+ end
25
+
26
+ # Return the full request URL (including service base)
27
+ # @return [String]
28
+ def url
29
+ ::URI.join("#{service.service_url}/", ::URI.escape(url_chunk)).to_s
30
+ end
31
+
32
+ # The content type for this request. Depends on format.
33
+ # @return [String]
34
+ def content_type
35
+ if format == :auto
36
+ MIME_TYPES.values.join(',')
37
+ elsif MIME_TYPES.has_key? format
38
+ MIME_TYPES[format]
39
+ else
40
+ raise ArgumentError, "Unknown format '#{format}'"
41
+ end
42
+ end
43
+
44
+ # Execute the request
45
+ #
46
+ # @param additional_options [Hash] options to pass to Typhoeus
47
+ # @return [OData4::Service::Response]
48
+ def execute(additional_options = {})
49
+ options = request_options(additional_options)
50
+ request = ::Typhoeus::Request.new(url, options)
51
+ logger.info "Requesting #{method.to_s.upcase} #{url}..."
52
+ request.run
53
+
54
+ Response.new(service, request.response, query)
55
+ end
56
+
57
+ private
58
+
59
+ attr_reader :url_chunk
60
+
61
+ def logger
62
+ service.logger
63
+ end
64
+
65
+ def request_options(additional_options = {})
66
+ options = service.options[:typhoeus]
67
+ .merge({ method: method })
68
+ .merge(additional_options)
69
+
70
+ # Don't overwrite Accept header if already present
71
+ unless options[:headers]['Accept']
72
+ options[:headers] = options[:headers].merge({
73
+ 'Accept' => content_type
74
+ })
75
+ end
76
+
77
+ options
78
+ end
79
+ end
80
+ end
81
+ end
@@ -1,14 +1,9 @@
1
1
  module OData4
2
- class Query
3
- class Result
4
- # Represents the results of executing a OData4::Query.
5
- # @api private
2
+ class Service
3
+ class Response
6
4
  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
5
+ def parse_entity(entity_xml, entity_options)
6
+ OData4::Entity.from_xml(entity_xml, entity_options)
12
7
  end
13
8
 
14
9
  def next_page
@@ -23,10 +18,14 @@ module OData4
23
18
  result_xml.xpath('//error/message').first.andand.text
24
19
  end
25
20
 
21
+ def parsed_body
22
+ result_xml
23
+ end
24
+
26
25
  private
27
26
 
28
27
  def result_xml
29
- @result_xml ||= ::Nokogiri::XML(result.body).remove_namespaces!
28
+ @result_xml ||= ::Nokogiri::XML(response.body).remove_namespaces!
30
29
  end
31
30
 
32
31
  # Find entity entries in a result set
@@ -1,14 +1,9 @@
1
1
  module OData4
2
- class Query
3
- class Result
4
- # Represents the results of executing a OData4::Query.
5
- # @api private
2
+ class Service
3
+ class Response
6
4
  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
5
+ def parse_entity(entity_json, entity_options)
6
+ OData4::Entity.from_json(entity_json, entity_options)
12
7
  end
13
8
 
14
9
  def next_page
@@ -23,10 +18,14 @@ module OData4
23
18
  result_json['error'].andand['message']
24
19
  end
25
20
 
21
+ def parsed_body
22
+ result_json
23
+ end
24
+
26
25
  private
27
26
 
28
27
  def result_json
29
- @result_json ||= ::JSON.parse(result.body)
28
+ @result_json ||= ::JSON.parse(response.body)
30
29
  end
31
30
 
32
31
  def single_entity?
@@ -0,0 +1,36 @@
1
+ module OData4
2
+ class Service
3
+ class Response
4
+ module Plain
5
+ def parse_entity(entity_data, entity_options)
6
+ raise NotImplementedError, 'Not Available'
7
+ end
8
+
9
+ def next_page
10
+ raise NotImplementedError, 'Not available'
11
+ end
12
+
13
+ def next_page_url
14
+ raise NotImplementedError, 'Not available'
15
+ end
16
+
17
+ def error_message
18
+ response.body
19
+ end
20
+
21
+ def parsed_body
22
+ response.body
23
+ end
24
+
25
+ private
26
+
27
+ # Find entity entries in a response set
28
+ #
29
+ # @return [Array]
30
+ def find_entities
31
+ []
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,40 @@
1
+ module OData4
2
+ class Service
3
+ class Response
4
+ module XML
5
+ def parse_entity(entity_data, entity_options)
6
+ raise NotImplementedError, 'Not Available'
7
+ end
8
+
9
+ def next_page
10
+ raise NotImplementedError, 'Not Available'
11
+ end
12
+
13
+ def next_page_url
14
+ raise NotImplementedError, 'Not Available'
15
+ end
16
+
17
+ def error_message
18
+ response_xml.xpath('//error/message').first.andand.text
19
+ end
20
+
21
+ def parsed_body
22
+ response_xml
23
+ end
24
+
25
+ private
26
+
27
+ def response_xml
28
+ @response_xml ||= ::Nokogiri::XML(response.body).remove_namespaces!
29
+ end
30
+
31
+ # Find entity entries in a response set
32
+ #
33
+ # @return [Array]
34
+ def find_entities
35
+ []
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,160 @@
1
+ require 'odata4/service/response/atom'
2
+ require 'odata4/service/response/json'
3
+ require 'odata4/service/response/plain'
4
+ require 'odata4/service/response/xml'
5
+
6
+ module OData4
7
+ class Service
8
+ # The result of executing a OData4::Service::Request.
9
+ class Response
10
+ include Enumerable
11
+
12
+ # The service that generated this response
13
+ attr_reader :service
14
+ # The underlying (raw) response
15
+ attr_reader :response
16
+ # The query that generated the response (optional)
17
+ attr_reader :query
18
+
19
+ # Create a new response given a service and a raw response.
20
+ # @param service [OData4::Service]
21
+ # @param response [Typhoeus::Result]
22
+ def initialize(service, response, query = nil)
23
+ @service = service
24
+ @response = response
25
+ @query = query
26
+ check_content_type
27
+ validate!
28
+ end
29
+
30
+ # Returns the HTTP status code.
31
+ def status
32
+ response.code
33
+ end
34
+
35
+ # Whether the request was successful.
36
+ def success?
37
+ 200 <= status && status < 300
38
+ end
39
+
40
+ # Returns the content type of the resonse.
41
+ def content_type
42
+ response.headers['Content-Type'] || ''
43
+ end
44
+
45
+ def is_atom?
46
+ content_type =~ /#{Regexp.escape OData4::Service::MIME_TYPES[:atom]}/
47
+ end
48
+
49
+ def is_json?
50
+ content_type =~ /#{Regexp.escape OData4::Service::MIME_TYPES[:json]}/
51
+ end
52
+
53
+ def is_plain?
54
+ content_type =~ /#{Regexp.escape OData4::Service::MIME_TYPES[:plain]}/
55
+ end
56
+
57
+ def is_xml?
58
+ content_type =~ /#{Regexp.escape OData4::Service::MIME_TYPES[:xml]}/
59
+ end
60
+
61
+ # Whether the response contained any entities.
62
+ # @return [Boolean]
63
+ def empty?
64
+ @empty ||= find_entities.empty?
65
+ end
66
+
67
+ # Whether the response failed due to a timeout
68
+ def timed_out?
69
+ response.timed_out?
70
+ end
71
+
72
+ # Iterates over all entities in the response, using
73
+ # automatic paging if necessary.
74
+ # Provided for Enumerable functionality.
75
+ # @param block [block] a block to evaluate
76
+ # @return [OData4::Entity] each entity in turn for the query result
77
+ def each(&block)
78
+ unless empty?
79
+ process_results(&block)
80
+ unless next_page.nil?
81
+ # ensure request gets executed with the same options
82
+ query.execute(URI.decode next_page_url).each(&block)
83
+ end
84
+ end
85
+ end
86
+
87
+ # Returns the response body.
88
+ def body
89
+ response.body
90
+ end
91
+
92
+ # Validates the response. Throws an exception with
93
+ # an appropriate message if a 4xx or 5xx status code
94
+ # occured.
95
+ #
96
+ # @return [self]
97
+ def validate!
98
+ raise "Bad Request. #{error_message(response)}" if response.code == 400
99
+ raise "Access Denied" if response.code == 401
100
+ raise "Forbidden" if response.code == 403
101
+ raise "Not Found" if [0,404].include?(response.code)
102
+ raise "Method Not Allowed" if response.code == 405
103
+ raise "Not Acceptable" if response.code == 406
104
+ raise "Request Entity Too Large" if response.code == 413
105
+ raise "Internal Server Error" if response.code == 500
106
+ raise "Service Unavailable" if response.code == 503
107
+ self
108
+ end
109
+
110
+ private
111
+
112
+ def logger
113
+ service.logger
114
+ end
115
+
116
+ def check_content_type
117
+ logger.debug <<-EOS
118
+ [OData4: #{service.name}] Received response:
119
+ Headers: #{response.headers}
120
+ Body: #{response.body}
121
+ EOS
122
+ # Dynamically extend instance with methods for
123
+ # processing the current result type
124
+ if is_atom?
125
+ extend OData4::Service::Response::Atom
126
+ elsif is_json?
127
+ extend OData4::Service::Response::JSON
128
+ elsif is_xml?
129
+ extend OData4::Service::Response::XML
130
+ elsif is_plain?
131
+ extend OData4::Service::Response::Plain
132
+ elsif response.body.empty?
133
+ # Some services (*cough* Microsoft *cough*) return
134
+ # an empty response with no `Content-Type` header set.
135
+ # We catch that here and bypass content type detection.
136
+ @empty = true
137
+ else
138
+ raise ArgumentError, "Invalid response type '#{content_type}'"
139
+ end
140
+ end
141
+
142
+ def entity_options
143
+ if query
144
+ query.entity_set.entity_options
145
+ else
146
+ {
147
+ service_name: service.name,
148
+ }
149
+ end
150
+ end
151
+
152
+ def process_results(&block)
153
+ find_entities.each do |entity_data|
154
+ entity = parse_entity(entity_data, entity_options)
155
+ block_given? ? block.call(entity) : yield(entity)
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end