frodata 0.9.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.autotest +2 -0
- data/.gitignore +24 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +75 -0
- data/CHANGELOG.md +150 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +23 -0
- data/README.md +427 -0
- data/Rakefile +7 -0
- data/TODO.md +55 -0
- data/frodata.gemspec +34 -0
- data/lib/frodata.rb +36 -0
- data/lib/frodata/entity.rb +332 -0
- data/lib/frodata/entity_container.rb +75 -0
- data/lib/frodata/entity_set.rb +161 -0
- data/lib/frodata/errors.rb +68 -0
- data/lib/frodata/navigation_property.rb +29 -0
- data/lib/frodata/navigation_property/proxy.rb +80 -0
- data/lib/frodata/properties.rb +32 -0
- data/lib/frodata/properties/binary.rb +50 -0
- data/lib/frodata/properties/boolean.rb +37 -0
- data/lib/frodata/properties/collection.rb +50 -0
- data/lib/frodata/properties/complex.rb +114 -0
- data/lib/frodata/properties/date.rb +27 -0
- data/lib/frodata/properties/date_time.rb +83 -0
- data/lib/frodata/properties/date_time_offset.rb +17 -0
- data/lib/frodata/properties/decimal.rb +50 -0
- data/lib/frodata/properties/enum.rb +62 -0
- data/lib/frodata/properties/float.rb +67 -0
- data/lib/frodata/properties/geography.rb +13 -0
- data/lib/frodata/properties/geography/base.rb +162 -0
- data/lib/frodata/properties/geography/line_string.rb +33 -0
- data/lib/frodata/properties/geography/point.rb +31 -0
- data/lib/frodata/properties/geography/polygon.rb +38 -0
- data/lib/frodata/properties/guid.rb +17 -0
- data/lib/frodata/properties/integer.rb +107 -0
- data/lib/frodata/properties/number.rb +14 -0
- data/lib/frodata/properties/string.rb +72 -0
- data/lib/frodata/properties/time.rb +40 -0
- data/lib/frodata/properties/time_of_day.rb +27 -0
- data/lib/frodata/property.rb +139 -0
- data/lib/frodata/property_registry.rb +41 -0
- data/lib/frodata/query.rb +233 -0
- data/lib/frodata/query/criteria.rb +92 -0
- data/lib/frodata/query/criteria/comparison_operators.rb +49 -0
- data/lib/frodata/query/criteria/date_functions.rb +61 -0
- data/lib/frodata/query/criteria/geography_functions.rb +21 -0
- data/lib/frodata/query/criteria/lambda_operators.rb +27 -0
- data/lib/frodata/query/criteria/string_functions.rb +40 -0
- data/lib/frodata/query/in_batches.rb +58 -0
- data/lib/frodata/railtie.rb +19 -0
- data/lib/frodata/schema.rb +155 -0
- data/lib/frodata/schema/complex_type.rb +79 -0
- data/lib/frodata/schema/enum_type.rb +95 -0
- data/lib/frodata/service.rb +254 -0
- data/lib/frodata/service/request.rb +85 -0
- data/lib/frodata/service/response.rb +162 -0
- data/lib/frodata/service/response/atom.rb +40 -0
- data/lib/frodata/service/response/json.rb +41 -0
- data/lib/frodata/service/response/plain.rb +36 -0
- data/lib/frodata/service/response/xml.rb +40 -0
- data/lib/frodata/service_registry.rb +52 -0
- data/lib/frodata/version.rb +3 -0
- data/spec/fixtures/files/entity_to_xml.xml +17 -0
- data/spec/fixtures/files/error.xml +5 -0
- data/spec/fixtures/files/metadata.xml +150 -0
- data/spec/fixtures/files/product_0.json +10 -0
- data/spec/fixtures/files/product_0.xml +28 -0
- data/spec/fixtures/files/products.json +106 -0
- data/spec/fixtures/files/products.xml +308 -0
- data/spec/fixtures/files/supplier_0.json +26 -0
- data/spec/fixtures/files/supplier_0.xml +32 -0
- data/spec/fixtures/vcr_cassettes/entity_set_specs.yml +1635 -0
- data/spec/fixtures/vcr_cassettes/entity_set_specs/bad_entry.yml +183 -0
- data/spec/fixtures/vcr_cassettes/entity_set_specs/existing_entry.yml +256 -0
- data/spec/fixtures/vcr_cassettes/entity_set_specs/new_entry.yml +185 -0
- data/spec/fixtures/vcr_cassettes/entity_specs.yml +285 -0
- data/spec/fixtures/vcr_cassettes/navigation_property_proxy_specs.yml +346 -0
- data/spec/fixtures/vcr_cassettes/query/result_specs.yml +189 -0
- data/spec/fixtures/vcr_cassettes/query_specs.yml +1060 -0
- data/spec/fixtures/vcr_cassettes/schema/complex_type_specs.yml +127 -0
- data/spec/fixtures/vcr_cassettes/service/request_specs.yml +193 -0
- data/spec/fixtures/vcr_cassettes/service_registry_specs.yml +129 -0
- data/spec/fixtures/vcr_cassettes/service_specs.yml +127 -0
- data/spec/fixtures/vcr_cassettes/usage_example_specs.yml +1330 -0
- data/spec/frodata/entity/shared_examples.rb +82 -0
- data/spec/frodata/entity_container_spec.rb +38 -0
- data/spec/frodata/entity_set_spec.rb +168 -0
- data/spec/frodata/entity_spec.rb +151 -0
- data/spec/frodata/errors_spec.rb +48 -0
- data/spec/frodata/navigation_property/proxy_spec.rb +44 -0
- data/spec/frodata/navigation_property_spec.rb +55 -0
- data/spec/frodata/properties/binary_spec.rb +50 -0
- data/spec/frodata/properties/boolean_spec.rb +72 -0
- data/spec/frodata/properties/collection_spec.rb +44 -0
- data/spec/frodata/properties/date_spec.rb +23 -0
- data/spec/frodata/properties/date_time_offset_spec.rb +30 -0
- data/spec/frodata/properties/date_time_spec.rb +23 -0
- data/spec/frodata/properties/decimal_spec.rb +51 -0
- data/spec/frodata/properties/float_spec.rb +45 -0
- data/spec/frodata/properties/geography/line_string_spec.rb +33 -0
- data/spec/frodata/properties/geography/point_spec.rb +29 -0
- data/spec/frodata/properties/geography/polygon_spec.rb +55 -0
- data/spec/frodata/properties/geography/shared_examples.rb +72 -0
- data/spec/frodata/properties/guid_spec.rb +17 -0
- data/spec/frodata/properties/integer_spec.rb +58 -0
- data/spec/frodata/properties/string_spec.rb +46 -0
- data/spec/frodata/properties/time_of_day_spec.rb +23 -0
- data/spec/frodata/properties/time_spec.rb +15 -0
- data/spec/frodata/property_registry_spec.rb +16 -0
- data/spec/frodata/property_spec.rb +71 -0
- data/spec/frodata/query/criteria_spec.rb +229 -0
- data/spec/frodata/query_spec.rb +199 -0
- data/spec/frodata/schema/complex_type_spec.rb +96 -0
- data/spec/frodata/schema/enum_type_spec.rb +112 -0
- data/spec/frodata/schema_spec.rb +97 -0
- data/spec/frodata/service/request_spec.rb +49 -0
- data/spec/frodata/service/response_spec.rb +85 -0
- data/spec/frodata/service_registry_spec.rb +18 -0
- data/spec/frodata/service_spec.rb +191 -0
- data/spec/frodata/usage_example_spec.rb +188 -0
- data/spec/spec_helper.rb +32 -0
- data/spec/support/coverage.rb +2 -0
- data/spec/support/vcr.rb +9 -0
- metadata +401 -0
@@ -0,0 +1,79 @@
|
|
1
|
+
module FrOData
|
2
|
+
class Schema
|
3
|
+
# ComplexTypes are used in FrOData to either encapsulate richer data types for
|
4
|
+
# use as Entity properties. ComplexTypes are composed of properties the same
|
5
|
+
# way that Entities are and, so, the interface for working with the various
|
6
|
+
# properties of a ComplexType mimics that of Entities.
|
7
|
+
class ComplexType
|
8
|
+
# Creates a new ComplexType based on the supplied options.
|
9
|
+
# @param type_xml [Nokogiri::XML::Element]
|
10
|
+
# @param service [FrOData::Service]
|
11
|
+
def initialize(type_definition, schema)
|
12
|
+
@type_definition = type_definition
|
13
|
+
@schema = schema
|
14
|
+
end
|
15
|
+
|
16
|
+
# The name of the ComplexType
|
17
|
+
# @return [String]
|
18
|
+
def name
|
19
|
+
@name ||= type_definition.attributes['Name'].value
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns the namespaced type for the ComplexType.
|
23
|
+
# @return [String]
|
24
|
+
def type
|
25
|
+
"#{namespace}.#{name}"
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns the namespace this ComplexType belongs to.
|
29
|
+
# @return [String]
|
30
|
+
def namespace
|
31
|
+
@namespace ||= service.namespace
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns this ComplexType's properties.
|
35
|
+
# @return [Hash<String, FrOData::Property>]
|
36
|
+
def properties
|
37
|
+
@properties ||= collect_properties
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns a list of this ComplexType's property names.
|
41
|
+
# @return [Array<String>]
|
42
|
+
def property_names
|
43
|
+
@property_names ||= properties.keys
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns the property class that implements this `ComplexType`.
|
47
|
+
# @return [Class < FrOData::Properties::Complex]
|
48
|
+
def property_class
|
49
|
+
@property_class ||= lambda { |type, complex_type|
|
50
|
+
klass = Class.new ::FrOData::Properties::Complex
|
51
|
+
klass.send(:define_method, :type) { type }
|
52
|
+
klass.send(:define_method, :complex_type) { complex_type }
|
53
|
+
klass
|
54
|
+
}.call(type, self)
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def schema
|
60
|
+
@schema
|
61
|
+
end
|
62
|
+
|
63
|
+
def service
|
64
|
+
@schema.service
|
65
|
+
end
|
66
|
+
|
67
|
+
def type_definition
|
68
|
+
@type_definition
|
69
|
+
end
|
70
|
+
|
71
|
+
def collect_properties
|
72
|
+
Hash[type_definition.xpath('./Property').map do |property_xml|
|
73
|
+
property_name, property = schema.send(:process_property_from_xml, property_xml)
|
74
|
+
[property_name, property]
|
75
|
+
end]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module FrOData
|
2
|
+
class Schema
|
3
|
+
# Enumeration types are nominal types that represent a series of related values.
|
4
|
+
# Enumeration types expose these related values as members of the enumeration.
|
5
|
+
class EnumType
|
6
|
+
# Creates a new EnumType based on the supplied options.
|
7
|
+
# @param type_xml [Nokogiri::XML::Element]
|
8
|
+
# @param service [FrOData::Service]
|
9
|
+
# @return [self]
|
10
|
+
def initialize(type_definition, schema)
|
11
|
+
@type_definition = type_definition
|
12
|
+
@schema = schema
|
13
|
+
end
|
14
|
+
|
15
|
+
# The name of the EnumType
|
16
|
+
# @return [String]
|
17
|
+
def name
|
18
|
+
options['Name']
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns the namespaced type for the EnumType.
|
22
|
+
# @return [String]
|
23
|
+
def type
|
24
|
+
"#{namespace}.#{name}"
|
25
|
+
end
|
26
|
+
|
27
|
+
# Whether this EnumType supports setting multiple values.
|
28
|
+
# @return [Boolean]
|
29
|
+
def is_flags?
|
30
|
+
options['IsFlags'] == 'true'
|
31
|
+
end
|
32
|
+
|
33
|
+
# The underlying type of this EnumType.
|
34
|
+
# @return [String]
|
35
|
+
def underlying_type
|
36
|
+
options['UnderlyingType'] || 'Edm.Int32'
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns the namespace this EnumType belongs to.
|
40
|
+
# @return [String]
|
41
|
+
def namespace
|
42
|
+
@namespace ||= service.namespace
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns the members of this EnumType and their values.
|
46
|
+
# @return [Hash]
|
47
|
+
def members
|
48
|
+
@members ||= collect_members
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns the property class that implements this `EnumType`.
|
52
|
+
# @return [Class < FrOData::Properties::Enum]
|
53
|
+
def property_class
|
54
|
+
@property_class ||= lambda { |type, members, is_flags|
|
55
|
+
klass = Class.new ::FrOData::Properties::Enum
|
56
|
+
klass.send(:define_method, :type) { type }
|
57
|
+
klass.send(:define_method, :members) { members }
|
58
|
+
klass.send(:define_method, :is_flags?) { is_flags }
|
59
|
+
klass
|
60
|
+
}.call(type, members, is_flags?)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns the value of the requested member.
|
64
|
+
# @param member_name [to_s]
|
65
|
+
# @return [*]
|
66
|
+
def [](member_name)
|
67
|
+
members.invert[member_name.to_s]
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def service
|
73
|
+
@schema.service
|
74
|
+
end
|
75
|
+
|
76
|
+
def type_definition
|
77
|
+
@type_definition
|
78
|
+
end
|
79
|
+
|
80
|
+
def options
|
81
|
+
@options = type_definition.attributes.map do |name, attr|
|
82
|
+
[name, attr.value]
|
83
|
+
end.to_h
|
84
|
+
end
|
85
|
+
|
86
|
+
def collect_members
|
87
|
+
Hash[type_definition.xpath('./Member').map.with_index do |member_xml, index|
|
88
|
+
member_name = member_xml.attributes['Name'].value
|
89
|
+
member_value = member_xml.attributes['Value'].andand.value.andand.to_i
|
90
|
+
[member_value || index, member_name]
|
91
|
+
end]
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,254 @@
|
|
1
|
+
require 'frodata/service/request'
|
2
|
+
require 'frodata/service/response'
|
3
|
+
|
4
|
+
module FrOData
|
5
|
+
# Encapsulates the basic details and functionality needed to interact with an
|
6
|
+
# FrOData service.
|
7
|
+
class Service
|
8
|
+
# The Faraday connection object used by the service to make requests
|
9
|
+
attr_reader :connection
|
10
|
+
# The FrOData Service's URL
|
11
|
+
attr_reader :service_url
|
12
|
+
# Service options
|
13
|
+
attr_reader :options
|
14
|
+
|
15
|
+
DEFAULT_TIMEOUT = 20
|
16
|
+
|
17
|
+
METADATA_TIMEOUTS = [20, 60]
|
18
|
+
|
19
|
+
MIME_TYPES = {
|
20
|
+
atom: 'application/atom+xml',
|
21
|
+
json: 'application/json',
|
22
|
+
xml: 'application/xml',
|
23
|
+
plain: 'text/plain'
|
24
|
+
}
|
25
|
+
|
26
|
+
# Opens the service based on the requested URL and adds the service to
|
27
|
+
# {FrOData::Registry}
|
28
|
+
#
|
29
|
+
# @param service_url [String|Faraday::Connection]
|
30
|
+
# The URL to the desired FrOData service, or a Faraday connection object
|
31
|
+
# @param options [Hash] options to pass to the service
|
32
|
+
# @return [FrOData::Service] an instance of the service
|
33
|
+
def initialize(service_url, options = {}, &block)
|
34
|
+
if service_url.is_a? Faraday::Connection
|
35
|
+
@connection = service_url
|
36
|
+
@service_url = connection.url_prefix
|
37
|
+
else
|
38
|
+
@service_url = service_url
|
39
|
+
@connection = Faraday.new(service_url, options[:connection], &block)
|
40
|
+
end
|
41
|
+
@options = default_options.merge(options)
|
42
|
+
FrOData::ServiceRegistry.add(self)
|
43
|
+
register_custom_types
|
44
|
+
end
|
45
|
+
|
46
|
+
# Opens the service based on the requested URL and adds the service to
|
47
|
+
# {FrOData::Registry}
|
48
|
+
# @deprecated Use {Service.new} instead.
|
49
|
+
#
|
50
|
+
# @param service_url [String] the URL to the desired FrOData service
|
51
|
+
# @param options [Hash] options to pass to the service
|
52
|
+
# @return [FrOData::Service] an instance of the service
|
53
|
+
def self.open(service_url, options = {}, &block)
|
54
|
+
Service.new(service_url, options, &block)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns user supplied name for service, or its URL
|
58
|
+
# @return [String]
|
59
|
+
def name
|
60
|
+
@name ||= options[:name] || service_url
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns the service's metadata URL.
|
64
|
+
# @return [String]
|
65
|
+
def metadata_url
|
66
|
+
"#{service_url}/$metadata"
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns the service's metadata definition.
|
70
|
+
# @return [Nokogiri::XML]
|
71
|
+
def metadata
|
72
|
+
@metadata ||= lambda { read_metadata }.call
|
73
|
+
end
|
74
|
+
|
75
|
+
# Returns all of the service's schemas.
|
76
|
+
# @return Hash<String, FrOData::Schema>
|
77
|
+
def schemas
|
78
|
+
@schemas ||= metadata.xpath('//Schema').map do |schema_xml|
|
79
|
+
[
|
80
|
+
schema_xml.attributes['Namespace'].value,
|
81
|
+
Schema.new(schema_xml, self)
|
82
|
+
]
|
83
|
+
end.to_h
|
84
|
+
end
|
85
|
+
|
86
|
+
# Returns the service's EntityContainer (singleton)
|
87
|
+
# @return FrOData::EntityContainer
|
88
|
+
def entity_container
|
89
|
+
@entity_container ||= EntityContainer.new(self)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Returns a hash of EntitySet names and their respective EntityType names
|
93
|
+
# @return Hash<String, String>
|
94
|
+
def entity_sets
|
95
|
+
entity_container.entity_sets
|
96
|
+
end
|
97
|
+
|
98
|
+
# Retrieves the EntitySet associated with a specific EntityType by name
|
99
|
+
#
|
100
|
+
# @param entity_set_name [to_s] the name of the EntitySet desired
|
101
|
+
# @return [FrOData::EntitySet] an FrOData::EntitySet to query
|
102
|
+
def [](entity_set_name)
|
103
|
+
entity_container[entity_set_name]
|
104
|
+
end
|
105
|
+
|
106
|
+
# Returns the default namespace, that is, the namespace of the schema
|
107
|
+
# that contains the service's EntityContainer.
|
108
|
+
# @return [String]
|
109
|
+
def namespace
|
110
|
+
entity_container.namespace
|
111
|
+
end
|
112
|
+
|
113
|
+
# Returns a list of `EntityType`s exposed by the service
|
114
|
+
# @return Array<String>
|
115
|
+
def entity_types
|
116
|
+
@entity_types ||= schemas.map do |namespace, schema|
|
117
|
+
schema.entity_types.map do |entity_type|
|
118
|
+
"#{namespace}.#{entity_type}"
|
119
|
+
end
|
120
|
+
end.flatten
|
121
|
+
end
|
122
|
+
|
123
|
+
# Returns a list of `ComplexType`s used by the service.
|
124
|
+
# @return [Hash<String, FrOData::Schema::ComplexType>]
|
125
|
+
def complex_types
|
126
|
+
@complex_types ||= schemas.map do |namespace, schema|
|
127
|
+
schema.complex_types.map do |name, complex_type|
|
128
|
+
[ "#{namespace}.#{name}", complex_type ]
|
129
|
+
end.to_h
|
130
|
+
end.reduce({}, :merge)
|
131
|
+
end
|
132
|
+
|
133
|
+
# Returns a list of `EnumType`s used by the service
|
134
|
+
# @return [Hash<String, FrOData::Schema::EnumType>]
|
135
|
+
def enum_types
|
136
|
+
@enum_types ||= schemas.map do |namespace, schema|
|
137
|
+
schema.enum_types.map do |name, enum_type|
|
138
|
+
[ "#{namespace}.#{name}", enum_type ]
|
139
|
+
end.to_h
|
140
|
+
end.reduce({}, :merge)
|
141
|
+
end
|
142
|
+
|
143
|
+
# Returns a more compact inspection of the service object
|
144
|
+
def inspect
|
145
|
+
"#<#{self.class.name}:#{self.object_id} name='#{name}' service_url='#{self.service_url}'>"
|
146
|
+
end
|
147
|
+
|
148
|
+
# Execute a request against the service
|
149
|
+
#
|
150
|
+
# @param url_chunk [to_s] string to append to service URL
|
151
|
+
# @param options [Hash] additional request options
|
152
|
+
# @return [FrOData::Service::Response]
|
153
|
+
def execute(url_chunk, options = {})
|
154
|
+
Request.new(self, url_chunk, options).execute
|
155
|
+
end
|
156
|
+
|
157
|
+
# Get the property type for an entity from metadata.
|
158
|
+
#
|
159
|
+
# @param entity_name [to_s] the fully qualified entity name
|
160
|
+
# @param property_name [to_s] the property name needed
|
161
|
+
# @return [String] the name of the property's type
|
162
|
+
def get_property_type(entity_name, property_name)
|
163
|
+
namespace, _, entity_name = entity_name.rpartition('.')
|
164
|
+
raise ArgumentError, 'Namespace missing' if namespace.nil? || namespace.empty?
|
165
|
+
schemas[namespace].get_property_type(entity_name, property_name)
|
166
|
+
end
|
167
|
+
|
168
|
+
# Get the primary key for the supplied Entity.
|
169
|
+
#
|
170
|
+
# @param entity_name [to_s] The fully qualified entity name
|
171
|
+
# @return [String]
|
172
|
+
def primary_key_for(entity_name)
|
173
|
+
namespace, _, entity_name = entity_name.rpartition('.')
|
174
|
+
raise ArgumentError, 'Namespace missing' if namespace.nil? || namespace.empty?
|
175
|
+
schemas[namespace].primary_key_for(entity_name)
|
176
|
+
end
|
177
|
+
|
178
|
+
# Get the list of properties and their various options for the supplied
|
179
|
+
# Entity name.
|
180
|
+
# @param entity_name [to_s]
|
181
|
+
# @return [Hash]
|
182
|
+
# @api private
|
183
|
+
def properties_for_entity(entity_name)
|
184
|
+
namespace, _, entity_name = entity_name.rpartition('.')
|
185
|
+
raise ArgumentError, 'Namespace missing' if namespace.nil? || namespace.empty?
|
186
|
+
schemas[namespace].properties_for_entity(entity_name)
|
187
|
+
end
|
188
|
+
|
189
|
+
# Returns the logger instance used by the service.
|
190
|
+
# When Ruby on Rails has been detected, the service will
|
191
|
+
# use `Rails.logger`. The log level will NOT be changed.
|
192
|
+
#
|
193
|
+
# When no Rails has been detected, a default logger will
|
194
|
+
# be used that logs to STDOUT with the log level supplied
|
195
|
+
# via options, or the default log level if none was given.
|
196
|
+
# @return [Logger]
|
197
|
+
def logger
|
198
|
+
@logger ||= options[:logger] || if defined?(Rails)
|
199
|
+
Rails.logger
|
200
|
+
else
|
201
|
+
default_logger
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
# Allows the logger to be set to a custom `Logger` instance.
|
206
|
+
# @param custom_logger [Logger]
|
207
|
+
def logger=(custom_logger)
|
208
|
+
@logger = custom_logger
|
209
|
+
end
|
210
|
+
|
211
|
+
private
|
212
|
+
|
213
|
+
def default_options
|
214
|
+
{
|
215
|
+
request: {
|
216
|
+
timeout: DEFAULT_TIMEOUT
|
217
|
+
},
|
218
|
+
strict: true # strict property validation
|
219
|
+
}
|
220
|
+
end
|
221
|
+
|
222
|
+
def default_logger
|
223
|
+
Logger.new(STDOUT).tap do |logger|
|
224
|
+
logger.level = options[:log_level] || Logger::WARN
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def read_metadata
|
229
|
+
# From file, good for debugging
|
230
|
+
if options[:metadata_file]
|
231
|
+
data = File.read(options[:metadata_file])
|
232
|
+
::Nokogiri::XML(data).remove_namespaces!
|
233
|
+
else # From a URL
|
234
|
+
response = nil
|
235
|
+
METADATA_TIMEOUTS.each do |timeout|
|
236
|
+
response = execute(metadata_url, timeout: timeout)
|
237
|
+
break unless response.timed_out?
|
238
|
+
end
|
239
|
+
raise "Metadata Timeout" if response.timed_out?
|
240
|
+
::Nokogiri::XML(response.body).remove_namespaces!
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
def register_custom_types
|
245
|
+
complex_types.each do |name, type|
|
246
|
+
::FrOData::PropertyRegistry.add(name, type.property_class)
|
247
|
+
end
|
248
|
+
|
249
|
+
enum_types.each do |name, type|
|
250
|
+
::FrOData::PropertyRegistry.add(name, type.property_class)
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module FrOData
|
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 FrOData::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 [FrOData::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.delete(:method) || :get
|
22
|
+
@format = options.delete(:format) || :auto
|
23
|
+
@query = options.delete(:query)
|
24
|
+
@options = options
|
25
|
+
end
|
26
|
+
|
27
|
+
# Return the full request URL (including service base)
|
28
|
+
# @return [String]
|
29
|
+
def url
|
30
|
+
::URI.join("#{service.service_url}/", ::URI.escape(url_chunk)).to_s
|
31
|
+
end
|
32
|
+
|
33
|
+
# The content type for this request. Depends on format.
|
34
|
+
# @return [String]
|
35
|
+
def content_type
|
36
|
+
if format == :auto
|
37
|
+
MIME_TYPES.values.join(',')
|
38
|
+
elsif MIME_TYPES.has_key? format
|
39
|
+
MIME_TYPES[format]
|
40
|
+
else
|
41
|
+
raise ArgumentError, "Unknown format '#{format}'"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Execute the request
|
46
|
+
#
|
47
|
+
# @param additional_options [Hash] Request options to pass to Faraday
|
48
|
+
# @return [FrOData::Service::Response]
|
49
|
+
def execute(additional_options = {})
|
50
|
+
request_options = service.options[:request].merge(additional_options)
|
51
|
+
|
52
|
+
logger.info "Requesting #{method.to_s.upcase} #{url}..."
|
53
|
+
Response.new(service, query) do
|
54
|
+
connection.run_request(method, url_chunk, nil, headers) do |conn|
|
55
|
+
conn.options.merge! request_options
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
attr_reader :url_chunk
|
63
|
+
|
64
|
+
def connection
|
65
|
+
service.connection
|
66
|
+
end
|
67
|
+
|
68
|
+
def default_headers
|
69
|
+
{
|
70
|
+
'Accept' => content_type,
|
71
|
+
'Content-Type' => content_type,
|
72
|
+
'OData-Version' => '4.0'
|
73
|
+
}
|
74
|
+
end
|
75
|
+
|
76
|
+
def headers
|
77
|
+
default_headers.merge(@options[:headers] || {})
|
78
|
+
end
|
79
|
+
|
80
|
+
def logger
|
81
|
+
service.logger
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|