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.
Files changed (153) hide show
  1. checksums.yaml +7 -0
  2. data/.autotest +2 -0
  3. data/.circleci/config.yml +54 -0
  4. data/.gitignore +24 -0
  5. data/.gitlab-ci.yml +9 -0
  6. data/.rspec +2 -0
  7. data/.ruby-gemset +1 -0
  8. data/.ruby-version +1 -0
  9. data/.travis.yml +75 -0
  10. data/CHANGELOG.md +163 -0
  11. data/Gemfile +4 -0
  12. data/LICENSE.txt +23 -0
  13. data/README.md +479 -0
  14. data/Rakefile +7 -0
  15. data/TODO.md +55 -0
  16. data/frodo.gemspec +39 -0
  17. data/images/frodo.jpg +0 -0
  18. data/lib/frodo/abstract_client.rb +11 -0
  19. data/lib/frodo/client.rb +6 -0
  20. data/lib/frodo/concerns/api.rb +292 -0
  21. data/lib/frodo/concerns/authentication.rb +32 -0
  22. data/lib/frodo/concerns/base.rb +84 -0
  23. data/lib/frodo/concerns/caching.rb +26 -0
  24. data/lib/frodo/concerns/connection.rb +79 -0
  25. data/lib/frodo/concerns/verbs.rb +68 -0
  26. data/lib/frodo/config.rb +143 -0
  27. data/lib/frodo/entity.rb +335 -0
  28. data/lib/frodo/entity_container.rb +75 -0
  29. data/lib/frodo/entity_set.rb +131 -0
  30. data/lib/frodo/errors.rb +70 -0
  31. data/lib/frodo/middleware/authentication/token.rb +13 -0
  32. data/lib/frodo/middleware/authentication.rb +87 -0
  33. data/lib/frodo/middleware/authorization.rb +18 -0
  34. data/lib/frodo/middleware/caching.rb +30 -0
  35. data/lib/frodo/middleware/custom_headers.rb +14 -0
  36. data/lib/frodo/middleware/gzip.rb +33 -0
  37. data/lib/frodo/middleware/instance_url.rb +20 -0
  38. data/lib/frodo/middleware/logger.rb +42 -0
  39. data/lib/frodo/middleware/multipart.rb +64 -0
  40. data/lib/frodo/middleware/odata_headers.rb +13 -0
  41. data/lib/frodo/middleware/raise_error.rb +47 -0
  42. data/lib/frodo/middleware.rb +33 -0
  43. data/lib/frodo/navigation_property/proxy.rb +80 -0
  44. data/lib/frodo/navigation_property.rb +29 -0
  45. data/lib/frodo/properties/binary.rb +50 -0
  46. data/lib/frodo/properties/boolean.rb +37 -0
  47. data/lib/frodo/properties/collection.rb +50 -0
  48. data/lib/frodo/properties/complex.rb +114 -0
  49. data/lib/frodo/properties/date.rb +27 -0
  50. data/lib/frodo/properties/date_time.rb +83 -0
  51. data/lib/frodo/properties/date_time_offset.rb +17 -0
  52. data/lib/frodo/properties/decimal.rb +54 -0
  53. data/lib/frodo/properties/enum.rb +62 -0
  54. data/lib/frodo/properties/float.rb +67 -0
  55. data/lib/frodo/properties/geography/base.rb +162 -0
  56. data/lib/frodo/properties/geography/line_string.rb +33 -0
  57. data/lib/frodo/properties/geography/point.rb +31 -0
  58. data/lib/frodo/properties/geography/polygon.rb +38 -0
  59. data/lib/frodo/properties/geography.rb +13 -0
  60. data/lib/frodo/properties/guid.rb +17 -0
  61. data/lib/frodo/properties/integer.rb +107 -0
  62. data/lib/frodo/properties/number.rb +14 -0
  63. data/lib/frodo/properties/string.rb +72 -0
  64. data/lib/frodo/properties/time.rb +40 -0
  65. data/lib/frodo/properties/time_of_day.rb +27 -0
  66. data/lib/frodo/properties.rb +32 -0
  67. data/lib/frodo/property.rb +139 -0
  68. data/lib/frodo/property_registry.rb +41 -0
  69. data/lib/frodo/query/criteria/comparison_operators.rb +49 -0
  70. data/lib/frodo/query/criteria/date_functions.rb +61 -0
  71. data/lib/frodo/query/criteria/geography_functions.rb +21 -0
  72. data/lib/frodo/query/criteria/lambda_operators.rb +27 -0
  73. data/lib/frodo/query/criteria/string_functions.rb +40 -0
  74. data/lib/frodo/query/criteria.rb +92 -0
  75. data/lib/frodo/query/in_batches.rb +58 -0
  76. data/lib/frodo/query.rb +221 -0
  77. data/lib/frodo/railtie.rb +19 -0
  78. data/lib/frodo/schema/complex_type.rb +79 -0
  79. data/lib/frodo/schema/enum_type.rb +95 -0
  80. data/lib/frodo/schema.rb +164 -0
  81. data/lib/frodo/service.rb +199 -0
  82. data/lib/frodo/service_registry.rb +52 -0
  83. data/lib/frodo/version.rb +3 -0
  84. data/lib/frodo.rb +67 -0
  85. data/spec/fixtures/auth_success_response.json +11 -0
  86. data/spec/fixtures/error.json +11 -0
  87. data/spec/fixtures/files/entity_to_xml.xml +18 -0
  88. data/spec/fixtures/files/error.xml +5 -0
  89. data/spec/fixtures/files/metadata.xml +150 -0
  90. data/spec/fixtures/files/metadata_with_error.xml +157 -0
  91. data/spec/fixtures/files/product_0.json +10 -0
  92. data/spec/fixtures/files/product_0.xml +28 -0
  93. data/spec/fixtures/files/products.json +106 -0
  94. data/spec/fixtures/files/products.xml +308 -0
  95. data/spec/fixtures/files/supplier_0.json +26 -0
  96. data/spec/fixtures/files/supplier_0.xml +32 -0
  97. data/spec/fixtures/leads.json +923 -0
  98. data/spec/fixtures/refresh_error_response.json +8 -0
  99. data/spec/frodo/abstract_client_spec.rb +13 -0
  100. data/spec/frodo/client_spec.rb +57 -0
  101. data/spec/frodo/concerns/authentication_spec.rb +79 -0
  102. data/spec/frodo/concerns/base_spec.rb +68 -0
  103. data/spec/frodo/concerns/caching_spec.rb +40 -0
  104. data/spec/frodo/concerns/connection_spec.rb +65 -0
  105. data/spec/frodo/config_spec.rb +127 -0
  106. data/spec/frodo/entity/shared_examples.rb +83 -0
  107. data/spec/frodo/entity_container_spec.rb +38 -0
  108. data/spec/frodo/entity_set_spec.rb +169 -0
  109. data/spec/frodo/entity_spec.rb +153 -0
  110. data/spec/frodo/errors_spec.rb +48 -0
  111. data/spec/frodo/middleware/authentication/token_spec.rb +87 -0
  112. data/spec/frodo/middleware/authentication_spec.rb +83 -0
  113. data/spec/frodo/middleware/authorization_spec.rb +17 -0
  114. data/spec/frodo/middleware/custom_headers_spec.rb +21 -0
  115. data/spec/frodo/middleware/gzip_spec.rb +68 -0
  116. data/spec/frodo/middleware/instance_url_spec.rb +27 -0
  117. data/spec/frodo/middleware/logger_spec.rb +21 -0
  118. data/spec/frodo/middleware/odata_headers_spec.rb +15 -0
  119. data/spec/frodo/middleware/raise_error_spec.rb +66 -0
  120. data/spec/frodo/navigation_property/proxy_spec.rb +46 -0
  121. data/spec/frodo/navigation_property_spec.rb +55 -0
  122. data/spec/frodo/properties/binary_spec.rb +50 -0
  123. data/spec/frodo/properties/boolean_spec.rb +72 -0
  124. data/spec/frodo/properties/collection_spec.rb +44 -0
  125. data/spec/frodo/properties/date_spec.rb +23 -0
  126. data/spec/frodo/properties/date_time_offset_spec.rb +30 -0
  127. data/spec/frodo/properties/date_time_spec.rb +23 -0
  128. data/spec/frodo/properties/decimal_spec.rb +50 -0
  129. data/spec/frodo/properties/float_spec.rb +45 -0
  130. data/spec/frodo/properties/geography/line_string_spec.rb +33 -0
  131. data/spec/frodo/properties/geography/point_spec.rb +29 -0
  132. data/spec/frodo/properties/geography/polygon_spec.rb +55 -0
  133. data/spec/frodo/properties/geography/shared_examples.rb +72 -0
  134. data/spec/frodo/properties/guid_spec.rb +17 -0
  135. data/spec/frodo/properties/integer_spec.rb +58 -0
  136. data/spec/frodo/properties/string_spec.rb +46 -0
  137. data/spec/frodo/properties/time_of_day_spec.rb +23 -0
  138. data/spec/frodo/properties/time_spec.rb +15 -0
  139. data/spec/frodo/property_registry_spec.rb +16 -0
  140. data/spec/frodo/property_spec.rb +71 -0
  141. data/spec/frodo/query/criteria_spec.rb +229 -0
  142. data/spec/frodo/query_spec.rb +156 -0
  143. data/spec/frodo/schema/complex_type_spec.rb +97 -0
  144. data/spec/frodo/schema/enum_type_spec.rb +112 -0
  145. data/spec/frodo/schema_spec.rb +113 -0
  146. data/spec/frodo/service_registry_spec.rb +19 -0
  147. data/spec/frodo/service_spec.rb +153 -0
  148. data/spec/frodo/usage_example_spec.rb +161 -0
  149. data/spec/spec_helper.rb +35 -0
  150. data/spec/support/coverage.rb +2 -0
  151. data/spec/support/fixture_helpers.rb +14 -0
  152. data/spec/support/middleware.rb +19 -0
  153. metadata +479 -0
@@ -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