odata4 0.8.0 → 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
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