frodo 0.10.0
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 +7 -0
- data/.autotest +2 -0
- data/.circleci/config.yml +54 -0
- data/.gitignore +24 -0
- data/.gitlab-ci.yml +9 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +75 -0
- data/CHANGELOG.md +163 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +23 -0
- data/README.md +479 -0
- data/Rakefile +7 -0
- data/TODO.md +55 -0
- data/frodo.gemspec +39 -0
- data/images/frodo.jpg +0 -0
- data/lib/frodo/abstract_client.rb +11 -0
- data/lib/frodo/client.rb +6 -0
- data/lib/frodo/concerns/api.rb +292 -0
- data/lib/frodo/concerns/authentication.rb +32 -0
- data/lib/frodo/concerns/base.rb +84 -0
- data/lib/frodo/concerns/caching.rb +26 -0
- data/lib/frodo/concerns/connection.rb +79 -0
- data/lib/frodo/concerns/verbs.rb +68 -0
- data/lib/frodo/config.rb +143 -0
- data/lib/frodo/entity.rb +335 -0
- data/lib/frodo/entity_container.rb +75 -0
- data/lib/frodo/entity_set.rb +131 -0
- data/lib/frodo/errors.rb +70 -0
- data/lib/frodo/middleware/authentication/token.rb +13 -0
- data/lib/frodo/middleware/authentication.rb +87 -0
- data/lib/frodo/middleware/authorization.rb +18 -0
- data/lib/frodo/middleware/caching.rb +30 -0
- data/lib/frodo/middleware/custom_headers.rb +14 -0
- data/lib/frodo/middleware/gzip.rb +33 -0
- data/lib/frodo/middleware/instance_url.rb +20 -0
- data/lib/frodo/middleware/logger.rb +42 -0
- data/lib/frodo/middleware/multipart.rb +64 -0
- data/lib/frodo/middleware/odata_headers.rb +13 -0
- data/lib/frodo/middleware/raise_error.rb +47 -0
- data/lib/frodo/middleware.rb +33 -0
- data/lib/frodo/navigation_property/proxy.rb +80 -0
- data/lib/frodo/navigation_property.rb +29 -0
- data/lib/frodo/properties/binary.rb +50 -0
- data/lib/frodo/properties/boolean.rb +37 -0
- data/lib/frodo/properties/collection.rb +50 -0
- data/lib/frodo/properties/complex.rb +114 -0
- data/lib/frodo/properties/date.rb +27 -0
- data/lib/frodo/properties/date_time.rb +83 -0
- data/lib/frodo/properties/date_time_offset.rb +17 -0
- data/lib/frodo/properties/decimal.rb +54 -0
- data/lib/frodo/properties/enum.rb +62 -0
- data/lib/frodo/properties/float.rb +67 -0
- data/lib/frodo/properties/geography/base.rb +162 -0
- data/lib/frodo/properties/geography/line_string.rb +33 -0
- data/lib/frodo/properties/geography/point.rb +31 -0
- data/lib/frodo/properties/geography/polygon.rb +38 -0
- data/lib/frodo/properties/geography.rb +13 -0
- data/lib/frodo/properties/guid.rb +17 -0
- data/lib/frodo/properties/integer.rb +107 -0
- data/lib/frodo/properties/number.rb +14 -0
- data/lib/frodo/properties/string.rb +72 -0
- data/lib/frodo/properties/time.rb +40 -0
- data/lib/frodo/properties/time_of_day.rb +27 -0
- data/lib/frodo/properties.rb +32 -0
- data/lib/frodo/property.rb +139 -0
- data/lib/frodo/property_registry.rb +41 -0
- data/lib/frodo/query/criteria/comparison_operators.rb +49 -0
- data/lib/frodo/query/criteria/date_functions.rb +61 -0
- data/lib/frodo/query/criteria/geography_functions.rb +21 -0
- data/lib/frodo/query/criteria/lambda_operators.rb +27 -0
- data/lib/frodo/query/criteria/string_functions.rb +40 -0
- data/lib/frodo/query/criteria.rb +92 -0
- data/lib/frodo/query/in_batches.rb +58 -0
- data/lib/frodo/query.rb +221 -0
- data/lib/frodo/railtie.rb +19 -0
- data/lib/frodo/schema/complex_type.rb +79 -0
- data/lib/frodo/schema/enum_type.rb +95 -0
- data/lib/frodo/schema.rb +164 -0
- data/lib/frodo/service.rb +199 -0
- data/lib/frodo/service_registry.rb +52 -0
- data/lib/frodo/version.rb +3 -0
- data/lib/frodo.rb +67 -0
- data/spec/fixtures/auth_success_response.json +11 -0
- data/spec/fixtures/error.json +11 -0
- data/spec/fixtures/files/entity_to_xml.xml +18 -0
- data/spec/fixtures/files/error.xml +5 -0
- data/spec/fixtures/files/metadata.xml +150 -0
- data/spec/fixtures/files/metadata_with_error.xml +157 -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/leads.json +923 -0
- data/spec/fixtures/refresh_error_response.json +8 -0
- data/spec/frodo/abstract_client_spec.rb +13 -0
- data/spec/frodo/client_spec.rb +57 -0
- data/spec/frodo/concerns/authentication_spec.rb +79 -0
- data/spec/frodo/concerns/base_spec.rb +68 -0
- data/spec/frodo/concerns/caching_spec.rb +40 -0
- data/spec/frodo/concerns/connection_spec.rb +65 -0
- data/spec/frodo/config_spec.rb +127 -0
- data/spec/frodo/entity/shared_examples.rb +83 -0
- data/spec/frodo/entity_container_spec.rb +38 -0
- data/spec/frodo/entity_set_spec.rb +169 -0
- data/spec/frodo/entity_spec.rb +153 -0
- data/spec/frodo/errors_spec.rb +48 -0
- data/spec/frodo/middleware/authentication/token_spec.rb +87 -0
- data/spec/frodo/middleware/authentication_spec.rb +83 -0
- data/spec/frodo/middleware/authorization_spec.rb +17 -0
- data/spec/frodo/middleware/custom_headers_spec.rb +21 -0
- data/spec/frodo/middleware/gzip_spec.rb +68 -0
- data/spec/frodo/middleware/instance_url_spec.rb +27 -0
- data/spec/frodo/middleware/logger_spec.rb +21 -0
- data/spec/frodo/middleware/odata_headers_spec.rb +15 -0
- data/spec/frodo/middleware/raise_error_spec.rb +66 -0
- data/spec/frodo/navigation_property/proxy_spec.rb +46 -0
- data/spec/frodo/navigation_property_spec.rb +55 -0
- data/spec/frodo/properties/binary_spec.rb +50 -0
- data/spec/frodo/properties/boolean_spec.rb +72 -0
- data/spec/frodo/properties/collection_spec.rb +44 -0
- data/spec/frodo/properties/date_spec.rb +23 -0
- data/spec/frodo/properties/date_time_offset_spec.rb +30 -0
- data/spec/frodo/properties/date_time_spec.rb +23 -0
- data/spec/frodo/properties/decimal_spec.rb +50 -0
- data/spec/frodo/properties/float_spec.rb +45 -0
- data/spec/frodo/properties/geography/line_string_spec.rb +33 -0
- data/spec/frodo/properties/geography/point_spec.rb +29 -0
- data/spec/frodo/properties/geography/polygon_spec.rb +55 -0
- data/spec/frodo/properties/geography/shared_examples.rb +72 -0
- data/spec/frodo/properties/guid_spec.rb +17 -0
- data/spec/frodo/properties/integer_spec.rb +58 -0
- data/spec/frodo/properties/string_spec.rb +46 -0
- data/spec/frodo/properties/time_of_day_spec.rb +23 -0
- data/spec/frodo/properties/time_spec.rb +15 -0
- data/spec/frodo/property_registry_spec.rb +16 -0
- data/spec/frodo/property_spec.rb +71 -0
- data/spec/frodo/query/criteria_spec.rb +229 -0
- data/spec/frodo/query_spec.rb +156 -0
- data/spec/frodo/schema/complex_type_spec.rb +97 -0
- data/spec/frodo/schema/enum_type_spec.rb +112 -0
- data/spec/frodo/schema_spec.rb +113 -0
- data/spec/frodo/service_registry_spec.rb +19 -0
- data/spec/frodo/service_spec.rb +153 -0
- data/spec/frodo/usage_example_spec.rb +161 -0
- data/spec/spec_helper.rb +35 -0
- data/spec/support/coverage.rb +2 -0
- data/spec/support/fixture_helpers.rb +14 -0
- data/spec/support/middleware.rb +19 -0
- metadata +479 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'erb'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'frodo/concerns/verbs'
|
|
6
|
+
|
|
7
|
+
module Frodo
|
|
8
|
+
module Concerns
|
|
9
|
+
module API
|
|
10
|
+
extend Frodo::Concerns::Verbs
|
|
11
|
+
|
|
12
|
+
# Public: Helper methods for performing arbitrary actions against the API using
|
|
13
|
+
# various HTTP verbs.
|
|
14
|
+
#
|
|
15
|
+
# Examples
|
|
16
|
+
#
|
|
17
|
+
# # Perform a get request
|
|
18
|
+
# client.get '/api/data/v9.1/leads'
|
|
19
|
+
# client.api_get 'leads'
|
|
20
|
+
#
|
|
21
|
+
# # Perform a post request
|
|
22
|
+
# client.post '/api/data/v9.1/leads', { ... }
|
|
23
|
+
# client.api_post 'leads', { ... }
|
|
24
|
+
#
|
|
25
|
+
# # Perform a put request
|
|
26
|
+
# client.put '/api/data/v9.1/leads(073ca9c8-2a41-e911-a81d-000d3a1d5a0b)', { ... }
|
|
27
|
+
# client.api_put 'leads(073ca9c8-2a41-e911-a81d-000d3a1d5a0b)', { ... }
|
|
28
|
+
#
|
|
29
|
+
# # Perform a delete request
|
|
30
|
+
# client.delete '/api/data/v9.1/leads(073ca9c8-2a41-e911-a81d-000d3a1d5a0b)'
|
|
31
|
+
# client.api_delete 'leads(073ca9c8-2a41-e911-a81d-000d3a1d5a0b)'
|
|
32
|
+
#
|
|
33
|
+
# Returns the Faraday::Response.
|
|
34
|
+
define_verbs :get, :post, :put, :delete, :patch, :head
|
|
35
|
+
|
|
36
|
+
# Public: Return the metadata XML schema for the service
|
|
37
|
+
#
|
|
38
|
+
# Returns [String]
|
|
39
|
+
def metadata
|
|
40
|
+
api_get("$metadata").body
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Public: Execute a query and returns the result.
|
|
44
|
+
#
|
|
45
|
+
# Query can be the url_chunk per the OData V4 spec or
|
|
46
|
+
# a Frodo::Query. The latter being preferred
|
|
47
|
+
#
|
|
48
|
+
# Examples
|
|
49
|
+
#
|
|
50
|
+
# # Find the names of all Accounts
|
|
51
|
+
# client.query("leads?$filter=firstname eq 'yo'")
|
|
52
|
+
#
|
|
53
|
+
# or
|
|
54
|
+
#
|
|
55
|
+
# query = client.service['leads'].query
|
|
56
|
+
# query.where("firstname eq 'yo'")
|
|
57
|
+
# client.query(query)
|
|
58
|
+
#
|
|
59
|
+
# Returns a list of Frodo::Entity
|
|
60
|
+
def query(query)
|
|
61
|
+
url_chunk, entity_set = if query.is_a?(Frodo::Query)
|
|
62
|
+
[query.to_s, query.entity_set.name]
|
|
63
|
+
else
|
|
64
|
+
[query]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
body = api_get(url_chunk).body
|
|
68
|
+
|
|
69
|
+
# if manual query as a string we detect the set on the response
|
|
70
|
+
entity_set = body['@odata.context'].split('#')[-1] if entity_set.nil?
|
|
71
|
+
build_entity(entity_set, body)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Public: Insert a new record.
|
|
75
|
+
#
|
|
76
|
+
# entity_set - The set the entity belongs to
|
|
77
|
+
# attrs - Hash of attributes to set on the new record.
|
|
78
|
+
#
|
|
79
|
+
# Examples
|
|
80
|
+
#
|
|
81
|
+
# # Add a new lead
|
|
82
|
+
# client.create('leads', {"firstname" =>'Bob'})
|
|
83
|
+
# # => '073ca9c8-2a41-e911-a81d-000d3a1d5a0b'
|
|
84
|
+
#
|
|
85
|
+
# Returns the primary key value of the newly created entity.
|
|
86
|
+
# Returns false if something bad happens.
|
|
87
|
+
def create(*args)
|
|
88
|
+
create!(*args)
|
|
89
|
+
rescue *exceptions
|
|
90
|
+
false
|
|
91
|
+
end
|
|
92
|
+
alias insert create
|
|
93
|
+
|
|
94
|
+
# Public: Insert a new record.
|
|
95
|
+
#
|
|
96
|
+
# entity_set - The set the entity belongs to
|
|
97
|
+
# attrs - Hash of attributes to set on the new record.
|
|
98
|
+
#
|
|
99
|
+
# Examples
|
|
100
|
+
#
|
|
101
|
+
# # Add a new lead
|
|
102
|
+
# client.create!('leads', {"firstname" =>'Bob'})
|
|
103
|
+
# # => '073ca9c8-2a41-e911-a81d-000d3a1d5a0b'
|
|
104
|
+
#
|
|
105
|
+
# Returns the primary key value of the newly created entity.
|
|
106
|
+
# Raises exceptions if an error is returned from Dynamics.
|
|
107
|
+
def create!(entity_set, attrs)
|
|
108
|
+
entity = service[entity_set].new_entity(attrs)
|
|
109
|
+
url_chunk = to_url_chunk(entity)
|
|
110
|
+
url = api_post(url_chunk, attrs).headers['odata-entityid']
|
|
111
|
+
id = url.match(/\(.+\)/)[0]
|
|
112
|
+
end
|
|
113
|
+
alias insert! create!
|
|
114
|
+
|
|
115
|
+
# Public: Update a record.
|
|
116
|
+
#
|
|
117
|
+
# entity_set - The set the entity belongs to
|
|
118
|
+
# attrs - Hash of attributes to set on the record.
|
|
119
|
+
#
|
|
120
|
+
# Examples
|
|
121
|
+
#
|
|
122
|
+
# # Update the lead with id '073ca9c8-2a41-e911-a81d-000d3a1d5a0b'
|
|
123
|
+
# client.update('leads', "leadid": '073ca9c8-2a41-e911-a81d-000d3a1d5a0b', Name: 'Whizbang Corp')
|
|
124
|
+
#
|
|
125
|
+
# Returns true if the entity was successfully updated.
|
|
126
|
+
# Returns false if there was an error.
|
|
127
|
+
def update(*args)
|
|
128
|
+
update!(*args)
|
|
129
|
+
rescue *exceptions
|
|
130
|
+
false
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Public: Update a record.
|
|
134
|
+
#
|
|
135
|
+
# entity_set - The set the entity belongs to
|
|
136
|
+
# attrs - Hash of attributes to set on the record.
|
|
137
|
+
#
|
|
138
|
+
# Examples
|
|
139
|
+
#
|
|
140
|
+
# # Update the leads with id '073ca9c8-2a41-e911-a81d-000d3a1d5a0b'
|
|
141
|
+
# client.update!('leads', 'leadid' => '073ca9c8-2a41-e911-a81d-000d3a1d5a0b', "firstname" => 'Whizbang Corp')
|
|
142
|
+
#
|
|
143
|
+
# Returns true if the entity was successfully updated.
|
|
144
|
+
# Raises an exception if an error is returned from Dynamics.
|
|
145
|
+
def update!(entity_set, attrs)
|
|
146
|
+
entity = service[entity_set].new_entity(attrs)
|
|
147
|
+
url_chunk = to_url_chunk(entity)
|
|
148
|
+
|
|
149
|
+
raise ArgumentError, 'ID field missing from provided attributes' if entity.is_new?
|
|
150
|
+
|
|
151
|
+
api_patch url_chunk, attrs
|
|
152
|
+
true
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Public: Delete a record.
|
|
156
|
+
#
|
|
157
|
+
# entity_set - The set the entity belongs to
|
|
158
|
+
# id - The Dynamics primary key ID of the record.
|
|
159
|
+
#
|
|
160
|
+
# Examples
|
|
161
|
+
#
|
|
162
|
+
# # Delete the lead with id "073ca9c8-2a41-e911-a81d-000d3a1d5a0b"
|
|
163
|
+
# client.destroy('leads', "073ca9c8-2a41-e911-a81d-000d3a1d5a0b")
|
|
164
|
+
#
|
|
165
|
+
# Returns true if the entity was successfully deleted.
|
|
166
|
+
# Returns false if an error is returned from Dynamics.
|
|
167
|
+
def destroy(*args)
|
|
168
|
+
destroy!(*args)
|
|
169
|
+
rescue *exceptions
|
|
170
|
+
false
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Public: Delete a record.
|
|
174
|
+
#
|
|
175
|
+
# entity_set - The set the entity belongs to
|
|
176
|
+
# id - The Dynamics primary key ID of the record.
|
|
177
|
+
#
|
|
178
|
+
# Examples
|
|
179
|
+
#
|
|
180
|
+
# # Delete the lead with id "073ca9c8-2a41-e911-a81d-000d3a1d5a0b"
|
|
181
|
+
# client.destroy!('leads', "073ca9c8-2a41-e911-a81d-000d3a1d5a0b")
|
|
182
|
+
#
|
|
183
|
+
# Returns true of the entity was successfully deleted.
|
|
184
|
+
# Raises an exception if an error is returned from Dynamics.
|
|
185
|
+
def destroy!(entity_set, id)
|
|
186
|
+
query = service[entity_set].query
|
|
187
|
+
url_chunk = query.find(id).to_s
|
|
188
|
+
api_delete url_chunk
|
|
189
|
+
true
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Public: Finds a single record and returns all fields.
|
|
193
|
+
#
|
|
194
|
+
# entity_set - The set the entity belongs to
|
|
195
|
+
# id - The id of the record. If field is specified, id should be the id
|
|
196
|
+
# of the external field.
|
|
197
|
+
#
|
|
198
|
+
# Returns the Entity record.
|
|
199
|
+
def find(entity_set, id)
|
|
200
|
+
query = service[entity_set].query
|
|
201
|
+
url_chunk = query.find(id)
|
|
202
|
+
|
|
203
|
+
body = api_get(url_chunk).body
|
|
204
|
+
build_entity(entity_set, body)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Public: Finds a single record and returns select fields.
|
|
208
|
+
#
|
|
209
|
+
# entity_set - The set the entity belongs to
|
|
210
|
+
# id - The id of the record. If field is specified, id should be the id
|
|
211
|
+
# of the external field.
|
|
212
|
+
# fields - A String array denoting the fields to select. If nil or empty array
|
|
213
|
+
# is passed, all fields are selected.
|
|
214
|
+
def select(entity_set, id, fields)
|
|
215
|
+
query = service[entity_set].query
|
|
216
|
+
|
|
217
|
+
fields.each{|field| query.select(field)}
|
|
218
|
+
url_chunk = query.find(id)
|
|
219
|
+
|
|
220
|
+
body = api_get(url_chunk).body
|
|
221
|
+
build_entity(entity_set, body)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Public: Count the entity set or for the query passed
|
|
225
|
+
#
|
|
226
|
+
# entity_set or query - A String or a Frodo::Query. If String is passed,
|
|
227
|
+
# all entities for the set are counted.
|
|
228
|
+
def count(query)
|
|
229
|
+
url_chunk = if query.is_a?(Frodo::Query)
|
|
230
|
+
query.include_count
|
|
231
|
+
query.to_s
|
|
232
|
+
else
|
|
233
|
+
service[query].query.count
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
body = api_get(url_chunk).body
|
|
237
|
+
|
|
238
|
+
if query.is_a?(Frodo::Query)
|
|
239
|
+
body['@odata.count']
|
|
240
|
+
else
|
|
241
|
+
# Some servers (*cough* Microsoft *cough*) seem to return
|
|
242
|
+
# extraneous characters in the response.
|
|
243
|
+
# I found out that the _\xef\xbb\xbf contains probably invisible junk characters
|
|
244
|
+
# called the Unicode BOM (short name for: byte order mark).
|
|
245
|
+
body.scan(/\d+/).first.to_i
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
private
|
|
250
|
+
|
|
251
|
+
# Internal: Returns a path to an api endpoint based on configured client
|
|
252
|
+
#
|
|
253
|
+
# Examples
|
|
254
|
+
#
|
|
255
|
+
# api_path('leads')
|
|
256
|
+
# # => '/leads'
|
|
257
|
+
def api_path(path)
|
|
258
|
+
"#{options[:base_path]}/#{path}" || "/#{path}"
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def build_entity(entity_set, data)
|
|
262
|
+
entity_options = service[entity_set].entity_options
|
|
263
|
+
single_entity?(data) ? parse_entity(data, entity_options) : parse_entities(data, entity_options)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def single_entity?(body)
|
|
267
|
+
body['@odata.context'] =~ /\$entity$/
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def parse_entity(entity_json, entity_options)
|
|
271
|
+
Frodo::Entity.from_json(entity_json, entity_options)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def parse_entities(body, entity_options)
|
|
275
|
+
body['value'].map do |entity_data|
|
|
276
|
+
Frodo::Entity.from_json(entity_data, entity_options)
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def to_url_chunk(entity)
|
|
281
|
+
primary_key = entity.get_property(entity.primary_key).url_value
|
|
282
|
+
set = entity.entity_set.name
|
|
283
|
+
entity.is_new? ? set : "#{set}(#{primary_key})"
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Internal: Errors that should be rescued from in non-bang methods
|
|
287
|
+
def exceptions
|
|
288
|
+
[Faraday::Error::ClientError]
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Frodo
|
|
4
|
+
module Concerns
|
|
5
|
+
module Authentication
|
|
6
|
+
# Public: Force an authentication
|
|
7
|
+
def authenticate!
|
|
8
|
+
unless authentication_middleware
|
|
9
|
+
raise AuthenticationError, 'No authentication middleware present'
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
middleware = authentication_middleware.new nil, self, options
|
|
13
|
+
middleware.authenticate!
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Internal: Determines what middleware will be used based on the options provided
|
|
17
|
+
def authentication_middleware
|
|
18
|
+
if oauth_refresh?
|
|
19
|
+
Frodo::Middleware::Authentication::Token
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Internal: Returns true if oauth token refresh flow should be used for
|
|
24
|
+
# authentication.
|
|
25
|
+
def oauth_refresh?
|
|
26
|
+
options[:refresh_token] &&
|
|
27
|
+
options[:client_id] &&
|
|
28
|
+
options[:client_secret]
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Frodo
|
|
4
|
+
module Concerns
|
|
5
|
+
module Base
|
|
6
|
+
attr_reader :options
|
|
7
|
+
|
|
8
|
+
MIME_TYPES = {
|
|
9
|
+
json: 'application/json'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
# Public: Creates a new client instance
|
|
13
|
+
#
|
|
14
|
+
# opts - A hash of options to be passed in (default: {}).
|
|
15
|
+
#
|
|
16
|
+
# :oauth_token - The String oauth access token to authenticate
|
|
17
|
+
# API calls (required unless password
|
|
18
|
+
# authentication is used).
|
|
19
|
+
# :refresh_token - The String refresh token to obtain fresh
|
|
20
|
+
# OAuth access tokens (required if oauth
|
|
21
|
+
# authentication is used).
|
|
22
|
+
# :instance_url - The String base url for all api requests
|
|
23
|
+
# (required if oauth authentication is used).
|
|
24
|
+
#
|
|
25
|
+
# :client_id - The oauth client id to use. Needed for both
|
|
26
|
+
# password and oauth authentication
|
|
27
|
+
# :client_secret - The oauth client secret to use.
|
|
28
|
+
#
|
|
29
|
+
# :host - The String hostname to use during
|
|
30
|
+
# authentication requests
|
|
31
|
+
# (default: 'login.microsoftonline.com').
|
|
32
|
+
#
|
|
33
|
+
# :base_path - The base path for the REST api. (default: '/')
|
|
34
|
+
#
|
|
35
|
+
# :authentication_retries - The number of times that client
|
|
36
|
+
# should attempt to reauthenticate
|
|
37
|
+
# before raising an exception (default: 3).
|
|
38
|
+
#
|
|
39
|
+
# :compress - Set to true to have Dynamics compress the
|
|
40
|
+
# response (default: false).
|
|
41
|
+
# :raw_json - Set to true to skip the conversion of
|
|
42
|
+
# Entities responses (default: false).
|
|
43
|
+
# :timeout - Faraday connection request read/open timeout.
|
|
44
|
+
# (default: nil).
|
|
45
|
+
#
|
|
46
|
+
# :proxy_uri - Proxy URI: 'http://proxy.example.com:port' or
|
|
47
|
+
# 'http://user@pass:proxy.example.com:port'
|
|
48
|
+
#
|
|
49
|
+
# :authentication_callback - A Proc that is called with the response body
|
|
50
|
+
# after a successful authentication.
|
|
51
|
+
#
|
|
52
|
+
# :request_headers - A hash containing custom headers that will be
|
|
53
|
+
# appended to each request
|
|
54
|
+
|
|
55
|
+
def initialize(opts = {})
|
|
56
|
+
raise ArgumentError, 'Please specify a hash of options' unless opts.is_a?(Hash)
|
|
57
|
+
|
|
58
|
+
# allow injecting the service for performance purpose such as
|
|
59
|
+
# when you have already a local schema
|
|
60
|
+
@service = opts.delete(:service)
|
|
61
|
+
|
|
62
|
+
@options = Hash[Frodo.configuration.options.map do |option|
|
|
63
|
+
[option, Frodo.configuration.send(option)]
|
|
64
|
+
end]
|
|
65
|
+
|
|
66
|
+
@options.merge! opts
|
|
67
|
+
yield builder if block_given?
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def instance_url
|
|
71
|
+
authenticate! unless options[:instance_url]
|
|
72
|
+
options[:instance_url]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def service
|
|
76
|
+
@service ||= Frodo::Service.new(instance_url, strict: false, metadata_document: metadata)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def inspect
|
|
80
|
+
"#<#{self.class} @options=#{@options.inspect}>"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Frodo
|
|
4
|
+
module Concerns
|
|
5
|
+
module Caching
|
|
6
|
+
# Public: Runs the block with caching disabled.
|
|
7
|
+
#
|
|
8
|
+
# block - A query/describe/etc.
|
|
9
|
+
#
|
|
10
|
+
# Returns the result of the block
|
|
11
|
+
def without_caching
|
|
12
|
+
options[:use_cache] = false
|
|
13
|
+
yield
|
|
14
|
+
ensure
|
|
15
|
+
options.delete(:use_cache)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
# Internal: Cache to use for the caching middleware
|
|
21
|
+
def cache
|
|
22
|
+
options[:cache]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Frodo
|
|
4
|
+
module Concerns
|
|
5
|
+
module Connection
|
|
6
|
+
# Public: The Faraday::Builder instance used for the middleware stack. This
|
|
7
|
+
# can be used to insert an custom middleware.
|
|
8
|
+
#
|
|
9
|
+
# Examples
|
|
10
|
+
#
|
|
11
|
+
# # Add the instrumentation middleware for Rails.
|
|
12
|
+
# client.middleware.use FaradayMiddleware::Instrumentation
|
|
13
|
+
#
|
|
14
|
+
# Returns the Faraday::Builder for the Faraday connection.
|
|
15
|
+
def middleware
|
|
16
|
+
connection.builder
|
|
17
|
+
end
|
|
18
|
+
alias builder middleware
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
# Internal: Internal faraday connection where all requests go through
|
|
23
|
+
def connection
|
|
24
|
+
@connection ||= Faraday.new(options[:instance_url],
|
|
25
|
+
connection_options) do |builder|
|
|
26
|
+
|
|
27
|
+
# Converts the request into JSON.
|
|
28
|
+
builder.request :json
|
|
29
|
+
# Handles reauthentication for 403 responses.
|
|
30
|
+
if authentication_middleware
|
|
31
|
+
builder.use authentication_middleware, self, options
|
|
32
|
+
end
|
|
33
|
+
# Sets the oauth token in the headers.
|
|
34
|
+
builder.use Frodo::Middleware::Authorization, self, options
|
|
35
|
+
# Ensures the instance url is set.
|
|
36
|
+
builder.use Frodo::Middleware::InstanceURL, self, options
|
|
37
|
+
# Caches GET requests.
|
|
38
|
+
builder.use Frodo::Middleware::Caching, cache, options if cache
|
|
39
|
+
# Follows 30x redirects.
|
|
40
|
+
builder.use FaradayMiddleware::FollowRedirects
|
|
41
|
+
# Raises errors for 40x responses.
|
|
42
|
+
builder.use Frodo::Middleware::RaiseError
|
|
43
|
+
# Parses returned JSON response into a hash.
|
|
44
|
+
builder.response :json, content_type: /\bjson$/
|
|
45
|
+
# Compress/Decompress the request/response
|
|
46
|
+
unless adapter == :httpclient
|
|
47
|
+
builder.use Frodo::Middleware::Gzip, self, options
|
|
48
|
+
end
|
|
49
|
+
# Inject OData headers into requests
|
|
50
|
+
builder.use Frodo::Middleware::OdataHeaders, self, options
|
|
51
|
+
# Inject custom headers into requests
|
|
52
|
+
builder.use Frodo::Middleware::CustomHeaders, self, options
|
|
53
|
+
# Log request/responses
|
|
54
|
+
if Frodo.log?
|
|
55
|
+
builder.use Frodo::Middleware::Logger,
|
|
56
|
+
Frodo.configuration.logger,
|
|
57
|
+
options
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
builder.adapter adapter
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def adapter
|
|
65
|
+
options[:adapter]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Internal: Faraday Connection options
|
|
69
|
+
def connection_options
|
|
70
|
+
{ request: {
|
|
71
|
+
timeout: options[:timeout],
|
|
72
|
+
open_timeout: options[:timeout]
|
|
73
|
+
},
|
|
74
|
+
proxy: options[:proxy_uri],
|
|
75
|
+
ssl: options[:ssl] }
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Frodo
|
|
4
|
+
module Concerns
|
|
5
|
+
module Verbs
|
|
6
|
+
# Internal: Define methods to handle a verb.
|
|
7
|
+
#
|
|
8
|
+
# verbs - A list of verbs to define methods for.
|
|
9
|
+
#
|
|
10
|
+
# Examples
|
|
11
|
+
#
|
|
12
|
+
# define_verbs :get, :post
|
|
13
|
+
#
|
|
14
|
+
# Returns nil.
|
|
15
|
+
def define_verbs(*verbs)
|
|
16
|
+
verbs.each do |verb|
|
|
17
|
+
define_verb(verb)
|
|
18
|
+
define_api_verb(verb)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Internal: Defines a method to handle HTTP requests with the passed in
|
|
23
|
+
# verb.
|
|
24
|
+
#
|
|
25
|
+
# verb - Symbol name of the verb (e.g. :get).
|
|
26
|
+
#
|
|
27
|
+
# Examples
|
|
28
|
+
#
|
|
29
|
+
# define_verb :get
|
|
30
|
+
# # => get '/path/to/entity'
|
|
31
|
+
#
|
|
32
|
+
# Returns nil.
|
|
33
|
+
def define_verb(verb)
|
|
34
|
+
define_method verb do |*args, &block|
|
|
35
|
+
retries = options[:authentication_retries]
|
|
36
|
+
begin
|
|
37
|
+
connection.send(verb, *args, &block)
|
|
38
|
+
rescue Frodo::UnauthorizedError
|
|
39
|
+
if retries.positive?
|
|
40
|
+
retries -= 1
|
|
41
|
+
connection.url_prefix = options[:instance_url]
|
|
42
|
+
retry
|
|
43
|
+
end
|
|
44
|
+
raise
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Internal: Defines a method to handle HTTP requests with the passed in
|
|
50
|
+
# verb to a Dynamics api endpoint.
|
|
51
|
+
#
|
|
52
|
+
# verb - Symbol name of the verb (e.g. :get).
|
|
53
|
+
#
|
|
54
|
+
# Examples
|
|
55
|
+
#
|
|
56
|
+
# define_api_verb :get
|
|
57
|
+
# # => api_get 'entity_set'
|
|
58
|
+
#
|
|
59
|
+
# Returns nil.
|
|
60
|
+
def define_api_verb(verb)
|
|
61
|
+
define_method :"api_#{verb}" do |*args, &block|
|
|
62
|
+
args[0] = api_path(args[0])
|
|
63
|
+
send(verb, *args, &block)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|