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
data/lib/frodo/errors.rb
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
module Frodo
|
|
2
|
+
# Base class for Frodo errors
|
|
3
|
+
class Error < StandardError
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
# Base class for network errors
|
|
7
|
+
class RequestError < Error
|
|
8
|
+
attr_reader :response
|
|
9
|
+
|
|
10
|
+
def initialize(response, message = nil)
|
|
11
|
+
super(message)
|
|
12
|
+
@message = message
|
|
13
|
+
@response = response
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def http_status
|
|
17
|
+
response.status
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def message
|
|
21
|
+
[default_message, @message].compact.join(': ')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def default_message
|
|
25
|
+
nil
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class ClientError < RequestError
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class ServerError < RequestError
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
module Errors
|
|
36
|
+
ERROR_MAP = []
|
|
37
|
+
|
|
38
|
+
CLIENT_ERRORS = {
|
|
39
|
+
400 => "Bad Request",
|
|
40
|
+
401 => "Access Denied",
|
|
41
|
+
403 => "Forbidden",
|
|
42
|
+
404 => "Not Found",
|
|
43
|
+
405 => "Method Not Allowed",
|
|
44
|
+
406 => "Not Acceptable",
|
|
45
|
+
413 => "Request Entity Too Large",
|
|
46
|
+
415 => "Unsupported Media Type"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
CLIENT_ERRORS.each do |code, message|
|
|
50
|
+
klass = Class.new(ClientError) do
|
|
51
|
+
send(:define_method, :default_message) { "#{code} #{message}" }
|
|
52
|
+
end
|
|
53
|
+
const_set(message.delete(' \-\''), klass)
|
|
54
|
+
ERROR_MAP[code] = klass
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
SERVER_ERRORS = {
|
|
58
|
+
500 => "Internal Server Error",
|
|
59
|
+
503 => "Service Unavailable"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
SERVER_ERRORS.each do |code, message|
|
|
63
|
+
klass = Class.new(ServerError) do
|
|
64
|
+
send(:define_method, :default_message) { "#{code} #{message}" }
|
|
65
|
+
end
|
|
66
|
+
const_set(message.delete(' \-\''), klass)
|
|
67
|
+
ERROR_MAP[code] = klass
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Frodo
|
|
4
|
+
# Authentication middleware used if oauth_token and refresh_token are set
|
|
5
|
+
class Middleware::Authentication::Token < Frodo::Middleware::Authentication
|
|
6
|
+
def params
|
|
7
|
+
{ grant_type: 'refresh_token',
|
|
8
|
+
refresh_token: @options[:refresh_token],
|
|
9
|
+
client_id: @options[:client_id],
|
|
10
|
+
client_secret: @options[:client_secret] }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Frodo
|
|
4
|
+
# Faraday middleware that allows for on the fly authentication of requests.
|
|
5
|
+
# When a request fails (a status of 401 is returned), the middleware
|
|
6
|
+
# will attempt to either reauthenticate (username and password) or refresh
|
|
7
|
+
# the oauth access token (if a refresh token is present).
|
|
8
|
+
class Middleware::Authentication < Frodo::Middleware
|
|
9
|
+
autoload :Token, 'frodo/middleware/authentication/token'
|
|
10
|
+
|
|
11
|
+
# Rescue from 401's, authenticate then raise the error again so the client
|
|
12
|
+
# can reissue the request.
|
|
13
|
+
def call(env)
|
|
14
|
+
@app.call(env)
|
|
15
|
+
rescue Frodo::UnauthorizedError
|
|
16
|
+
authenticate!
|
|
17
|
+
raise
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Internal: Performs the authentication and returns the response body.
|
|
21
|
+
def authenticate!
|
|
22
|
+
response = connection.post '/common/oauth2/token' do |req|
|
|
23
|
+
req.body = encode_www_form(params)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
if response.status >= 500
|
|
27
|
+
raise Frodo::ServerError, error_message(response)
|
|
28
|
+
elsif response.status != 200
|
|
29
|
+
raise Frodo::AuthenticationError, error_message(response)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
@options[:oauth_token] = response.body['access_token']
|
|
33
|
+
@options[:refresh_token] = response.body['refresh_token']
|
|
34
|
+
@options[:authentication_callback]&.call(response.body)
|
|
35
|
+
|
|
36
|
+
response.body
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Internal: The params to post to the OAuth service.
|
|
40
|
+
def params
|
|
41
|
+
raise NotImplementedError
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Internal: Faraday connection to use when sending an authentication request.
|
|
45
|
+
def connection
|
|
46
|
+
@connection ||= Faraday.new(faraday_options) do |builder|
|
|
47
|
+
builder.use Faraday::Request::UrlEncoded
|
|
48
|
+
builder.response :json
|
|
49
|
+
|
|
50
|
+
if Frodo.log?
|
|
51
|
+
builder.use Frodo::Middleware::Logger,
|
|
52
|
+
Frodo.configuration.logger,
|
|
53
|
+
@options
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
builder.adapter @options[:adapter]
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Internal: The parsed error response.
|
|
61
|
+
def error_message(response)
|
|
62
|
+
"#{response.body['error']}: #{response.body['error_description']}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Featured detect form encoding.
|
|
66
|
+
# URI in 1.8 does not include encode_www_form
|
|
67
|
+
def encode_www_form(params)
|
|
68
|
+
if URI.respond_to?(:encode_www_form)
|
|
69
|
+
URI.encode_www_form(params)
|
|
70
|
+
else
|
|
71
|
+
params.map do |k, v|
|
|
72
|
+
k = CGI.escape(k.to_s)
|
|
73
|
+
v = CGI.escape(v.to_s)
|
|
74
|
+
"#{k}=#{v}"
|
|
75
|
+
end.join('&')
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def faraday_options
|
|
82
|
+
{ url: "https://#{@options[:host]}",
|
|
83
|
+
proxy: @options[:proxy_uri],
|
|
84
|
+
ssl: @options[:ssl] }
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Frodo
|
|
4
|
+
# Piece of middleware that simply injects the OAuth token into the request
|
|
5
|
+
# headers.
|
|
6
|
+
class Middleware::Authorization < Frodo::Middleware
|
|
7
|
+
AUTH_HEADER = 'Authorization'
|
|
8
|
+
|
|
9
|
+
def call(env)
|
|
10
|
+
env[:request_headers][AUTH_HEADER] = %(Bearer #{token})
|
|
11
|
+
@app.call(env)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def token
|
|
15
|
+
@options[:oauth_token]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Frodo
|
|
4
|
+
class Middleware::Caching < FaradayMiddleware::Caching
|
|
5
|
+
def call(env)
|
|
6
|
+
expire(cache_key(env)) unless use_cache?
|
|
7
|
+
super
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def expire(key)
|
|
11
|
+
cache&.delete(key)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# We don't want to cache requests for different clients, so append the
|
|
15
|
+
# oauth token to the cache key.
|
|
16
|
+
def cache_key(env)
|
|
17
|
+
super(env) + hashed_auth_header(env)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def use_cache?
|
|
21
|
+
@options.fetch(:use_cache, true)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def hashed_auth_header(env)
|
|
25
|
+
Digest::SHA1.hexdigest(
|
|
26
|
+
env[:request_headers][Restforce::Middleware::Authorization::AUTH_HEADER]
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Frodo
|
|
4
|
+
# Middleware that allows you to specify custom request headers
|
|
5
|
+
# when initializing Frodo client
|
|
6
|
+
class Middleware::CustomHeaders < Frodo::Middleware
|
|
7
|
+
def call(env)
|
|
8
|
+
headers = @options[:request_headers]
|
|
9
|
+
env[:request_headers].merge!(headers) if headers.is_a?(Hash)
|
|
10
|
+
|
|
11
|
+
@app.call(env)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'zlib'
|
|
4
|
+
|
|
5
|
+
module Frodo
|
|
6
|
+
# Middleware to uncompress GZIP compressed responses from Salesforce.
|
|
7
|
+
class Middleware::Gzip < Frodo::Middleware
|
|
8
|
+
ACCEPT_ENCODING_HEADER = 'Accept-Encoding'
|
|
9
|
+
CONTENT_ENCODING_HEADER = 'Content-Encoding'
|
|
10
|
+
ENCODING = 'gzip'
|
|
11
|
+
|
|
12
|
+
def call(env)
|
|
13
|
+
env[:request_headers][ACCEPT_ENCODING_HEADER] = ENCODING if @options[:compress]
|
|
14
|
+
@app.call(env).on_complete do |environment|
|
|
15
|
+
on_complete(environment)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def on_complete(env)
|
|
20
|
+
env[:body] = decompress(env[:body]) if gzipped?(env)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Internal: Returns true if the response is gzipped.
|
|
24
|
+
def gzipped?(env)
|
|
25
|
+
env[:response_headers][CONTENT_ENCODING_HEADER] == ENCODING
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Internal: Decompresses a gzipped string.
|
|
29
|
+
def decompress(body)
|
|
30
|
+
Zlib::GzipReader.new(StringIO.new(body)).read
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Frodo
|
|
4
|
+
# Middleware which asserts that the instance_url is always set
|
|
5
|
+
class Middleware::InstanceURL < Frodo::Middleware
|
|
6
|
+
def call(env)
|
|
7
|
+
# If the connection url_prefix isn't set, we must not be authenticated.
|
|
8
|
+
unless url_prefix_set?
|
|
9
|
+
raise Frodo::UnauthorizedError,
|
|
10
|
+
'Connection prefix not set'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
@app.call(env)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def url_prefix_set?
|
|
17
|
+
!!(connection.url_prefix&.host)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'forwardable'
|
|
4
|
+
|
|
5
|
+
module Frodo
|
|
6
|
+
class Middleware::Logger < Faraday::Response::Middleware
|
|
7
|
+
extend Forwardable
|
|
8
|
+
|
|
9
|
+
def initialize(app, logger, options)
|
|
10
|
+
super(app)
|
|
11
|
+
@options = options
|
|
12
|
+
@logger = logger || begin
|
|
13
|
+
require 'logger'
|
|
14
|
+
::Logger.new(STDOUT)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def_delegators :@logger, :debug, :info, :warn, :error, :fatal
|
|
19
|
+
|
|
20
|
+
def call(env)
|
|
21
|
+
debug('request') do
|
|
22
|
+
dump url: env[:url].to_s,
|
|
23
|
+
method: env[:method],
|
|
24
|
+
headers: env[:request_headers],
|
|
25
|
+
body: env[:body]
|
|
26
|
+
end
|
|
27
|
+
super
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def on_complete(env)
|
|
31
|
+
debug('response') do
|
|
32
|
+
dump status: env[:status].to_s,
|
|
33
|
+
headers: env[:response_headers],
|
|
34
|
+
body: env[:body]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def dump(hash)
|
|
39
|
+
"\n" + hash.map { |k, v| " #{k}: #{v.inspect}" }.join("\n")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Frodo
|
|
4
|
+
class Middleware::Multipart < Faraday::Request::UrlEncoded
|
|
5
|
+
self.mime_type = 'multipart/form-data'
|
|
6
|
+
DEFAULT_BOUNDARY = "--boundary_string"
|
|
7
|
+
JSON_CONTENT_TYPE = { "Content-Type" => "application/json" }.freeze
|
|
8
|
+
|
|
9
|
+
def call(env)
|
|
10
|
+
match_content_type(env) do |params|
|
|
11
|
+
env[:request] ||= {}
|
|
12
|
+
env[:request][:boundary] ||= DEFAULT_BOUNDARY
|
|
13
|
+
env[:request_headers][CONTENT_TYPE] += ";boundary=#{env[:request][:boundary]}"
|
|
14
|
+
env[:body] = create_multipart(env, params)
|
|
15
|
+
end
|
|
16
|
+
@app.call env
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def process_request?(env)
|
|
20
|
+
type = request_type(env)
|
|
21
|
+
env[:body].respond_to?(:each_key) && !env[:body].empty? && (
|
|
22
|
+
(type.empty? && has_multipart?(env[:body])) ||
|
|
23
|
+
type == self.class.mime_type
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def has_multipart?(obj)
|
|
28
|
+
# string is an enum in 1.8, returning list of itself
|
|
29
|
+
if obj.respond_to?(:each) && !obj.is_a?(String)
|
|
30
|
+
(obj.respond_to?(:values) ? obj.values : obj).each do |val|
|
|
31
|
+
return true if val.respond_to?(:content_type) || has_multipart?(val)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def create_multipart(env, params)
|
|
38
|
+
boundary = env[:request][:boundary]
|
|
39
|
+
parts = []
|
|
40
|
+
|
|
41
|
+
# Fields
|
|
42
|
+
parts << Faraday::Parts::Part.new(
|
|
43
|
+
boundary,
|
|
44
|
+
'entity_content',
|
|
45
|
+
params.reject { |k, v| v.respond_to? :content_type }.to_json,
|
|
46
|
+
JSON_CONTENT_TYPE
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Files
|
|
50
|
+
params.each do |k, v|
|
|
51
|
+
next unless v.respond_to? :content_type
|
|
52
|
+
parts << Faraday::Parts::Part.new(boundary,
|
|
53
|
+
k.to_s,
|
|
54
|
+
v)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
parts << Faraday::Parts::EpiloguePart.new(boundary)
|
|
58
|
+
|
|
59
|
+
body = Faraday::CompositeReadIO.new(parts)
|
|
60
|
+
env[:request_headers]['Content-Length'] = body.length.to_s
|
|
61
|
+
body
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Frodo
|
|
4
|
+
# Middleware that allows you to specify custom request headers
|
|
5
|
+
# when initializing Frodo client
|
|
6
|
+
class Middleware::OdataHeaders < Frodo::Middleware
|
|
7
|
+
def call(env)
|
|
8
|
+
env[:request_headers].merge!({'OData-Version' => '4.0', 'Content-type' => 'application/json'})
|
|
9
|
+
|
|
10
|
+
@app.call(env)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module Frodo
|
|
3
|
+
class Middleware::RaiseError < Faraday::Response::Middleware
|
|
4
|
+
def on_complete(env)
|
|
5
|
+
@env = env
|
|
6
|
+
|
|
7
|
+
case env[:status]
|
|
8
|
+
when 300
|
|
9
|
+
raise Faraday::Error::ClientError.new("300: The external ID provided matches " \
|
|
10
|
+
"more than one record",
|
|
11
|
+
response_values)
|
|
12
|
+
when 401
|
|
13
|
+
raise Frodo::UnauthorizedError, message
|
|
14
|
+
when 404
|
|
15
|
+
raise Faraday::Error::ResourceNotFound, message
|
|
16
|
+
when 413
|
|
17
|
+
raise Faraday::Error::ClientError.new("413: Request Entity Too Large",
|
|
18
|
+
response_values)
|
|
19
|
+
when 400...600
|
|
20
|
+
raise Faraday::Error::ClientError.new(message, response_values)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def message
|
|
25
|
+
"#{body['error']['code']}: #{body['error']['message']}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def body
|
|
29
|
+
@body = (@env[:body].is_a?(Array) ? @env[:body].first : @env[:body])
|
|
30
|
+
|
|
31
|
+
case @body
|
|
32
|
+
when Hash
|
|
33
|
+
@body
|
|
34
|
+
else
|
|
35
|
+
{ 'error' => {'code' => '(error code missing)', 'message' => @body}}
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def response_values
|
|
40
|
+
{
|
|
41
|
+
status: @env[:status],
|
|
42
|
+
headers: @env[:response_headers],
|
|
43
|
+
body: @env[:body]
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Frodo
|
|
4
|
+
# Base class that all middleware can extend. Provides some convenient helper
|
|
5
|
+
# functions.
|
|
6
|
+
class Middleware < Faraday::Middleware
|
|
7
|
+
autoload :RaiseError, 'frodo/middleware/raise_error'
|
|
8
|
+
autoload :Authentication, 'frodo/middleware/authentication'
|
|
9
|
+
autoload :Authorization, 'frodo/middleware/authorization'
|
|
10
|
+
autoload :InstanceURL, 'frodo/middleware/instance_url'
|
|
11
|
+
autoload :Multipart, 'frodo/middleware/multipart'
|
|
12
|
+
autoload :Caching, 'frodo/middleware/caching'
|
|
13
|
+
autoload :Logger, 'frodo/middleware/logger'
|
|
14
|
+
autoload :Gzip, 'frodo/middleware/gzip'
|
|
15
|
+
autoload :CustomHeaders, 'frodo/middleware/custom_headers'
|
|
16
|
+
|
|
17
|
+
def initialize(app, client, options)
|
|
18
|
+
@app = app
|
|
19
|
+
@client = client
|
|
20
|
+
@options = options
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Internal: Proxy to the client.
|
|
24
|
+
def client
|
|
25
|
+
@client
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Internal: Proxy to the client's faraday connection.
|
|
29
|
+
def connection
|
|
30
|
+
client.send(:connection)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
module Frodo
|
|
2
|
+
class NavigationProperty
|
|
3
|
+
class Proxy
|
|
4
|
+
def initialize(entity, nav_name)
|
|
5
|
+
@entity = entity
|
|
6
|
+
@nav_name = nav_name
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def value=(value)
|
|
10
|
+
@value = value
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def value
|
|
14
|
+
if link.nil?
|
|
15
|
+
if nav_property.nav_type == :collection
|
|
16
|
+
[]
|
|
17
|
+
else
|
|
18
|
+
nil
|
|
19
|
+
end
|
|
20
|
+
else
|
|
21
|
+
@value ||= fetch_result
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
attr_reader :entity, :nav_name
|
|
28
|
+
|
|
29
|
+
def service
|
|
30
|
+
@service ||= Frodo::ServiceRegistry[entity.service_name]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def namespace
|
|
34
|
+
@namespace ||= service.namespace
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def schema
|
|
38
|
+
@schema ||= service.schemas[namespace]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def entity_type
|
|
42
|
+
@entity_type ||= entity.name
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def link
|
|
46
|
+
entity.links[nav_name]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def nav_property
|
|
50
|
+
schema.navigation_properties[entity_type][nav_name]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def fetch_result
|
|
54
|
+
raise "Invalid navigation link for #{nav_name}" unless link[:href]
|
|
55
|
+
|
|
56
|
+
options = {
|
|
57
|
+
type: nav_property.entity_type,
|
|
58
|
+
namespace: namespace,
|
|
59
|
+
service_name: entity.service_name
|
|
60
|
+
}
|
|
61
|
+
entity_set = Struct.new(:service, :entity_options)
|
|
62
|
+
.new(entity.service, options)
|
|
63
|
+
|
|
64
|
+
query = Frodo::Query.new(entity_set)
|
|
65
|
+
begin
|
|
66
|
+
result = query.execute(link[:href])
|
|
67
|
+
rescue => ex
|
|
68
|
+
raise ex unless ex.message =~ /Not Found/
|
|
69
|
+
result = []
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
if nav_property.nav_type == :collection
|
|
73
|
+
result
|
|
74
|
+
else
|
|
75
|
+
result.first
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require 'frodo/navigation_property/proxy'
|
|
2
|
+
|
|
3
|
+
module Frodo
|
|
4
|
+
class NavigationProperty
|
|
5
|
+
attr_reader :name, :type, :nullable, :partner
|
|
6
|
+
|
|
7
|
+
def initialize(options)
|
|
8
|
+
@name = options[:name] or raise ArgumentError, 'Name is required'
|
|
9
|
+
@type = options[:type] or raise ArgumentError, 'Type is required'
|
|
10
|
+
@nullable = options[:nullable] || true
|
|
11
|
+
@partner = options[:partner]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def nav_type
|
|
15
|
+
@nav_type ||= type =~ /^Collection/ ? :collection : :entity
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def entity_type
|
|
19
|
+
@entity_type ||= type.split(/[()]/).last
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.build(nav_property_xml)
|
|
23
|
+
options = nav_property_xml.attributes.map do |name, attr|
|
|
24
|
+
[name.downcase.to_sym, attr.value]
|
|
25
|
+
end.to_h
|
|
26
|
+
new(options)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module Frodo
|
|
2
|
+
module Properties
|
|
3
|
+
# Defines the Binary Frodo type.
|
|
4
|
+
class Binary < Frodo::Property
|
|
5
|
+
# Returns the property value, properly typecast
|
|
6
|
+
# @return [Integer,nil]
|
|
7
|
+
def value
|
|
8
|
+
if (@value.nil? || @value.empty?) && allows_nil?
|
|
9
|
+
nil
|
|
10
|
+
else
|
|
11
|
+
@value.to_i
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Sets the property value
|
|
16
|
+
# @params new_value [0,1,Boolean]
|
|
17
|
+
def value=(new_value)
|
|
18
|
+
validate(new_value)
|
|
19
|
+
@value = parse_value(new_value)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# The Frodo type name
|
|
23
|
+
def type
|
|
24
|
+
'Edm.Binary'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Value to be used in URLs.
|
|
28
|
+
# @return [String]
|
|
29
|
+
def url_value
|
|
30
|
+
"binary'#{value}'"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def parse_value(value)
|
|
36
|
+
if value == 0 || value == '0' || value == false
|
|
37
|
+
'0'
|
|
38
|
+
else
|
|
39
|
+
'1'
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def validate(value)
|
|
44
|
+
unless [0,1,'0','1',true,false].include?(value)
|
|
45
|
+
validation_error 'Value is outside accepted range: 0 or 1'
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Frodo
|
|
2
|
+
module Properties
|
|
3
|
+
# Defines the Boolean Frodo type.
|
|
4
|
+
class Boolean < Frodo::Property
|
|
5
|
+
# Returns the property value, properly typecast
|
|
6
|
+
# @return [Boolean, nil]
|
|
7
|
+
def value
|
|
8
|
+
if (@value.nil? || @value.empty?) && allows_nil?
|
|
9
|
+
nil
|
|
10
|
+
else
|
|
11
|
+
(@value == 'true' || @value == '1')
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Sets the property value
|
|
16
|
+
# @params new_value [Boolean]
|
|
17
|
+
def value=(new_value)
|
|
18
|
+
validate(new_value)
|
|
19
|
+
@value = new_value.to_s
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# The Frodo type name
|
|
23
|
+
def type
|
|
24
|
+
'Edm.Boolean'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def validate(value)
|
|
30
|
+
return if value.nil? && allows_nil?
|
|
31
|
+
unless [0,1,'0','1','true','false',true,false].include?(value)
|
|
32
|
+
validation_error 'Value is outside accepted range: true or false'
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|