odata 0.0.10 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +5 -5
- data/lib/odata.rb +5 -1
- data/lib/odata/entity.rb +71 -3
- data/lib/odata/entity_set.rb +45 -1
- data/lib/odata/properties/date_time.rb +3 -1
- data/lib/odata/property.rb +4 -0
- data/lib/odata/service.rb +46 -55
- data/lib/odata/version.rb +1 -1
- data/odata.gemspec +1 -0
- data/spec/fixtures/sample_service/first_product.xml +42 -0
- data/spec/fixtures/sample_service/product_9999.xml +34 -0
- data/spec/odata/entity_set_spec.rb +62 -2
- data/spec/odata/entity_spec.rb +3 -0
- data/spec/odata/service_spec.rb +0 -21
- data/spec/spec_helper.rb +6 -0
- metadata +21 -7
- data/lib/odata/model.rb +0 -116
- data/spec/examples/product_model.rb +0 -13
- data/spec/odata/model_spec.rb +0 -25
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a62f54f63a4c6c397d711b31eea1d31f41383e80
|
4
|
+
data.tar.gz: 8a4e945b39e401bfbaaaba43e2e52c884949318a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 591263ed5f45b8f12ef3ebf167bed9fff9e3e61c98de34b58601a5c3db2b89f18c4207feabbdccf08d9970a67c93f0f2d340747b7f767f64a615255c54769735
|
7
|
+
data.tar.gz: 4bc3b99da624d26e44d77ff8400c1ae5067d77aa56a8f435f739b37de0d50de3dc28967e8aae3fce004491e00808167fdd022133d2d25782347b28296d698cc3
|
data/CHANGELOG.md
ADDED
data/README.md
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
# OData
|
2
2
|
|
3
|
-
[![Build Status](https://travis-ci.org/
|
4
|
-
[![Code Climate](https://codeclimate.com/github/
|
5
|
-
[![Coverage](https://codeclimate.com/github/
|
3
|
+
[![Build Status](https://travis-ci.org/ruby-odata/odata.svg?branch=master)](https://travis-ci.org/ruby-odata/odata)
|
4
|
+
[![Code Climate](https://codeclimate.com/github/ruby-odata/odata.png)](https://codeclimate.com/github/ruby-odata/odata)
|
5
|
+
[![Coverage](https://codeclimate.com/github/ruby-odata/odata/coverage.png)](https://codeclimate.com/github/ruby-odata/odata)
|
6
6
|
[![Gem Version](https://badge.fury.io/rb/odata.svg)](http://badge.fury.io/rb/odata)
|
7
|
-
[![Dependency Status](https://gemnasium.com/
|
8
|
-
[![Documentation](http://inch-ci.org/github/
|
7
|
+
[![Dependency Status](https://gemnasium.com/ruby-odata/odata.svg)](https://gemnasium.com/ruby-odata/odata)
|
8
|
+
[![Documentation](http://inch-ci.org/github/ruby-odata/odata.png?branch=master)](http://rubydoc.info/github/ruby-odata/odata/master/frames)
|
9
9
|
|
10
10
|
The OData gem provides a simple wrapper around the OData API protocol. It has
|
11
11
|
the ability to automatically inspect compliant APIs and expose the relevant
|
data/lib/odata.rb
CHANGED
@@ -9,14 +9,18 @@ require 'active_support/core_ext'
|
|
9
9
|
require 'active_support/concern'
|
10
10
|
|
11
11
|
require 'odata/version'
|
12
|
+
require 'odata/property'
|
13
|
+
require 'odata/properties'
|
12
14
|
require 'odata/entity'
|
13
15
|
require 'odata/entity_set'
|
14
16
|
require 'odata/service'
|
15
17
|
require 'odata/service_registry'
|
16
|
-
require 'odata/model'
|
17
18
|
|
18
19
|
require 'odata/railtie' if defined?(::Rails)
|
19
20
|
|
21
|
+
# The OData gem provides a convenient way to interact with OData services from
|
22
|
+
# Ruby. Please look to the {file:README.md README} for how to get started using
|
23
|
+
# the OData gem.
|
20
24
|
module OData
|
21
25
|
# Your code goes here...
|
22
26
|
end
|
data/lib/odata/entity.rb
CHANGED
@@ -5,7 +5,6 @@ module OData
|
|
5
5
|
def initialize(options = {})
|
6
6
|
@type = options[:type]
|
7
7
|
@namespace = options[:namespace]
|
8
|
-
@properties = {}
|
9
8
|
end
|
10
9
|
|
11
10
|
def name
|
@@ -13,7 +12,34 @@ module OData
|
|
13
12
|
end
|
14
13
|
|
15
14
|
def [](property_name)
|
16
|
-
|
15
|
+
begin
|
16
|
+
properties[property_name.to_s].value
|
17
|
+
rescue NoMethodError
|
18
|
+
raise ArgumentError, "Unknown property: #{property_name}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def []=(property_name, value)
|
23
|
+
begin
|
24
|
+
properties[property_name.to_s].value = value
|
25
|
+
rescue NoMethodError
|
26
|
+
raise ArgumentError, "Unknown property: #{property_name}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.with_properties(new_properties = {}, options = {})
|
31
|
+
entity = OData::Entity.new(options)
|
32
|
+
entity.instance_eval do
|
33
|
+
# TODO Define the properties
|
34
|
+
service.properties_for(name).each do |name, instance|
|
35
|
+
set_property(name, instance)
|
36
|
+
end
|
37
|
+
|
38
|
+
new_properties.each do |property_name, property_value|
|
39
|
+
self[property_name] = property_value
|
40
|
+
end
|
41
|
+
end
|
42
|
+
entity
|
17
43
|
end
|
18
44
|
|
19
45
|
def self.from_xml(xml_doc, options = {})
|
@@ -55,14 +81,56 @@ module OData
|
|
55
81
|
entity
|
56
82
|
end
|
57
83
|
|
84
|
+
def to_xml
|
85
|
+
builder = Nokogiri::XML::Builder.new do |xml|
|
86
|
+
xml.entry('xmlns' => 'http://www.w3.org/2005/Atom',
|
87
|
+
'xmlns:data' => 'http://schemas.microsoft.com/ado/2007/08/dataservices',
|
88
|
+
'xmlns:metadata' => 'http://schemas.microsoft.com/ado/2007/08/dataservices/metadata',
|
89
|
+
'xmlns:georss' => 'http://www.georss.org/georss',
|
90
|
+
'xmlns:gml' => 'http://www.opengis.net/gml',
|
91
|
+
'xml:base' => 'http://services.odata.org/OData/OData.svc/') do
|
92
|
+
xml.category(term: "#{namespace}.#{type}",
|
93
|
+
scheme: 'http://schemas.microsoft.com/ado/2007/08/dataservices/scheme')
|
94
|
+
xml.author { xml.name }
|
95
|
+
|
96
|
+
xml.content(type: 'application/xml') do
|
97
|
+
xml['metadata'].properties do
|
98
|
+
properties.each do |name, property|
|
99
|
+
next if name == primary_key
|
100
|
+
attributes = {
|
101
|
+
'metadata:type' => property.type,
|
102
|
+
}
|
103
|
+
|
104
|
+
if property.value.nil?
|
105
|
+
attributes['metadata:null'] = 'true'
|
106
|
+
xml['data'].send(name.to_sym, attributes)
|
107
|
+
else
|
108
|
+
xml['data'].send(name.to_sym, attributes, property.xml_value)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
builder.to_xml
|
116
|
+
end
|
117
|
+
|
118
|
+
def primary_key
|
119
|
+
service.primary_key_for(name)
|
120
|
+
end
|
121
|
+
|
58
122
|
private
|
59
123
|
|
124
|
+
def properties
|
125
|
+
@properties ||= {}
|
126
|
+
end
|
127
|
+
|
60
128
|
def service
|
61
129
|
@service ||= OData::ServiceRegistry[namespace]
|
62
130
|
end
|
63
131
|
|
64
132
|
def set_property(name, odata_property)
|
65
|
-
|
133
|
+
properties[name.to_s] = odata_property.dup
|
66
134
|
end
|
67
135
|
end
|
68
136
|
end
|
data/lib/odata/entity_set.rb
CHANGED
@@ -39,7 +39,7 @@ module OData
|
|
39
39
|
page_position = 0
|
40
40
|
end
|
41
41
|
|
42
|
-
entity = OData::Entity.from_xml(entities[page_position],
|
42
|
+
entity = OData::Entity.from_xml(entities[page_position], entity_options)
|
43
43
|
block_given? ? block.call(entity) : yield(entity)
|
44
44
|
|
45
45
|
counter += 1
|
@@ -47,17 +47,61 @@ module OData
|
|
47
47
|
end
|
48
48
|
end
|
49
49
|
|
50
|
+
def first
|
51
|
+
result = service.execute("#{name}?$skip=0&$top=1")
|
52
|
+
entities = service.find_entities(result)
|
53
|
+
OData::Entity.from_xml(entities[0], entity_options)
|
54
|
+
end
|
55
|
+
|
50
56
|
# Returns the number of entities within the set
|
51
57
|
def count
|
52
58
|
service.execute("#{name}/$count").body.to_i
|
53
59
|
end
|
54
60
|
|
61
|
+
def new_entity(properties = {})
|
62
|
+
OData::Entity.with_properties(properties, entity_options)
|
63
|
+
end
|
64
|
+
|
65
|
+
def <<(entity)
|
66
|
+
new_entity = entity[entity.primary_key].nil?
|
67
|
+
|
68
|
+
url_chunk = name
|
69
|
+
url_chunk += "(#{entity[entity.primary_key]})" unless new_entity
|
70
|
+
|
71
|
+
options = {
|
72
|
+
method: :post,
|
73
|
+
body: entity.to_xml.gsub(/\n\s+/, ''),
|
74
|
+
headers: {
|
75
|
+
Accept: 'application/atom+xml',
|
76
|
+
'Content-Type' => 'application/atom+xml'
|
77
|
+
}
|
78
|
+
}
|
79
|
+
|
80
|
+
result = service.execute(url_chunk, options)
|
81
|
+
if result.code.to_s =~ /^2[0-9][0-9]$/
|
82
|
+
if new_entity
|
83
|
+
doc = ::Nokogiri::XML(result.body).remove_namespaces!
|
84
|
+
entity[entity.primary_key] = doc.xpath("//content/properties/#{entity.primary_key}").first.content
|
85
|
+
end
|
86
|
+
else
|
87
|
+
raise StandardError, 'Something went wrong committing your entity'
|
88
|
+
end
|
89
|
+
entity
|
90
|
+
end
|
91
|
+
|
55
92
|
private
|
56
93
|
|
57
94
|
def service
|
58
95
|
@service ||= OData::ServiceRegistry[namespace]
|
59
96
|
end
|
60
97
|
|
98
|
+
def entity_options
|
99
|
+
{
|
100
|
+
namespace: namespace,
|
101
|
+
type: type
|
102
|
+
}
|
103
|
+
end
|
104
|
+
|
61
105
|
def get_paginated_entities(per_page, page)
|
62
106
|
result = service.execute("#{name}?$inlinecount=allpages&$skip=#{per_page * page}&$top=#{per_page}")
|
63
107
|
entities = service.find_entities(result)
|
@@ -26,14 +26,16 @@ module OData
|
|
26
26
|
|
27
27
|
def validate(value)
|
28
28
|
begin
|
29
|
+
return if value.nil? && allows_nil?
|
29
30
|
return if value.is_a?(::DateTime)
|
30
31
|
::DateTime.strptime(value, '%Y-%m-%dT%H:%M:%S.%L')
|
31
|
-
rescue
|
32
|
+
rescue
|
32
33
|
raise ArgumentError, 'Value is not a date time format that can be parsed'
|
33
34
|
end
|
34
35
|
end
|
35
36
|
|
36
37
|
def parse_value(value)
|
38
|
+
return value if value.nil? && allows_nil?
|
37
39
|
parsed_value = value
|
38
40
|
parsed_value = ::DateTime.parse(value) unless value.is_a?(::DateTime)
|
39
41
|
parsed_value.strftime('%Y-%m-%dT%H:%M:%S.%L')
|
data/lib/odata/property.rb
CHANGED
data/lib/odata/service.rb
CHANGED
@@ -61,27 +61,9 @@ module OData
|
|
61
61
|
"#<#{self.class.name}:#{self.object_id} namespace='#{self.namespace}' service_url='#{self.service_url}'>"
|
62
62
|
end
|
63
63
|
|
64
|
-
# Handles getting OData resources from the service.
|
65
|
-
#
|
66
|
-
# @param model [OData::Model] the type of resource being requested
|
67
|
-
# @param criteria [Hash] any criteria to narrow the request
|
68
|
-
# @return [Array] instances of the requested model
|
69
|
-
def get(model, criteria = {})
|
70
|
-
request = ::Typhoeus::Request.new(
|
71
|
-
build_request_url(model, criteria),
|
72
|
-
options[:typhoeus].merge({
|
73
|
-
method: :get
|
74
|
-
})
|
75
|
-
)
|
76
|
-
request.run
|
77
|
-
response = request.response
|
78
|
-
feed = ::Nokogiri::XML(response.body).remove_namespaces!
|
79
|
-
feed.xpath('//entry').collect {|entry| parse_model_from_feed(model, entry)}
|
80
|
-
end
|
81
|
-
|
82
64
|
# Retrieves the EntitySet associated with a specific EntityType by name
|
83
65
|
#
|
84
|
-
# @param
|
66
|
+
# @param entity_set_name [to_s] the name of the EntitySet desired
|
85
67
|
# @return [OData::EntitySet] an OData::EntitySet to query
|
86
68
|
def [](entity_set_name)
|
87
69
|
xpath_query = "//EntityContainer/EntitySet[@Name='#{entity_set_name}']"
|
@@ -95,6 +77,11 @@ module OData
|
|
95
77
|
container: container_name)
|
96
78
|
end
|
97
79
|
|
80
|
+
# Execute a request against the service
|
81
|
+
#
|
82
|
+
# @param url_chunk [to_s] string to append to service url
|
83
|
+
# @param additional_options [Hash] options to pass to Typhoeus
|
84
|
+
# @return [Typhoeus::Response]
|
98
85
|
def execute(url_chunk, additional_options = {})
|
99
86
|
request = ::Typhoeus::Request.new(
|
100
87
|
"#{service_url}/#{url_chunk}",
|
@@ -106,30 +93,70 @@ module OData
|
|
106
93
|
request.response
|
107
94
|
end
|
108
95
|
|
96
|
+
# Find a specific node in the given result set
|
97
|
+
#
|
98
|
+
# @param results [Typhoeus::Response]
|
99
|
+
# @return [Nokogiri::XML::Element]
|
109
100
|
def find_node(results, node_name)
|
110
101
|
document = ::Nokogiri::XML(results.body)
|
111
102
|
document.remove_namespaces!
|
112
103
|
document.xpath("//#{node_name}").first
|
113
104
|
end
|
114
105
|
|
106
|
+
# Find entity entries in a result set
|
107
|
+
#
|
108
|
+
# @param results [Typhoeus::Response]
|
109
|
+
# @return [Nokogiri::XML::NodeSet]
|
115
110
|
def find_entities(results)
|
116
111
|
document = ::Nokogiri::XML(results.body)
|
117
112
|
document.remove_namespaces!
|
118
113
|
document.xpath('//entry')
|
119
114
|
end
|
120
115
|
|
116
|
+
# Get the property type for an entity from metadata.
|
117
|
+
#
|
118
|
+
# @param entity_name [to_s] the name of the relevant entity
|
119
|
+
# @param property_name [to_s] the property name needed
|
120
|
+
# @return [String] the name of the property's type
|
121
121
|
def get_property_type(entity_name, property_name)
|
122
122
|
metadata.xpath("//EntityType[@Name='#{entity_name}']/Property[@Name='#{property_name}']").first.attributes['Type'].value
|
123
123
|
end
|
124
124
|
|
125
|
+
# Get the property used as the title for an entity from metadata.
|
126
|
+
#
|
127
|
+
# @param entity_name [to_s] the name of the relevant entity
|
128
|
+
# @return [String] the name of the property used as the entity title
|
125
129
|
def get_title_property_name(entity_name)
|
126
130
|
metadata.xpath("//EntityType[@Name='#{entity_name}']/Property[@FC_TargetPath='SyndicationTitle']").first.attributes['Name'].value
|
127
131
|
end
|
128
132
|
|
133
|
+
# Get the property used as the summary for an entity from metadata.
|
134
|
+
#
|
135
|
+
# @param entity_name [to_s] the name of the relevant entity
|
136
|
+
# @return [String] the name of the property used as the entity summary
|
129
137
|
def get_summary_property_name(entity_name)
|
130
138
|
metadata.xpath("//EntityType[@Name='#{entity_name}']/Property[@FC_TargetPath='SyndicationSummary']").first.attributes['Name'].value
|
131
139
|
end
|
132
140
|
|
141
|
+
def primary_key_for(entity_name)
|
142
|
+
metadata.xpath("//EntityType[@Name='#{entity_name}']/Key/PropertyRef").first.attributes['Name'].value
|
143
|
+
end
|
144
|
+
|
145
|
+
def properties_for(entity_name)
|
146
|
+
type_definition = metadata.xpath("//EntityType[@Name='#{entity_name}']").first
|
147
|
+
raise ArgumentError, "Unknown EntityType: #{entity_name}" if type_definition.nil?
|
148
|
+
properties_to_return = {}
|
149
|
+
type_definition.xpath('./Property').each do |property_xml|
|
150
|
+
property_name = property_xml.attributes['Name'].value
|
151
|
+
value_type = property_xml.attributes['Type'].value
|
152
|
+
property_options = {}
|
153
|
+
property_options[:allows_nil] = false if property_xml.attributes['Nullable'] == 'false'
|
154
|
+
klass_name = value_type.gsub(/^Edm\./, '')
|
155
|
+
properties_to_return[property_name] = "OData::Properties::#{klass_name}".constantize.new(property_name, nil, property_options)
|
156
|
+
end
|
157
|
+
properties_to_return
|
158
|
+
end
|
159
|
+
|
133
160
|
private
|
134
161
|
|
135
162
|
def default_options
|
@@ -151,41 +178,5 @@ module OData
|
|
151
178
|
::Nokogiri::XML(response.body).remove_namespaces!
|
152
179
|
}.call
|
153
180
|
end
|
154
|
-
|
155
|
-
def build_request_url(model, criteria)
|
156
|
-
request_url = "#{service_url}/#{model.odata_name}"
|
157
|
-
request_url += "(#{criteria[:key]})" if criteria[:key]
|
158
|
-
request_url
|
159
|
-
end
|
160
|
-
|
161
|
-
def parse_model_from_feed(model, entry)
|
162
|
-
attributes = {}
|
163
|
-
|
164
|
-
%w{title summary}.each do |attribute_name|
|
165
|
-
attributes[attribute_name.to_sym] = {
|
166
|
-
value: entry.xpath("//#{attribute_name}").first.content,
|
167
|
-
type: entry.xpath("//#{attribute_name}").first.attributes['type'].value
|
168
|
-
}
|
169
|
-
end
|
170
|
-
|
171
|
-
entry.xpath('//content/properties/*').each do |property|
|
172
|
-
if property.attributes['null']
|
173
|
-
if property.attributes['null'].value == 'true'
|
174
|
-
property_type = nil
|
175
|
-
else
|
176
|
-
property_type = property.attributes['type'].value
|
177
|
-
end
|
178
|
-
else
|
179
|
-
property_type = property.attributes['type'].value
|
180
|
-
end
|
181
|
-
|
182
|
-
attributes[property.name.underscore.to_sym] = {
|
183
|
-
value: property.content,
|
184
|
-
type: property_type
|
185
|
-
}
|
186
|
-
end
|
187
|
-
|
188
|
-
model.load_from_feed(attributes)
|
189
|
-
end
|
190
181
|
end
|
191
182
|
end
|
data/lib/odata/version.rb
CHANGED
data/odata.gemspec
CHANGED
@@ -24,6 +24,7 @@ Gem::Specification.new do |spec|
|
|
24
24
|
spec.add_development_dependency 'codeclimate-test-reporter'
|
25
25
|
spec.add_development_dependency 'rspec', '~> 3.0.0'
|
26
26
|
spec.add_development_dependency 'webmock', '~> 1.18.0'
|
27
|
+
spec.add_development_dependency 'timecop', '~> 0.7.1'
|
27
28
|
|
28
29
|
spec.add_dependency 'backports', '~> 3.6.0'
|
29
30
|
spec.add_dependency 'nokogiri', '~> 1.6.2'
|
@@ -0,0 +1,42 @@
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
2
|
+
<feed xml:base="http://services.odata.org/OData/OData.svc/" xmlns="http://www.w3.org/2005/Atom"
|
3
|
+
xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices"
|
4
|
+
xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
|
5
|
+
xmlns:georss="http://www.georss.org/georss" xmlns:gml="http://www.opengis.net/gml">
|
6
|
+
<id>http://services.odata.org/OData/OData.svc/Products</id>
|
7
|
+
<title type="text">Products</title>
|
8
|
+
<updated>2014-07-07T20:01:27Z</updated>
|
9
|
+
<link rel="self" title="Products" href="Products"/>
|
10
|
+
<entry>
|
11
|
+
<id>http://services.odata.org/OData/OData.svc/Products(0)</id>
|
12
|
+
<category term="ODataDemo.Product" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme"/>
|
13
|
+
<link rel="edit" title="Product" href="Products(0)"/>
|
14
|
+
<link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/Categories"
|
15
|
+
type="application/atom+xml;type=feed" title="Categories" href="Products(0)/Categories"/>
|
16
|
+
<link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/Supplier"
|
17
|
+
type="application/atom+xml;type=entry" title="Supplier" href="Products(0)/Supplier"/>
|
18
|
+
<link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/ProductDetail"
|
19
|
+
type="application/atom+xml;type=entry" title="ProductDetail" href="Products(0)/ProductDetail"/>
|
20
|
+
<title type="text">Bread</title>
|
21
|
+
<summary type="text">Whole grain bread</summary>
|
22
|
+
<updated>2014-07-07T20:01:27Z</updated>
|
23
|
+
<author>
|
24
|
+
<name/>
|
25
|
+
</author>
|
26
|
+
<link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/relatedlinks/Categories" type="application/xml"
|
27
|
+
title="Categories" href="Products(0)/$links/Categories"/>
|
28
|
+
<link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/relatedlinks/Supplier" type="application/xml"
|
29
|
+
title="Supplier" href="Products(0)/$links/Supplier"/>
|
30
|
+
<link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/relatedlinks/ProductDetail" type="application/xml"
|
31
|
+
title="ProductDetail" href="Products(0)/$links/ProductDetail"/>
|
32
|
+
<content type="application/xml">
|
33
|
+
<m:properties>
|
34
|
+
<d:ID m:type="Edm.Int32">0</d:ID>
|
35
|
+
<d:ReleaseDate m:type="Edm.DateTime">1992-01-01T00:00:00</d:ReleaseDate>
|
36
|
+
<d:DiscontinuedDate m:null="true"/>
|
37
|
+
<d:Rating m:type="Edm.Int16">4</d:Rating>
|
38
|
+
<d:Price m:type="Edm.Double">2.5</d:Price>
|
39
|
+
</m:properties>
|
40
|
+
</content>
|
41
|
+
</entry>
|
42
|
+
</feed>
|
@@ -0,0 +1,34 @@
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
2
|
+
<entry xml:base="http://services.odata.org/OData/OData.svc/"
|
3
|
+
xmlns="http://www.w3.org/2005/Atom" xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices"
|
4
|
+
xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
|
5
|
+
xmlns:georss="http://www.georss.org/georss" xmlns:gml="http://www.opengis.net/gml">
|
6
|
+
<id>http://services.odata.org/OData/OData.svc/Products(9999)</id>
|
7
|
+
<category term="ODataDemo.Product" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme"/>
|
8
|
+
<link rel="edit" title="Product" href="Products(9999)"/>
|
9
|
+
<link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/Categories" type="application/atom+xml;type=feed" title="Categories" href="Products(9999)/Categories"/>
|
10
|
+
<link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/Supplier" type="application/atom+xml;type=entry" title="Supplier" href="Products(9999)/Supplier"/>
|
11
|
+
<link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/ProductDetail" type="application/atom+xml;type=entry" title="ProductDetail" href="Products(9999)/ProductDetail"/>
|
12
|
+
<title type="text">Widget</title>
|
13
|
+
<summary type="text">A simple widget</summary>
|
14
|
+
<updated>2014-07-07T18:47:05Z</updated>
|
15
|
+
<author>
|
16
|
+
<name/>
|
17
|
+
</author>
|
18
|
+
<link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/relatedlinks/Categories" type="application/xml" title="Categories" href="Products(9999)/$links/Categories"/>
|
19
|
+
<link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/relatedlinks/Supplier" type="application/xml" title="Supplier" href="Products(9999)/$links/Supplier"/>
|
20
|
+
<link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/relatedlinks/ProductDetail" type="application/xml" title="ProductDetail" href="Products(9999)/$links/ProductDetail"/>
|
21
|
+
<m:action
|
22
|
+
metadata="http://services.odata.org/OData/OData.svc/$metadata#DemoService.Discount"
|
23
|
+
title="Discount"
|
24
|
+
target="http://services.odata.org/OData/OData.svc/Products(9999)/Discount"/>
|
25
|
+
<content type="application/xml">
|
26
|
+
<m:properties>
|
27
|
+
<d:ID m:type="Edm.Int32">9999</d:ID>
|
28
|
+
<d:ReleaseDate m:type="Edm.DateTime">2014-07-07T12:44:13</d:ReleaseDate>
|
29
|
+
<d:DiscontinuedDate m:null="true"/>
|
30
|
+
<d:Rating m:type="Edm.Int16">4</d:Rating>
|
31
|
+
<d:Price m:type="Edm.Double">3.5</d:Price>
|
32
|
+
</m:properties>
|
33
|
+
</content>
|
34
|
+
</entry>
|
@@ -11,8 +11,7 @@ describe OData::EntitySet do
|
|
11
11
|
OData::Service.open('http://services.odata.org/OData/OData.svc')
|
12
12
|
end
|
13
13
|
|
14
|
-
|
15
|
-
it { expect(subject).to respond_to(:name, :type, :container, :namespace) }
|
14
|
+
it { expect(subject).to respond_to(:name, :type, :container, :namespace, :new_entity) }
|
16
15
|
|
17
16
|
it { expect(subject.name).to eq('Products') }
|
18
17
|
it { expect(subject.container).to eq('DemoService') }
|
@@ -37,4 +36,65 @@ describe OData::EntitySet do
|
|
37
36
|
it { expect(subject).to respond_to(:count) }
|
38
37
|
it { expect(subject.count).to eq(11) }
|
39
38
|
end
|
39
|
+
|
40
|
+
describe '#new_entity' do
|
41
|
+
let(:new_entity) { subject.new_entity(properties) }
|
42
|
+
let(:release_date) { DateTime.new(2014,7,5) }
|
43
|
+
let(:properties) { {
|
44
|
+
Name: 'Widget',
|
45
|
+
Description: 'Just a simple widget',
|
46
|
+
ReleaseDate: release_date,
|
47
|
+
DiscontinuedDate: nil,
|
48
|
+
Rating: 4,
|
49
|
+
Price: 3.5
|
50
|
+
} }
|
51
|
+
|
52
|
+
it { expect(new_entity['ID']).to be_nil }
|
53
|
+
it { expect(new_entity['Name']).to eq('Widget') }
|
54
|
+
it { expect(new_entity['Description']).to eq('Just a simple widget') }
|
55
|
+
it { expect(new_entity['ReleaseDate']).to eq(release_date) }
|
56
|
+
it { expect(new_entity['DiscontinuedDate']).to be_nil }
|
57
|
+
it { expect(new_entity['Rating']).to eq(4) }
|
58
|
+
it { expect(new_entity['Price']).to eq(3.5) }
|
59
|
+
end
|
60
|
+
|
61
|
+
describe '#<<' do
|
62
|
+
let(:new_entity) { subject.new_entity(properties) }
|
63
|
+
let(:bad_entity) { subject.new_entity }
|
64
|
+
let(:existing_entity) { subject.first }
|
65
|
+
let(:properties) { {
|
66
|
+
Name: 'Widget',
|
67
|
+
Description: 'Just a simple widget',
|
68
|
+
ReleaseDate: DateTime.now.new_offset(0),
|
69
|
+
DiscontinuedDate: nil,
|
70
|
+
Rating: 4,
|
71
|
+
Price: 3.5
|
72
|
+
} }
|
73
|
+
|
74
|
+
it { expect(subject).to respond_to(:<<) }
|
75
|
+
|
76
|
+
it 'with an existing entity' do
|
77
|
+
WebMock.stub_request(:post, 'http://services.odata.org/OData/OData.svc/Products(0)').
|
78
|
+
to_return(status: 200, body: File.open('spec/fixtures/sample_service/product_0.xml'))
|
79
|
+
|
80
|
+
expect {subject << existing_entity}.to_not raise_error
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'with a new entity' do
|
84
|
+
WebMock.stub_request(:post, 'http://services.odata.org/OData/OData.svc/Products').
|
85
|
+
to_return(status: 201, body: File.open('spec/fixtures/sample_service/product_9999.xml'))
|
86
|
+
|
87
|
+
expect(new_entity['ID']).to be_nil
|
88
|
+
expect {subject << new_entity}.to_not raise_error
|
89
|
+
expect(new_entity['ID']).to_not be_nil
|
90
|
+
expect(new_entity['ID']).to eq(9999)
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'with a bad entity' do
|
94
|
+
WebMock.stub_request(:post, 'http://services.odata.org/OData/OData.svc/Products').
|
95
|
+
to_return(status: 400, body: nil)
|
96
|
+
|
97
|
+
expect {subject << bad_entity}.to raise_error(StandardError, 'Something went wrong committing your entity')
|
98
|
+
end
|
99
|
+
end
|
40
100
|
end
|
data/spec/odata/entity_spec.rb
CHANGED
@@ -39,5 +39,8 @@ describe OData::Entity do
|
|
39
39
|
it { expect(subject['DiscontinuedDate']).to be_nil }
|
40
40
|
it { expect(subject['Rating']).to eq(4) }
|
41
41
|
it { expect(subject['Price']).to eq(2.5) }
|
42
|
+
|
43
|
+
it { expect {subject['NonExistant']}.to raise_error(ArgumentError) }
|
44
|
+
it { expect {subject['NonExistant'] = 5}.to raise_error(ArgumentError) }
|
42
45
|
end
|
43
46
|
end
|
data/spec/odata/service_spec.rb
CHANGED
@@ -59,27 +59,6 @@ describe OData::Service do
|
|
59
59
|
it { expect(subject.namespace).to eq('ODataDemo') }
|
60
60
|
end
|
61
61
|
|
62
|
-
describe '#get' do
|
63
|
-
describe 'a set of entities' do
|
64
|
-
it { expect(subject.get(::Examples::Product).size).to eq(11) }
|
65
|
-
it { expect(subject.get(::Examples::Product).first).to be_instance_of(::Examples::Product) }
|
66
|
-
it { expect(subject.get(::Examples::Product).shuffle.first).to be_instance_of(::Examples::Product) }
|
67
|
-
it { expect(subject.get(::Examples::Product).last).to be_instance_of(::Examples::Product) }
|
68
|
-
end
|
69
|
-
|
70
|
-
describe 'a specific entity by key' do
|
71
|
-
it { expect(subject.get(::Examples::Product, {key: 0}).size).to eq(1) }
|
72
|
-
it { expect(subject.get(::Examples::Product, {key: 0}).first).to be_instance_of(::Examples::Product) }
|
73
|
-
it { expect(subject.get(::Examples::Product, {key: 0}).first.id).to eq(0) }
|
74
|
-
it { expect(subject.get(::Examples::Product, {key: 0}).first.name).to eq('Bread') }
|
75
|
-
it { expect(subject.get(::Examples::Product, {key: 0}).first.description).to eq('Whole grain bread') }
|
76
|
-
it { expect(subject.get(::Examples::Product, {key: 0}).first.release_date).to eq(DateTime.parse('1992-01-01T00:00:00')) }
|
77
|
-
it { expect(subject.get(::Examples::Product, {key: 0}).first.discontinued_date).to eq(nil) }
|
78
|
-
it { expect(subject.get(::Examples::Product, {key: 0}).first.rating).to eq(4) }
|
79
|
-
it { expect(subject.get(::Examples::Product, {key: 0}).first.price).to eq(2.5) }
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
62
|
describe '#[]' do
|
84
63
|
it { expect(subject['Products']).to be_a(OData::EntitySet) }
|
85
64
|
it { expect {subject['Nonexistant']}.to raise_error(ArgumentError) }
|
data/spec/spec_helper.rb
CHANGED
@@ -17,6 +17,8 @@ else
|
|
17
17
|
WebMock.disable_net_connect!(allow_localhost: true, allow: 'codeclimate.com')
|
18
18
|
end
|
19
19
|
|
20
|
+
require 'timecop'
|
21
|
+
|
20
22
|
RSpec.configure do |config|
|
21
23
|
if config.files_to_run.one?
|
22
24
|
config.default_formatter = 'doc'
|
@@ -52,6 +54,9 @@ RSpec.configure do |config|
|
|
52
54
|
WebMock.stub_request(:get, 'http://services.odata.org/OData/OData.svc/Products?$inlinecount=allpages&$skip=10&$top=5').
|
53
55
|
to_return(status: 200, body: File.open('spec/fixtures/sample_service/products_skip10_top5.xml'))
|
54
56
|
|
57
|
+
WebMock.stub_request(:get, 'http://services.odata.org/OData/OData.svc/Products?$skip=0&$top=1').
|
58
|
+
to_return(status: 200, body: File.open('spec/fixtures/sample_service/first_product.xml'))
|
59
|
+
|
55
60
|
WebMock.stub_request(:get, 'http://services.odata.org/OData/OData.svc/Products/$count').
|
56
61
|
to_return(status: 200, :body => '11')
|
57
62
|
|
@@ -64,5 +69,6 @@ RSpec.configure do |config|
|
|
64
69
|
# We're calling this as a private method because there should not be any
|
65
70
|
# reasons to have to flush the service registry except in testing.
|
66
71
|
OData::ServiceRegistry.instance.send(:flush)
|
72
|
+
WebMock.reset!
|
67
73
|
end
|
68
74
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: odata
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- James Thompson
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-07-
|
11
|
+
date: 2014-07-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -94,6 +94,20 @@ dependencies:
|
|
94
94
|
- - "~>"
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: 1.18.0
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: timecop
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 0.7.1
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 0.7.1
|
97
111
|
- !ruby/object:Gem::Dependency
|
98
112
|
name: backports
|
99
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -161,6 +175,7 @@ files:
|
|
161
175
|
- ".rspec"
|
162
176
|
- ".ruby-version"
|
163
177
|
- ".travis.yml"
|
178
|
+
- CHANGELOG.md
|
164
179
|
- Gemfile
|
165
180
|
- LICENSE.txt
|
166
181
|
- README.md
|
@@ -168,7 +183,6 @@ files:
|
|
168
183
|
- lib/odata.rb
|
169
184
|
- lib/odata/entity.rb
|
170
185
|
- lib/odata/entity_set.rb
|
171
|
-
- lib/odata/model.rb
|
172
186
|
- lib/odata/properties.rb
|
173
187
|
- lib/odata/properties/binary.rb
|
174
188
|
- lib/odata/properties/boolean.rb
|
@@ -187,16 +201,16 @@ files:
|
|
187
201
|
- lib/odata/service_registry.rb
|
188
202
|
- lib/odata/version.rb
|
189
203
|
- odata.gemspec
|
190
|
-
- spec/
|
204
|
+
- spec/fixtures/sample_service/first_product.xml
|
191
205
|
- spec/fixtures/sample_service/metadata.xml
|
192
206
|
- spec/fixtures/sample_service/product_0.xml
|
207
|
+
- spec/fixtures/sample_service/product_9999.xml
|
193
208
|
- spec/fixtures/sample_service/products.xml
|
194
209
|
- spec/fixtures/sample_service/products_skip0_top5.xml
|
195
210
|
- spec/fixtures/sample_service/products_skip10_top5.xml
|
196
211
|
- spec/fixtures/sample_service/products_skip5_top5.xml
|
197
212
|
- spec/odata/entity_set_spec.rb
|
198
213
|
- spec/odata/entity_spec.rb
|
199
|
-
- spec/odata/model_spec.rb
|
200
214
|
- spec/odata/properties/binary_spec.rb
|
201
215
|
- spec/odata/properties/boolean_spec.rb
|
202
216
|
- spec/odata/properties/date_time_offset_spec.rb
|
@@ -236,16 +250,16 @@ signing_key:
|
|
236
250
|
specification_version: 4
|
237
251
|
summary: Simple OData library
|
238
252
|
test_files:
|
239
|
-
- spec/
|
253
|
+
- spec/fixtures/sample_service/first_product.xml
|
240
254
|
- spec/fixtures/sample_service/metadata.xml
|
241
255
|
- spec/fixtures/sample_service/product_0.xml
|
256
|
+
- spec/fixtures/sample_service/product_9999.xml
|
242
257
|
- spec/fixtures/sample_service/products.xml
|
243
258
|
- spec/fixtures/sample_service/products_skip0_top5.xml
|
244
259
|
- spec/fixtures/sample_service/products_skip10_top5.xml
|
245
260
|
- spec/fixtures/sample_service/products_skip5_top5.xml
|
246
261
|
- spec/odata/entity_set_spec.rb
|
247
262
|
- spec/odata/entity_spec.rb
|
248
|
-
- spec/odata/model_spec.rb
|
249
263
|
- spec/odata/properties/binary_spec.rb
|
250
264
|
- spec/odata/properties/boolean_spec.rb
|
251
265
|
- spec/odata/properties/date_time_offset_spec.rb
|
data/lib/odata/model.rb
DELETED
@@ -1,116 +0,0 @@
|
|
1
|
-
require 'odata/property'
|
2
|
-
require 'odata/properties'
|
3
|
-
|
4
|
-
module OData
|
5
|
-
# Provides a convenient way to represent OData entities as Ruby objects.
|
6
|
-
# Instances that include this module gain the ability to define how to map
|
7
|
-
# OData properties to attributes as well as the ability to query and persist
|
8
|
-
# data to the underlying OData service.
|
9
|
-
#
|
10
|
-
# If the module detects the presence of Rails, along with ActiveModel::Model
|
11
|
-
# it will automatically mixin support ActiveModel::Model to provide better
|
12
|
-
# integration with Rails apps.
|
13
|
-
module Model
|
14
|
-
extend ::ActiveSupport::Concern
|
15
|
-
include ::ActiveModel::Model if defined?(::Rails) && defined?(::ActiveModel::Model)
|
16
|
-
|
17
|
-
included do
|
18
|
-
cattr_accessor :registered_properties
|
19
|
-
cattr_accessor :primary_key
|
20
|
-
|
21
|
-
self.class_eval <<-EOS
|
22
|
-
@@registered_properties = {}
|
23
|
-
@@primary_key = nil
|
24
|
-
EOS
|
25
|
-
end
|
26
|
-
|
27
|
-
# Converts supplied value based on its type
|
28
|
-
#
|
29
|
-
# @param value [*] usually a string representation of the value
|
30
|
-
# @param type [String] OData type name as string
|
31
|
-
# @return [*]
|
32
|
-
def convert_value_for_type(value, type)
|
33
|
-
return nil if value.nil? || type.nil?
|
34
|
-
if type =~ /Int(16|32|64)$/
|
35
|
-
value.to_i
|
36
|
-
elsif type =~ /Boolean$/
|
37
|
-
(value == 'true' || value == '1')
|
38
|
-
elsif type =~ /(Decimal|Double|Single)$/
|
39
|
-
value.to_f
|
40
|
-
elsif type =~ /DateTime$/
|
41
|
-
DateTime.parse(value)
|
42
|
-
else
|
43
|
-
value.to_s
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
module ClassMethods
|
48
|
-
# Returns the class' name
|
49
|
-
def model_name
|
50
|
-
self.to_s.demodulize
|
51
|
-
end
|
52
|
-
|
53
|
-
# Pluralizes #model_name for OData requests
|
54
|
-
def odata_name
|
55
|
-
model_name.pluralize
|
56
|
-
end
|
57
|
-
|
58
|
-
# Instantiates an OData::Model instance from a hash of attributes and
|
59
|
-
# their details.
|
60
|
-
#
|
61
|
-
# @param feed_hash [Hash] attribute names as keys, and details as values
|
62
|
-
# @return [OData::Model]
|
63
|
-
def load_from_feed(feed_hash)
|
64
|
-
loaded_instance = self.new
|
65
|
-
feed_hash.each do |attribute_name, details|
|
66
|
-
loaded_instance.instance_eval do
|
67
|
-
@properties ||= {}
|
68
|
-
@properties[attribute_name] = convert_value_for_type(details[:value], details[:type])
|
69
|
-
end
|
70
|
-
end
|
71
|
-
loaded_instance
|
72
|
-
end
|
73
|
-
|
74
|
-
# Define a property and it's options
|
75
|
-
#
|
76
|
-
# @param name [to_s] the literal property name expected by the OData service
|
77
|
-
# @param options [Hash] options for setting up the property
|
78
|
-
def property(name, options = {})
|
79
|
-
register_property(name.to_s.underscore, {literal_name: name}.merge(options))
|
80
|
-
create_accessors(name.to_s.underscore, options)
|
81
|
-
register_primary_key(name.to_s.underscore) if options[:primary_key]
|
82
|
-
end
|
83
|
-
|
84
|
-
private
|
85
|
-
|
86
|
-
def register_property(name, options)
|
87
|
-
new_registered_properties = registered_properties
|
88
|
-
new_registered_properties[name.to_sym] = options
|
89
|
-
registered_properties = new_registered_properties
|
90
|
-
end
|
91
|
-
|
92
|
-
def create_accessors(name, options)
|
93
|
-
self.class_eval do
|
94
|
-
if options[:entity_title]
|
95
|
-
define_method(name) { @properties[:title] || nil }
|
96
|
-
define_method("#{name}=") {|value| @properties[:title] = value}
|
97
|
-
elsif options[:entity_summary]
|
98
|
-
define_method(name) { @properties[:summary] || nil }
|
99
|
-
define_method("#{name}=") {|value| @properties[:summary] = value}
|
100
|
-
else
|
101
|
-
define_method(name) { @properties[name.to_sym] || nil }
|
102
|
-
define_method("#{name}=") {|value| @properties[name.to_sym] = value}
|
103
|
-
end
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
def register_primary_key(name)
|
108
|
-
if primary_key.nil?
|
109
|
-
self.class_eval <<-EOS
|
110
|
-
@@primary_key = :#{name}
|
111
|
-
EOS
|
112
|
-
end
|
113
|
-
end
|
114
|
-
end
|
115
|
-
end
|
116
|
-
end
|
@@ -1,13 +0,0 @@
|
|
1
|
-
module Examples
|
2
|
-
class Product
|
3
|
-
include OData::Model
|
4
|
-
|
5
|
-
property 'ID', primary_key: true
|
6
|
-
property 'Name', entity_title: true
|
7
|
-
property 'Description', entity_summary: true
|
8
|
-
property 'ReleaseDate'
|
9
|
-
property 'DiscontinuedDate'
|
10
|
-
property 'Rating'
|
11
|
-
property 'Price'
|
12
|
-
end
|
13
|
-
end
|
data/spec/odata/model_spec.rb
DELETED
@@ -1,25 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
require 'examples/product_model'
|
3
|
-
|
4
|
-
describe OData::Model do
|
5
|
-
describe 'class methods' do
|
6
|
-
it { expect(::Examples::Product).to respond_to(:model_name) }
|
7
|
-
it { expect(::Examples::Product.model_name).to eq('Product') }
|
8
|
-
it { expect(::Examples::Product.odata_name).to eq('Products') }
|
9
|
-
end
|
10
|
-
|
11
|
-
describe 'defining properties' do
|
12
|
-
let(:subject) { ::Examples::Product.new }
|
13
|
-
|
14
|
-
it { expect(subject).to respond_to(:id) }
|
15
|
-
it { expect(subject).to respond_to(:name) }
|
16
|
-
it { expect(subject).to respond_to(:description) }
|
17
|
-
it { expect(subject).to respond_to(:release_date) }
|
18
|
-
it { expect(subject).to respond_to(:discontinued_date) }
|
19
|
-
it { expect(subject).to respond_to(:rating) }
|
20
|
-
it { expect(subject).to respond_to(:price) }
|
21
|
-
|
22
|
-
it { expect(::Examples::Product).to respond_to(:primary_key) }
|
23
|
-
it { expect(::Examples::Product.primary_key).to eq(:id)}
|
24
|
-
end
|
25
|
-
end
|