frodo 0.10.0
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/.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
|