alchemrest 3.1.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 (245) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +22 -0
  4. data/.rubocop_todo.yml +242 -0
  5. data/.ruby-version +1 -0
  6. data/Appraisals +19 -0
  7. data/Gemfile +6 -0
  8. data/Gemfile.lock +378 -0
  9. data/Rakefile +29 -0
  10. data/alchemrest.gemspec +71 -0
  11. data/coach.yml +5 -0
  12. data/examples/bank_api/client.rb +31 -0
  13. data/examples/bank_api/data/account.rb +21 -0
  14. data/examples/bank_api/data/ach.rb +16 -0
  15. data/examples/bank_api/data/business_account.rb +22 -0
  16. data/examples/bank_api/data/card.rb +21 -0
  17. data/examples/bank_api/data/check.rb +19 -0
  18. data/examples/bank_api/data/product.rb +20 -0
  19. data/examples/bank_api/data/transaction.rb +49 -0
  20. data/examples/bank_api/data/user.rb +27 -0
  21. data/examples/bank_api/factories.rb +68 -0
  22. data/examples/bank_api/graph_visualization.rb +45 -0
  23. data/examples/bank_api/positive_interest_string.rb +33 -0
  24. data/examples/bank_api/requests/delete_user.rb +17 -0
  25. data/examples/bank_api/requests/get_business_account.rb +24 -0
  26. data/examples/bank_api/requests/get_products.rb +12 -0
  27. data/examples/bank_api/requests/get_transactions.rb +34 -0
  28. data/examples/bank_api/requests/get_user.rb +19 -0
  29. data/examples/bank_api/requests/post_transaction.rb +20 -0
  30. data/examples/bank_api/requests/update_user.rb +28 -0
  31. data/examples/bank_api/root.rb +52 -0
  32. data/examples/bank_api.rb +33 -0
  33. data/gemfiles/faraday_2.gemfile +9 -0
  34. data/gemfiles/faraday_2.gemfile.lock +363 -0
  35. data/gemfiles/rails_7_0.gemfile.lock +341 -0
  36. data/gemfiles/rails_7_2.gemfile +9 -0
  37. data/gemfiles/rails_7_2.gemfile.lock +384 -0
  38. data/gemfiles/rails_8_0.gemfile +9 -0
  39. data/gemfiles/rails_8_0.gemfile.lock +385 -0
  40. data/lib/alchemrest/circuit_breaker.rb +84 -0
  41. data/lib/alchemrest/client/configuration/connection.rb +83 -0
  42. data/lib/alchemrest/client/configuration.rb +89 -0
  43. data/lib/alchemrest/client.rb +48 -0
  44. data/lib/alchemrest/cop.rb +8 -0
  45. data/lib/alchemrest/data/capture_configuration.rb +77 -0
  46. data/lib/alchemrest/data/field.rb +36 -0
  47. data/lib/alchemrest/data/graph.rb +40 -0
  48. data/lib/alchemrest/data/record.rb +60 -0
  49. data/lib/alchemrest/data/schema.rb +80 -0
  50. data/lib/alchemrest/data.rb +9 -0
  51. data/lib/alchemrest/endpoint_definition.rb +47 -0
  52. data/lib/alchemrest/error.rb +122 -0
  53. data/lib/alchemrest/factory_bot.rb +64 -0
  54. data/lib/alchemrest/faraday_middleware/external_api_instrumentation.rb +24 -0
  55. data/lib/alchemrest/faraday_middleware/json_parser.rb +30 -0
  56. data/lib/alchemrest/faraday_middleware/kill_switch.rb +22 -0
  57. data/lib/alchemrest/faraday_middleware/underscore_response.rb +24 -0
  58. data/lib/alchemrest/hash_path.rb +81 -0
  59. data/lib/alchemrest/http_request.rb +75 -0
  60. data/lib/alchemrest/kill_switch/adapters.rb +88 -0
  61. data/lib/alchemrest/kill_switch.rb +31 -0
  62. data/lib/alchemrest/railtie.rb +25 -0
  63. data/lib/alchemrest/request/endpoint.rb +29 -0
  64. data/lib/alchemrest/request/returns.rb +46 -0
  65. data/lib/alchemrest/request.rb +80 -0
  66. data/lib/alchemrest/request_definition/builder.rb +13 -0
  67. data/lib/alchemrest/request_definition.rb +26 -0
  68. data/lib/alchemrest/response/pipeline/extract_payload.rb +64 -0
  69. data/lib/alchemrest/response/pipeline/final.rb +11 -0
  70. data/lib/alchemrest/response/pipeline/omit.rb +46 -0
  71. data/lib/alchemrest/response/pipeline/sanitize.rb +59 -0
  72. data/lib/alchemrest/response/pipeline/transform.rb +26 -0
  73. data/lib/alchemrest/response/pipeline/was_successful.rb +29 -0
  74. data/lib/alchemrest/response/pipeline.rb +71 -0
  75. data/lib/alchemrest/response.rb +73 -0
  76. data/lib/alchemrest/response_captured_handler.rb +68 -0
  77. data/lib/alchemrest/result/halt.rb +15 -0
  78. data/lib/alchemrest/result/try_helpers.rb +16 -0
  79. data/lib/alchemrest/result.rb +128 -0
  80. data/lib/alchemrest/root.rb +77 -0
  81. data/lib/alchemrest/transforms/base_to_type_transform_registry.rb +42 -0
  82. data/lib/alchemrest/transforms/constrainable.rb +41 -0
  83. data/lib/alchemrest/transforms/constraint/block.rb +22 -0
  84. data/lib/alchemrest/transforms/constraint/greater_than.rb +19 -0
  85. data/lib/alchemrest/transforms/constraint/greater_than_or_eq.rb +19 -0
  86. data/lib/alchemrest/transforms/constraint/in_list.rb +19 -0
  87. data/lib/alchemrest/transforms/constraint/is_instance_of.rb +19 -0
  88. data/lib/alchemrest/transforms/constraint/is_uuid.rb +19 -0
  89. data/lib/alchemrest/transforms/constraint/less_than.rb +19 -0
  90. data/lib/alchemrest/transforms/constraint/less_than_or_eq.rb +19 -0
  91. data/lib/alchemrest/transforms/constraint/matches_regex.rb +19 -0
  92. data/lib/alchemrest/transforms/constraint/max_length.rb +19 -0
  93. data/lib/alchemrest/transforms/constraint/min_length.rb +19 -0
  94. data/lib/alchemrest/transforms/constraint.rb +17 -0
  95. data/lib/alchemrest/transforms/constraint_builder/for_number.rb +25 -0
  96. data/lib/alchemrest/transforms/constraint_builder/for_string.rb +21 -0
  97. data/lib/alchemrest/transforms/constraint_builder.rb +15 -0
  98. data/lib/alchemrest/transforms/date_transform.rb +30 -0
  99. data/lib/alchemrest/transforms/enum.rb +52 -0
  100. data/lib/alchemrest/transforms/epoch_time.rb +32 -0
  101. data/lib/alchemrest/transforms/from_chain.rb +15 -0
  102. data/lib/alchemrest/transforms/from_number/to_type_transform_registry.rb +18 -0
  103. data/lib/alchemrest/transforms/from_number.rb +47 -0
  104. data/lib/alchemrest/transforms/from_string/to_type_transform_registry.rb +17 -0
  105. data/lib/alchemrest/transforms/from_string.rb +36 -0
  106. data/lib/alchemrest/transforms/from_type/empty_to_type_transform_registry.rb +14 -0
  107. data/lib/alchemrest/transforms/from_type.rb +64 -0
  108. data/lib/alchemrest/transforms/iso_time.rb +58 -0
  109. data/lib/alchemrest/transforms/json_number.rb +26 -0
  110. data/lib/alchemrest/transforms/loose_hash.rb +96 -0
  111. data/lib/alchemrest/transforms/money_transform.rb +42 -0
  112. data/lib/alchemrest/transforms/number.rb +27 -0
  113. data/lib/alchemrest/transforms/output_type.rb +65 -0
  114. data/lib/alchemrest/transforms/to_decimal.rb +22 -0
  115. data/lib/alchemrest/transforms/to_type/from_string_to_time_selector.rb +29 -0
  116. data/lib/alchemrest/transforms/to_type/transforms_selector.rb +61 -0
  117. data/lib/alchemrest/transforms/to_type.rb +86 -0
  118. data/lib/alchemrest/transforms/typed.rb +32 -0
  119. data/lib/alchemrest/transforms/union.rb +44 -0
  120. data/lib/alchemrest/transforms/with_constraint.rb +26 -0
  121. data/lib/alchemrest/transforms.rb +93 -0
  122. data/lib/alchemrest/url_builder/encoders.rb +39 -0
  123. data/lib/alchemrest/url_builder/options.rb +33 -0
  124. data/lib/alchemrest/url_builder.rb +31 -0
  125. data/lib/alchemrest/version.rb +5 -0
  126. data/lib/alchemrest/webmock_helpers.rb +27 -0
  127. data/lib/alchemrest.rb +159 -0
  128. data/lib/generators/alchemrest/kill_switch_migration_generator.rb +27 -0
  129. data/lib/generators/alchemrest/templates/kill_switch_migration.rb.erb +17 -0
  130. data/lib/rubocop/cop/alchemrest/define_request_using_with_params.rb +53 -0
  131. data/lib/rubocop/cop/alchemrest/endpoint_definition_using_generic_params.rb +55 -0
  132. data/lib/rubocop/cop/alchemrest/request_hash_returning_block.rb +54 -0
  133. data/lib/rubocop/cop/alchemrest/time_transform_with_no_zone.rb +56 -0
  134. data/lib/tapioca/dsl/compilers/alchemrest_data.rb +84 -0
  135. data/lib/tapioca/dsl/compilers/alchemrest_root.rb +68 -0
  136. data/mutant.yml +16 -0
  137. data/rbi/alchemrest/result.rbi +80 -0
  138. data/rbi/alchemrest.rbi +246 -0
  139. data/sorbet/config +5 -0
  140. data/sorbet/rbi/gems/.gitattributes +1 -0
  141. data/sorbet/rbi/gems/abstract_type@0.0.7.rbi +41 -0
  142. data/sorbet/rbi/gems/actionpack@8.0.4.rbi +11733 -0
  143. data/sorbet/rbi/gems/actionview@8.0.4.rbi +6560 -0
  144. data/sorbet/rbi/gems/activemodel@8.0.4.rbi +2891 -0
  145. data/sorbet/rbi/gems/activesupport@8.0.4.rbi +9621 -0
  146. data/sorbet/rbi/gems/adamantium@0.2.0.rbi +144 -0
  147. data/sorbet/rbi/gems/addressable@2.8.7.rbi +779 -0
  148. data/sorbet/rbi/gems/anima@0.3.2.rbi +103 -0
  149. data/sorbet/rbi/gems/ast@2.4.2.rbi +107 -0
  150. data/sorbet/rbi/gems/base64@0.3.0.rbi +52 -0
  151. data/sorbet/rbi/gems/benchmark@0.5.0.rbi +153 -0
  152. data/sorbet/rbi/gems/bigdecimal@3.3.1.rbi +77 -0
  153. data/sorbet/rbi/gems/builder@3.3.0.rbi +9 -0
  154. data/sorbet/rbi/gems/circuitbox@2.0.0.rbi +297 -0
  155. data/sorbet/rbi/gems/concord@0.1.6.rbi +51 -0
  156. data/sorbet/rbi/gems/concurrent-ruby@1.3.5.rbi +4716 -0
  157. data/sorbet/rbi/gems/connection_pool@2.5.4.rbi +9 -0
  158. data/sorbet/rbi/gems/crack@1.0.0.rbi +110 -0
  159. data/sorbet/rbi/gems/crass@1.0.6.rbi +294 -0
  160. data/sorbet/rbi/gems/date@3.4.1.rbi +58 -0
  161. data/sorbet/rbi/gems/drb@2.2.3.rbi +639 -0
  162. data/sorbet/rbi/gems/equalizer@0.0.11.rbi +38 -0
  163. data/sorbet/rbi/gems/erubi@1.13.1.rbi +85 -0
  164. data/sorbet/rbi/gems/factory_bot@6.5.0.rbi +1529 -0
  165. data/sorbet/rbi/gems/faraday-em_http@1.0.0.rbi +181 -0
  166. data/sorbet/rbi/gems/faraday-em_synchrony@1.0.1.rbi +120 -0
  167. data/sorbet/rbi/gems/faraday-excon@1.1.0.rbi +128 -0
  168. data/sorbet/rbi/gems/faraday-httpclient@1.0.1.rbi +123 -0
  169. data/sorbet/rbi/gems/faraday-multipart@1.2.0.rbi +190 -0
  170. data/sorbet/rbi/gems/faraday-net_http@1.0.2.rbi +140 -0
  171. data/sorbet/rbi/gems/faraday-net_http_persistent@1.2.0.rbi +116 -0
  172. data/sorbet/rbi/gems/faraday-patron@1.0.0.rbi +119 -0
  173. data/sorbet/rbi/gems/faraday-rack@1.0.0.rbi +113 -0
  174. data/sorbet/rbi/gems/faraday-retry@1.0.3.rbi +149 -0
  175. data/sorbet/rbi/gems/faraday@1.10.5.rbi +1620 -0
  176. data/sorbet/rbi/gems/hansi@0.2.1.rbi +9 -0
  177. data/sorbet/rbi/gems/hashdiff@1.1.2.rbi +174 -0
  178. data/sorbet/rbi/gems/i18n@1.14.7.rbi +1328 -0
  179. data/sorbet/rbi/gems/ice_nine@0.11.2.rbi +145 -0
  180. data/sorbet/rbi/gems/io-console@0.8.0.rbi +9 -0
  181. data/sorbet/rbi/gems/json@2.9.1.rbi +282 -0
  182. data/sorbet/rbi/gems/language_server-protocol@3.17.0.3.rbi +8057 -0
  183. data/sorbet/rbi/gems/logger@1.7.0.rbi +260 -0
  184. data/sorbet/rbi/gems/loofah@2.24.0.rbi +571 -0
  185. data/sorbet/rbi/gems/memoizable@0.4.2.rbi +131 -0
  186. data/sorbet/rbi/gems/memosa@0.8.2.rbi +185 -0
  187. data/sorbet/rbi/gems/minitest@5.26.0.rbi +824 -0
  188. data/sorbet/rbi/gems/money@6.19.0.rbi +815 -0
  189. data/sorbet/rbi/gems/morpher@0.4.2.rbi +388 -0
  190. data/sorbet/rbi/gems/mprelude@0.1.0.rbi +140 -0
  191. data/sorbet/rbi/gems/multi_json@1.15.0.rbi +180 -0
  192. data/sorbet/rbi/gems/multipart-post@2.4.1.rbi +154 -0
  193. data/sorbet/rbi/gems/mustermann-contrib@3.0.3.rbi +9 -0
  194. data/sorbet/rbi/gems/mustermann@3.0.3.rbi +809 -0
  195. data/sorbet/rbi/gems/netrc@0.11.0.rbi +112 -0
  196. data/sorbet/rbi/gems/nokogiri@1.19.1.rbi +3412 -0
  197. data/sorbet/rbi/gems/parallel@1.26.3.rbi +234 -0
  198. data/sorbet/rbi/gems/parser@3.3.7.0.rbi +4877 -0
  199. data/sorbet/rbi/gems/pp@0.6.2.rbi +176 -0
  200. data/sorbet/rbi/gems/prettyprint@0.2.0.rbi +155 -0
  201. data/sorbet/rbi/gems/prism@1.5.1.rbi +26368 -0
  202. data/sorbet/rbi/gems/procto@0.0.3.rbi +9 -0
  203. data/sorbet/rbi/gems/psych@5.2.3.rbi +806 -0
  204. data/sorbet/rbi/gems/public_suffix@6.0.1.rbi +267 -0
  205. data/sorbet/rbi/gems/racc@1.8.1.rbi +120 -0
  206. data/sorbet/rbi/gems/rack-session@2.1.1.rbi +458 -0
  207. data/sorbet/rbi/gems/rack-test@2.2.0.rbi +405 -0
  208. data/sorbet/rbi/gems/rack@3.1.14.rbi +2774 -0
  209. data/sorbet/rbi/gems/rackup@2.2.1.rbi +132 -0
  210. data/sorbet/rbi/gems/rails-dom-testing@2.2.0.rbi +266 -0
  211. data/sorbet/rbi/gems/rails-html-sanitizer@1.6.2.rbi +545 -0
  212. data/sorbet/rbi/gems/railties@8.0.4.rbi +2150 -0
  213. data/sorbet/rbi/gems/rainbow@3.1.1.rbi +333 -0
  214. data/sorbet/rbi/gems/rake@13.2.1.rbi +2054 -0
  215. data/sorbet/rbi/gems/rbi@0.2.3.rbi +3961 -0
  216. data/sorbet/rbi/gems/rdoc@6.13.1.rbi +6784 -0
  217. data/sorbet/rbi/gems/regexp_parser@2.11.3.rbi +3020 -0
  218. data/sorbet/rbi/gems/reline@0.6.0.rbi +9 -0
  219. data/sorbet/rbi/gems/rexml@3.4.2.rbi +1777 -0
  220. data/sorbet/rbi/gems/rubocop-ast@1.38.0.rbi +5293 -0
  221. data/sorbet/rbi/gems/rubocop@1.71.1.rbi +31846 -0
  222. data/sorbet/rbi/gems/ruby-progressbar@1.13.0.rbi +980 -0
  223. data/sorbet/rbi/gems/ruby2_keywords@0.0.5.rbi +9 -0
  224. data/sorbet/rbi/gems/securerandom@0.4.1.rbi +33 -0
  225. data/sorbet/rbi/gems/sentry-ruby@5.22.1.rbi +3782 -0
  226. data/sorbet/rbi/gems/spoom@1.5.1.rbi +4321 -0
  227. data/sorbet/rbi/gems/stringio@3.1.2.rbi +9 -0
  228. data/sorbet/rbi/gems/tapioca@0.16.8.rbi +3399 -0
  229. data/sorbet/rbi/gems/thor@1.3.2.rbi +2012 -0
  230. data/sorbet/rbi/gems/thread_safe@0.3.6.rbi +711 -0
  231. data/sorbet/rbi/gems/timeout@0.4.4.rbi +80 -0
  232. data/sorbet/rbi/gems/tsort@0.2.0.rbi +50 -0
  233. data/sorbet/rbi/gems/tzinfo@2.0.6.rbi +1677 -0
  234. data/sorbet/rbi/gems/unicode-display_width@2.6.0.rbi +62 -0
  235. data/sorbet/rbi/gems/uri@1.1.0.rbi +760 -0
  236. data/sorbet/rbi/gems/useragent@0.16.11.rbi +9 -0
  237. data/sorbet/rbi/gems/webmock@3.24.0.rbi +1362 -0
  238. data/sorbet/rbi/gems/yard-sorbet@0.9.0.rbi +345 -0
  239. data/sorbet/rbi/gems/yard@0.9.37.rbi +8795 -0
  240. data/sorbet/rbi/gems/zeitwerk@2.7.1.rbi +589 -0
  241. data/sorbet/tapioca/config.yml +45 -0
  242. data/sorbet/tapioca/require.rb +8 -0
  243. data/sorbet/tapioca/sorbet/rbi/dsl/.gitattributes +1 -0
  244. data/sorbet/tapioca/sorbet/rbi/dsl/active_support/callbacks.rbi +23 -0
  245. metadata +737 -0
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ class Data
5
+ # This class has two purposes. First it provides the DSL that allows developers to mark fields on a
6
+ # Alchemrest::Data class as safe or omitted. Second, it walks the entire object graph of nested
7
+ # objects to build up full path definitions for all the safe an omitted fields defined on child
8
+ # objects as well.
9
+ #
10
+ # @example Using `Alchemrest::Data::CaptureConfiguration` to delete define some safe and omitted fields
11
+ # CaptureConfiguration.new(BankApi::Data::User) do
12
+ # safe :name
13
+ # omitted :age
14
+ # end
15
+ #
16
+ # The attribute `path_to_payload` allows us to deal with situations where the domain data defined
17
+ # by the host class is nested inside a larger payload. For example `{ data: { user: { user data } } }`
18
+ class CaptureConfiguration
19
+ attr_reader :safe_keys, :omitted_keys, :path_to_payload
20
+
21
+ def initialize(host_class:, path_to_payload: nil, omitted_keys: [], safe_keys: [], &block)
22
+ raise ArgumentError, "Must be a sub class of Alchemrest::Data" unless host_class < Alchemrest::Data
23
+
24
+ @host = host_class
25
+ @path_to_payload = path_to_payload
26
+ @safe_keys = safe_keys
27
+ @omitted_keys = omitted_keys
28
+ instance_eval(&block) unless block.nil?
29
+ end
30
+
31
+ def with_path_to_payload(path_to_payload)
32
+ self.class.new(
33
+ host_class: @host,
34
+ path_to_payload:,
35
+ safe_keys:,
36
+ omitted_keys:,
37
+ )
38
+ end
39
+
40
+ def safe_paths
41
+ paths = @safe_keys.dup
42
+
43
+ paths.concat(get_child_paths(:safe_paths))
44
+ paths.map { |path| prefix_with_path_to_payload_nodes(path) }
45
+ end
46
+
47
+ def omitted_paths
48
+ paths = @omitted_keys.dup
49
+
50
+ paths.concat(get_child_paths(:omitted_paths))
51
+ paths.map { |path| prefix_with_path_to_payload_nodes(path) }
52
+ end
53
+
54
+ def safe(*keys)
55
+ @safe_keys = keys
56
+ end
57
+
58
+ def omitted(*keys)
59
+ @omitted_keys = keys
60
+ end
61
+
62
+ private
63
+
64
+ def get_child_paths(type)
65
+ paths = []
66
+ @host.graph.sub_graphs.each do |key, child|
67
+ paths << { key => child.type.capture_configuration.public_send(type) }
68
+ end
69
+ paths
70
+ end
71
+
72
+ def prefix_with_path_to_payload_nodes(path)
73
+ Array(path_to_payload).reverse.reduce(path) { |child, key| { key => child } }
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ class Data
5
+ class Field
6
+ include Anima.new(:transform, :name, :required)
7
+
8
+ def initialize(transform:, name:, required: false)
9
+ unless transform.is_a?(Morpher::Transform) || transform.is_a?(Data)
10
+ raise ArgumentError, "transform must be an instance of Morpher::Transform or Alchemrest::Data, not #{transform.class}"
11
+ end
12
+ raise ArgumentError, "must provide name" unless name
13
+ raise ArgumentError, "must provide non-empty name" if name.to_s.strip.empty?
14
+
15
+ @transform = transform
16
+ @name = name.to_s
17
+ @required = required && !transform.instance_of?(Morpher::Transform::Maybe)
18
+ end
19
+
20
+ def output_type
21
+ base_type = transform.output_type if transform.respond_to?(:output_type)
22
+ return unless base_type
23
+
24
+ if required
25
+ base_type
26
+ else
27
+ base_type.with(sorbet_type: T.nilable(base_type.sorbet_type))
28
+ end
29
+ end
30
+
31
+ def constraints
32
+ output_type&.constraints || []
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ class Data
5
+ # A simple data structure to hold a graph of all the objects in a schema.
6
+ class Graph
7
+ include Anima.new(:type, :sub_graphs, :fields)
8
+
9
+ def initialize(type:, sub_graphs: nil, fields: nil)
10
+ unless type < Data
11
+ raise ArgumentError, "Graph types must be branching Alchemrest::Data class"
12
+ end
13
+ unless sub_graphs.nil? || sub_graph_hash_has_correct_types?(sub_graphs:)
14
+ raise ArgumentError, "sub_graphs must be a hash of graphs"
15
+ end
16
+ unless fields.nil? || fields_hash_has_correct_types?(fields:)
17
+ raise ArgumentError, "fields must be a hash of fields"
18
+ end
19
+
20
+ @type = type
21
+ @fields = fields
22
+ @sub_graphs = sub_graphs
23
+ end
24
+
25
+ def children
26
+ sub_graphs
27
+ end
28
+
29
+ private
30
+
31
+ def sub_graph_hash_has_correct_types?(sub_graphs:)
32
+ sub_graphs.instance_of?(Hash) && sub_graphs.values.all? { |graph| graph.instance_of?(Graph) }
33
+ end
34
+
35
+ def fields_hash_has_correct_types?(fields:)
36
+ fields.instance_of?(Hash) && fields.values.all? { |graph| graph.instance_of?(Field) }
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ class Data
5
+ class Record < Module
6
+ DEFAULTS = {
7
+ required: Morpher::EMPTY_HASH,
8
+ optional: Morpher::EMPTY_HASH,
9
+ allow_additional_properties: true,
10
+ }.freeze
11
+
12
+ include Anima.new(:required, :optional, :allow_additional_properties)
13
+
14
+ def self.new(**attributes)
15
+ super(**DEFAULTS.merge(attributes))
16
+ end
17
+
18
+ # rubocop:disable Metrics/AbcSize
19
+ def included(host)
20
+ optional = optional()
21
+ optional_transform = transform(optional)
22
+ required = required()
23
+ required_transform = transform(required)
24
+ additional_properties_allowed = allow_additional_properties
25
+
26
+ host.class_eval do
27
+ include Anima.new(*(required.keys + optional.keys))
28
+
29
+ const_set(
30
+ :TRANSFORM,
31
+ Transforms::Typed.new(
32
+ transform: Morpher::Transform::Sequence.new(
33
+ [
34
+ Morpher::Transform::Primitive.new(Hash),
35
+ Morpher::Transform::Hash::Symbolize.new,
36
+ Transforms::LooseHash.new(
37
+ required: required_transform,
38
+ optional: optional_transform,
39
+ allow_additional_properties: additional_properties_allowed,
40
+ ),
41
+ Morpher::Transform::Success.new(public_method(:new)),
42
+ ],
43
+ ),
44
+ output_type: Transforms::OutputType.simple(host),
45
+ ),
46
+ )
47
+ end
48
+ end
49
+ # rubocop:enable Metrics/AbcSize
50
+
51
+ private
52
+
53
+ def transform(attributes)
54
+ attributes.map do |name, transform|
55
+ Morpher::Transform::Hash::Key.new(name, transform)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ class Data
5
+ class Schema < Alchemrest::Data::Record
6
+ def included(host)
7
+ super
8
+
9
+ host.extend(ClassMethods)
10
+
11
+ host.const_set(
12
+ :Collection,
13
+ Class.new(host) do
14
+ const_set :TRANSFORM, superclass::TRANSFORM.array
15
+
16
+ def self.capture_configuration
17
+ superclass.capture_configuration
18
+ end
19
+ end,
20
+ )
21
+
22
+ # Store the graph as a constant for easy access from other classes
23
+ host.const_set(:GRAPH, extract_graph(host))
24
+ end
25
+
26
+ private
27
+
28
+ def extract_graph(host)
29
+ Graph.new(
30
+ type: host,
31
+ sub_graphs: extract_sub_graphs,
32
+ fields: extract_fields,
33
+ )
34
+ end
35
+
36
+ def extract_sub_graphs
37
+ required.merge(optional)
38
+ .filter { |_k, v| v.respond_to?(:output_type) && v.output_type.graph }
39
+ .transform_values { |v| v.output_type.graph }
40
+ end
41
+
42
+ def extract_fields
43
+ required_fields = required.map { |k, v| [k, Field.new(transform: v, name: k, required: true)] }
44
+ optional_fields = optional.map { |k, v| [k, Field.new(transform: v, name: k)] }
45
+ (required_fields + optional_fields).to_h
46
+ end
47
+
48
+ module ClassMethods
49
+ def capture_configuration
50
+ @capture_configuration ||= CaptureConfiguration.new(host_class: self)
51
+ end
52
+
53
+ def response_transformer
54
+ Alchemrest::ResponseTransformers::Morpher.new(self::TRANSFORM)
55
+ end
56
+
57
+ def from_hash(hash)
58
+ transform_prelude = self::TRANSFORM.call(hash.deep_stringify_keys)
59
+ if transform_prelude.right?
60
+ transform_prelude.from_right
61
+ else
62
+ raise MorpherTransformError, transform_prelude.from_left
63
+ end
64
+ end
65
+
66
+ def []
67
+ self::Collection
68
+ end
69
+
70
+ def graph
71
+ self::GRAPH
72
+ end
73
+
74
+ def configure_response_capture(&)
75
+ @capture_configuration = CaptureConfiguration.new(host_class: self, &)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ class Data
5
+ def self.schema
6
+ include Alchemrest::Data::Schema.new(**(yield Alchemrest::Transforms))
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ class EndpointDefinition
5
+ VALID_HTTP_METHODS = IceNine.deep_freeze(%w(get post put delete head patch options trace).to_set)
6
+ private_constant(:VALID_HTTP_METHODS)
7
+
8
+ include Anima.new(:template, :http_method, :builder_block)
9
+
10
+ def initialize(template:, http_method:, builder_block:)
11
+ super
12
+ raise ArgumentError, "template must be string" unless template.instance_of?(String)
13
+
14
+ raise ArgumentError, "missing template" if template.empty?
15
+
16
+ downcased_http_method = http_method.downcase
17
+ raise ArgumentError, "must provide a valid HTTP method" unless VALID_HTTP_METHODS.include?(downcased_http_method)
18
+ end
19
+
20
+ def url_for(context)
21
+ build_for(context).url
22
+ end
23
+
24
+ def params_for(context)
25
+ builder = build_for(context)
26
+ if builder.params
27
+ builder.params
28
+ elsif builder.values || builder.query
29
+ (builder.values || {}).merge(builder.query || {}).transform_values { |v| CGI.escape(v.to_s) }
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def build_for(context)
36
+ options = UrlBuilder::Options.new
37
+ if builder_block&.arity&.zero?
38
+ Alchemrest.deprecator.warn HASH_RETURNING_BLOCK_MESSAGE
39
+ options.params = context.instance_exec(&builder_block)
40
+ elsif builder_block
41
+ context.instance_exec(options, &builder_block)
42
+ end
43
+
44
+ options.create_builder(template:)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ class Error < StandardError
5
+ def deconstruct
6
+ [to_s]
7
+ end
8
+
9
+ def deconstruct_keys(_keys)
10
+ { error: to_s }
11
+ end
12
+ end
13
+
14
+ class ResponseError < Error
15
+ attr_reader :response
16
+
17
+ def initialize(response)
18
+ super
19
+ @response = response
20
+ end
21
+
22
+ def to_s
23
+ method = response.env.method
24
+ url = response.env.url
25
+ [
26
+ "HTTP #{response.status} for #{method.upcase} #{url}",
27
+ response.error_details.to_s,
28
+ ].compact.join(" - ")
29
+ end
30
+
31
+ def deconstruct
32
+ [response.status, response.error_details]
33
+ end
34
+
35
+ def deconstruct_keys(_keys)
36
+ { status: response.status, error: response.error_details }
37
+ end
38
+ end
39
+
40
+ class ServerError < ResponseError
41
+ attr_reader :response, :cause
42
+
43
+ def initialize(response, cause = nil)
44
+ @response = response
45
+ @cause = cause
46
+ super(response)
47
+ end
48
+
49
+ def to_s
50
+ if response.circuit_open?
51
+ "CIRCUIT OPEN"
52
+ elsif response.timeout?
53
+ "#{response.status} timeout"
54
+ else
55
+ error_message
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def error_message
62
+ method = response.env.method
63
+ url = response.env.url
64
+ [
65
+ "HTTP #{response.status} for #{method.upcase} #{url}",
66
+ response.error_details.to_s,
67
+ cause,
68
+ ].compact.join(" - ")
69
+ end
70
+ end
71
+
72
+ class ClientError < ResponseError; end
73
+
74
+ class AuthError < ClientError; end
75
+
76
+ class NotFoundError < ClientError; end
77
+
78
+ class UndefinedClientError < Error; end
79
+
80
+ class RequestFailedError < Error; end
81
+
82
+ class TimeoutError < RequestFailedError; end
83
+
84
+ class TransformError < Error; end
85
+
86
+ class ResultRescued < Error; end
87
+
88
+ class CircuitOpenError < Error; end
89
+
90
+ class MorpherTransformError < Error
91
+ attr_reader :error
92
+
93
+ def initialize(error)
94
+ @error = error
95
+ super()
96
+ end
97
+
98
+ def to_s
99
+ "Response does not match expected schema - #{error.compact_message}"
100
+ end
101
+ end
102
+
103
+ class ResponsePipelineError < Error; end
104
+
105
+ class InvalidConfigurationError < Error; end
106
+
107
+ class ConfigurationNotReadyError < Error; end
108
+
109
+ class KillSwitchEnabledError < Error; end
110
+
111
+ class NoRegisteredTransformError < Error
112
+ def initialize(from:, to:)
113
+ super("No registered transform for #{from.output_type_name} -> #{to}")
114
+ end
115
+ end
116
+
117
+ class NoTransformOptionForNameError < Error
118
+ def initialize(from:, to:, name:, options:)
119
+ super("No transform named #{name} for #{from.output_type_name} -> #{to}. Available options: #{options.keys.join(', ')}")
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module FactoryBot
5
+ def self.enable!
6
+ ::FactoryBot.register_strategy(:alchemrest_record_for, Alchemrest::FactoryBot::AlchemrestStrategy)
7
+ ::FactoryBot.register_strategy(:alchemrest_hash_for, Alchemrest::FactoryBot::HashStrategy)
8
+
9
+ ::FactoryBot::Syntax::Default::DSL.class_eval do
10
+ include Alchemrest::FactoryBot::Mixins
11
+ end
12
+ end
13
+
14
+ class OmitKey
15
+ include Singleton
16
+ end
17
+
18
+ module Mixins
19
+ def alchemrest_factory(name, options = {}, &block)
20
+ raise ArgumentError, "You must specify a class" unless options[:class]
21
+ raise ArgumentError, "You must specify a block" unless block
22
+
23
+ factory(name, options) do
24
+ skip_create
25
+
26
+ initialize_with do
27
+ provided_attributes = attributes.reject { |_, v| v.is_a?(Alchemrest::FactoryBot::OmitKey) }
28
+ options[:class].constantize.from_hash(provided_attributes)
29
+ end
30
+
31
+ instance_eval(&block)
32
+ end
33
+ end
34
+ end
35
+
36
+ class AlchemrestStrategy
37
+ def association(runner)
38
+ runner.run(:alchemrest_hash_for)
39
+ end
40
+
41
+ def result(evaluation)
42
+ evaluation.object
43
+ end
44
+
45
+ def to_sym
46
+ :alchemrest_record_for
47
+ end
48
+ end
49
+
50
+ class HashStrategy
51
+ def association(runner)
52
+ runner.run(:alchemrest_hash_for)
53
+ end
54
+
55
+ def result(evaluation)
56
+ evaluation.hash.reject { |_, v| v.is_a?(Alchemrest::FactoryBot::OmitKey) }
57
+ end
58
+
59
+ def to_sym
60
+ :alchemrest_hash_for
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module FaradayMiddleware
5
+ class ExternalApiInstrumentation < Faraday::Middleware
6
+ attr_reader :external_service
7
+
8
+ def initialize(app, options = {})
9
+ super(app)
10
+ @external_service = options.fetch(:external_service)
11
+ end
12
+
13
+ def call(env)
14
+ ::ActiveSupport::Notifications.instrument(name, env: env, external_service: external_service) do
15
+ @app.call(env)
16
+ end
17
+ end
18
+
19
+ def name
20
+ "#{external_service}_api_request"
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module FaradayMiddleware
5
+ class JsonParser < Faraday::Middleware
6
+ DEFAULT_RESPONSE = '{}'
7
+
8
+ def on_complete(env)
9
+ # stash the body for debugging
10
+ env[:raw_body] = env.body
11
+
12
+ env.body = if env.parse_body?
13
+ parse_body(env.body)
14
+ else
15
+ {}
16
+ end
17
+ rescue MultiJson::ParseError
18
+ env.body = {}
19
+ end
20
+
21
+ def parse_body(raw_body)
22
+ MultiJson.load(parseable_body(raw_body))
23
+ end
24
+
25
+ def parseable_body(raw_body)
26
+ raw_body.presence || DEFAULT_RESPONSE
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module FaradayMiddleware
5
+ class KillSwitch < Faraday::Middleware
6
+ attr_reader :service_name
7
+
8
+ def initialize(app, options = {})
9
+ @service_name = options.fetch(:service_name)
10
+ super(app)
11
+ end
12
+
13
+ def call(env)
14
+ kill_switch = Alchemrest::KillSwitch.new(service_name:)
15
+
16
+ raise KillSwitchEnabledError if kill_switch.active?
17
+
18
+ app.call(env)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module FaradayMiddleware
5
+ class UnderScoreResponse < Faraday::Middleware
6
+ def on_complete(env)
7
+ env.body = deep_transform_keys!(env.body)
8
+ end
9
+
10
+ private
11
+
12
+ def deep_transform_keys!(object)
13
+ case object
14
+ when Array
15
+ object.each { |item| deep_transform_keys!(item) }
16
+ when Hash
17
+ object.deep_transform_keys! { |k| k.to_s.underscore }
18
+ else
19
+ object
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end