frodo 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
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