odata 0.0.6 → 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -0
- data/lib/odata/model.rb +51 -3
- data/lib/odata/service.rb +56 -1
- data/lib/odata/service_registry.rb +10 -0
- data/lib/odata/version.rb +1 -1
- data/lib/odata.rb +1 -0
- data/spec/examples/product_model.rb +2 -2
- data/spec/odata/service_spec.rb +18 -2
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1e5c0770f691489dde6cbba87e2414dd75a1aa88
|
4
|
+
data.tar.gz: cfe4aecd0925e4301b0d031ae3f8119d169af98f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0008e948c633b04d6d8609796bcd3262e53ccb8ed82a75754d1edca84e153d0c04371fc695eee7b2e97d77715ea0dbc432eeb52bb533f232e6bb0466d8cf66fc
|
7
|
+
data.tar.gz: 23a3a1ca3bbf5dc67fc79fb15ec48512fd89dc84d7bcfb6cf54c37f3704b03b9c62b8e40dec95de1a8156b050e8f9413e96eb091fd123d2d010613bd89e2731e
|
data/README.md
CHANGED
@@ -4,6 +4,7 @@
|
|
4
4
|
[![Code Climate](https://codeclimate.com/github/plainprogrammer/odata.png)](https://codeclimate.com/github/plainprogrammer/odata)
|
5
5
|
[![Coverage](https://codeclimate.com/github/plainprogrammer/odata/coverage.png)](https://codeclimate.com/github/plainprogrammer/odata)
|
6
6
|
[![Gem Version](https://badge.fury.io/rb/odata.svg)](http://badge.fury.io/rb/odata)
|
7
|
+
[![Documentation](http://inch-ci.org/github/plainprogrammer/odata.png?branch=master)](http://rubydoc.info/github/plainprogrammer/odata/master/frames)
|
7
8
|
|
8
9
|
The OData gem provides a simple wrapper around the OData API protocol. It has
|
9
10
|
the ability to automatically inspect compliant APIs and expose the relevant
|
data/lib/odata/model.rb
CHANGED
@@ -1,4 +1,12 @@
|
|
1
1
|
module OData
|
2
|
+
# Provides a convenient way to represent OData entities as Ruby objects.
|
3
|
+
# Instances that include this module gain the ability to define how to map
|
4
|
+
# OData properties to attributes as well as the ability to query and persist
|
5
|
+
# data to the underlying OData service.
|
6
|
+
#
|
7
|
+
# If the module detects the presence of Rails, along with ActiveModel::Model
|
8
|
+
# it will automatically mixin support ActiveModel::Model to provide better
|
9
|
+
# integration with Rails apps.
|
2
10
|
module Model
|
3
11
|
extend ::ActiveSupport::Concern
|
4
12
|
include ::ActiveModel::Model if defined?(::Rails) && defined?(::ActiveModel::Model)
|
@@ -13,17 +21,49 @@ module OData
|
|
13
21
|
EOS
|
14
22
|
end
|
15
23
|
|
24
|
+
def convert_value_for_type(value, type)
|
25
|
+
return nil if value.nil? || type.nil?
|
26
|
+
if type =~ /Int(16|32|64)$/
|
27
|
+
value.to_i
|
28
|
+
elsif type =~ /Boolean$/
|
29
|
+
(value == 'true' || value == '1')
|
30
|
+
elsif type =~ /(Decimal|Double|Single)$/
|
31
|
+
value.to_f
|
32
|
+
elsif type =~ /DateTime$/
|
33
|
+
DateTime.parse(value)
|
34
|
+
else
|
35
|
+
value.to_s
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
16
39
|
module ClassMethods
|
40
|
+
# Returns the class' name
|
17
41
|
def model_name
|
18
42
|
self.to_s.demodulize
|
19
43
|
end
|
20
44
|
|
45
|
+
# Pluralizes #model_name for OData requests
|
21
46
|
def odata_name
|
22
47
|
model_name.pluralize
|
23
48
|
end
|
24
49
|
|
50
|
+
def load_from_feed(feed_hash)
|
51
|
+
loaded_instance = self.new
|
52
|
+
feed_hash.each do |attribute_name, details|
|
53
|
+
loaded_instance.instance_eval do
|
54
|
+
@properties ||= {}
|
55
|
+
@properties[attribute_name] = convert_value_for_type(details[:value], details[:type])
|
56
|
+
end
|
57
|
+
end
|
58
|
+
loaded_instance
|
59
|
+
end
|
60
|
+
|
61
|
+
# Define a property and it's options
|
62
|
+
#
|
63
|
+
# @param name [to_s] the literal property name expected by the OData service
|
64
|
+
# @param options [Hash] options for setting up the property
|
25
65
|
def property(name, options = {})
|
26
|
-
register_property(name.to_s.underscore,
|
66
|
+
register_property(name.to_s.underscore, {literal_name: name}.merge(options))
|
27
67
|
create_accessors(name.to_s.underscore, options)
|
28
68
|
register_primary_key(name.to_s.underscore) if options[:primary_key]
|
29
69
|
end
|
@@ -38,8 +78,16 @@ module OData
|
|
38
78
|
|
39
79
|
def create_accessors(name, options)
|
40
80
|
self.class_eval do
|
41
|
-
|
42
|
-
|
81
|
+
if options[:entity_title]
|
82
|
+
define_method(name) { @properties[:title] || nil }
|
83
|
+
define_method("#{name}=") {|value| @properties[:title] = value}
|
84
|
+
elsif options[:entity_summary]
|
85
|
+
define_method(name) { @properties[:summary] || nil }
|
86
|
+
define_method("#{name}=") {|value| @properties[:summary] = value}
|
87
|
+
else
|
88
|
+
define_method(name) { @properties[name.to_sym] || nil }
|
89
|
+
define_method("#{name}=") {|value| @properties[name.to_sym] = value}
|
90
|
+
end
|
43
91
|
end
|
44
92
|
end
|
45
93
|
|
data/lib/odata/service.rb
CHANGED
@@ -1,33 +1,55 @@
|
|
1
1
|
module OData
|
2
|
+
# Encapsulates the basic details and functionality needed to interact with an
|
3
|
+
# OData service.
|
2
4
|
class Service
|
5
|
+
# The OData Service's URL
|
3
6
|
attr_reader :service_url
|
4
7
|
|
8
|
+
# Opens the service based on the requested URL and adds the service to
|
9
|
+
# {OData::Registry}
|
10
|
+
#
|
11
|
+
# @param service_url [String] the URL to the desired OData service
|
12
|
+
# @return [OData::Service] an instance of the service
|
5
13
|
def initialize(service_url)
|
6
14
|
@service_url = service_url
|
7
15
|
OData::ServiceRegistry.add(self)
|
8
16
|
self
|
9
17
|
end
|
10
18
|
|
19
|
+
# Opens the service based on the requested URL and adds the service to
|
20
|
+
# {OData::Registry}
|
21
|
+
#
|
22
|
+
# @param service_url [String] the URL to the desired OData service
|
23
|
+
# @return [OData::Service] an instance of the service
|
11
24
|
def self.open(service_url)
|
12
25
|
Service.new(service_url)
|
13
26
|
end
|
14
27
|
|
28
|
+
# Returns a list of entities exposed by the service
|
15
29
|
def entities
|
16
30
|
@entities ||= metadata.xpath('//EntityType').collect {|entity| entity.attributes['Name'].value}
|
17
31
|
end
|
18
32
|
|
33
|
+
# Returns a list of ComplexTypes used by the service
|
19
34
|
def complex_types
|
20
35
|
@complex_types ||= metadata.xpath('//ComplexType').collect {|entity| entity.attributes['Name'].value}
|
21
36
|
end
|
22
37
|
|
38
|
+
# Returns the namespace defined on the service's schema
|
23
39
|
def namespace
|
24
40
|
@namespace ||= metadata.xpath('//Schema').first.attributes['Namespace'].value
|
25
41
|
end
|
26
42
|
|
43
|
+
# Returns a more compact inspection of the service object
|
27
44
|
def inspect
|
28
45
|
"#<#{self.class.name}:#{self.object_id} namespace='#{self.namespace}' service_url='#{self.service_url}'>"
|
29
46
|
end
|
30
47
|
|
48
|
+
# Handles getting OData resources from the service.
|
49
|
+
#
|
50
|
+
# @param model [OData::Model] the type of resource being requested
|
51
|
+
# @param criteria [Hash] any criteria to narrow the request
|
52
|
+
# @return [Array] instances of the requested model
|
31
53
|
def get(model, criteria = {})
|
32
54
|
request = ::Typhoeus::Request.new(
|
33
55
|
build_request_url(model, criteria),
|
@@ -36,7 +58,7 @@ module OData
|
|
36
58
|
request.run
|
37
59
|
response = request.response
|
38
60
|
feed = ::Nokogiri::XML(response.body).remove_namespaces!
|
39
|
-
feed.xpath('//entry')
|
61
|
+
feed.xpath('//entry').collect {|entry| parse_model_from_feed(model, entry)}
|
40
62
|
end
|
41
63
|
|
42
64
|
private
|
@@ -58,5 +80,38 @@ module OData
|
|
58
80
|
request_url += "(#{criteria[:key]})" if criteria[:key]
|
59
81
|
request_url
|
60
82
|
end
|
83
|
+
|
84
|
+
def parse_model_from_feed(model, entry)
|
85
|
+
attributes = {}
|
86
|
+
|
87
|
+
attributes[:title] = {
|
88
|
+
value: entry.xpath('//title').first.content,
|
89
|
+
type: entry.xpath('//title').first.attributes['type'].value
|
90
|
+
}
|
91
|
+
|
92
|
+
attributes[:summary] = {
|
93
|
+
value: entry.xpath('//summary').first.content,
|
94
|
+
type: entry.xpath('//summary').first.attributes['type'].value
|
95
|
+
}
|
96
|
+
|
97
|
+
entry.xpath('//content/properties/*').each do |property|
|
98
|
+
if property.attributes['null']
|
99
|
+
if property.attributes['null'].value == 'true'
|
100
|
+
property_type = nil
|
101
|
+
else
|
102
|
+
property_type = property.attributes['type'].value
|
103
|
+
end
|
104
|
+
else
|
105
|
+
property_type = property.attributes['type'].value
|
106
|
+
end
|
107
|
+
|
108
|
+
attributes[property.name.underscore.to_sym] = {
|
109
|
+
value: property.content,
|
110
|
+
type: property_type
|
111
|
+
}
|
112
|
+
end
|
113
|
+
|
114
|
+
model.load_from_feed(attributes)
|
115
|
+
end
|
61
116
|
end
|
62
117
|
end
|
@@ -1,9 +1,13 @@
|
|
1
1
|
require 'singleton'
|
2
2
|
|
3
3
|
module OData
|
4
|
+
# Provides a registry for keeping track of multiple OData::Service instances
|
4
5
|
class ServiceRegistry
|
5
6
|
include Singleton
|
6
7
|
|
8
|
+
# Add a service to the Registry
|
9
|
+
#
|
10
|
+
# @param service [OData::Service] service to add to the registry
|
7
11
|
def add(service)
|
8
12
|
initialize_instance_variables
|
9
13
|
@services << service if service.is_a?(OData::Service) && !@services.include?(service)
|
@@ -11,16 +15,22 @@ module OData
|
|
11
15
|
@services_by_url[service.service_url] = @services.find_index(service)
|
12
16
|
end
|
13
17
|
|
18
|
+
# Lookup a service by URL or namespace
|
19
|
+
#
|
20
|
+
# @param lookup_key [String] the URL or namespace to lookup
|
21
|
+
# @return [OData::Service, nil] the OData::Service or nil
|
14
22
|
def [](lookup_key)
|
15
23
|
initialize_instance_variables
|
16
24
|
index = @services_by_namespace[lookup_key] || @services_by_url[lookup_key]
|
17
25
|
index.nil? ? nil : @services[index]
|
18
26
|
end
|
19
27
|
|
28
|
+
# (see #add)
|
20
29
|
def self.add(service)
|
21
30
|
OData::ServiceRegistry.instance.add(service)
|
22
31
|
end
|
23
32
|
|
33
|
+
# (see #[])
|
24
34
|
def self.[](lookup_key)
|
25
35
|
OData::ServiceRegistry.instance[lookup_key]
|
26
36
|
end
|
data/lib/odata/version.rb
CHANGED
data/lib/odata.rb
CHANGED
@@ -3,8 +3,8 @@ module Examples
|
|
3
3
|
include OData::Model
|
4
4
|
|
5
5
|
property 'ID', primary_key: true
|
6
|
-
property 'Name'
|
7
|
-
property 'Description'
|
6
|
+
property 'Name', entity_title: true
|
7
|
+
property 'Description', entity_summary: true
|
8
8
|
property 'ReleaseDate'
|
9
9
|
property 'DiscontinuedDate'
|
10
10
|
property 'Rating'
|
data/spec/odata/service_spec.rb
CHANGED
@@ -51,7 +51,23 @@ describe OData::Service do
|
|
51
51
|
end
|
52
52
|
|
53
53
|
describe '#get' do
|
54
|
-
|
55
|
-
|
54
|
+
describe 'a set of entities' do
|
55
|
+
it { expect(subject.get(::Examples::Product).size).to eq(11) }
|
56
|
+
it { expect(subject.get(::Examples::Product).first).to be_instance_of(::Examples::Product) }
|
57
|
+
it { expect(subject.get(::Examples::Product).shuffle.first).to be_instance_of(::Examples::Product) }
|
58
|
+
it { expect(subject.get(::Examples::Product).last).to be_instance_of(::Examples::Product) }
|
59
|
+
end
|
60
|
+
|
61
|
+
describe 'a specific entity by key' do
|
62
|
+
it { expect(subject.get(::Examples::Product, {key: 0}).size).to eq(1) }
|
63
|
+
it { expect(subject.get(::Examples::Product, {key: 0}).first).to be_instance_of(::Examples::Product) }
|
64
|
+
it { expect(subject.get(::Examples::Product, {key: 0}).first.id).to eq(0) }
|
65
|
+
it { expect(subject.get(::Examples::Product, {key: 0}).first.name).to eq('Bread') }
|
66
|
+
it { expect(subject.get(::Examples::Product, {key: 0}).first.description).to eq('Whole grain bread') }
|
67
|
+
it { expect(subject.get(::Examples::Product, {key: 0}).first.release_date).to eq(DateTime.parse('1992-01-01T00:00:00')) }
|
68
|
+
it { expect(subject.get(::Examples::Product, {key: 0}).first.discontinued_date).to eq(nil) }
|
69
|
+
it { expect(subject.get(::Examples::Product, {key: 0}).first.rating).to eq(4) }
|
70
|
+
it { expect(subject.get(::Examples::Product, {key: 0}).first.price).to eq(2.5) }
|
71
|
+
end
|
56
72
|
end
|
57
73
|
end
|