shopify-cli 1.3.0 → 1.6.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 (206) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +2 -2
  3. data/.github/CONTRIBUTING.md +9 -1
  4. data/.github/PULL_REQUEST_TEMPLATE.md +10 -1
  5. data/.github/workflows/release.yml +61 -0
  6. data/.github/workflows/triage.yml +22 -0
  7. data/.gitignore +0 -1
  8. data/.rubocop.yml +61 -8
  9. data/.rubocop_todo.yml +11 -0
  10. data/.travis.yml +1 -0
  11. data/CHANGELOG.md +30 -0
  12. data/Gemfile +3 -2
  13. data/Gemfile.lock +39 -37
  14. data/README.md +39 -7
  15. data/RELEASING.md +19 -29
  16. data/Rakefile +2 -0
  17. data/dev.yml +2 -2
  18. data/docs/_config.yml +1 -18
  19. data/docs/app/node/commands/index.md +2 -80
  20. data/docs/app/node/index.md +2 -33
  21. data/docs/app/rails/commands/index.md +2 -78
  22. data/docs/app/rails/index.md +2 -34
  23. data/docs/core/index.md +2 -84
  24. data/docs/getting-started/index.md +2 -25
  25. data/docs/getting-started/install/index.md +1 -118
  26. data/docs/getting-started/migrate/index.md +2 -94
  27. data/docs/getting-started/uninstall/index.md +2 -35
  28. data/docs/getting-started/upgrade/index.md +2 -39
  29. data/docs/help/start-app/index.md +2 -4
  30. data/docs/index.md +2 -24
  31. data/install.sh +1 -1
  32. data/lib/project_types/extension/cli.rb +21 -11
  33. data/lib/project_types/extension/commands/extension_command.rb +2 -2
  34. data/lib/project_types/extension/features/argo.rb +117 -0
  35. data/lib/project_types/extension/forms/create.rb +2 -2
  36. data/lib/project_types/extension/models/specification.rb +35 -0
  37. data/lib/project_types/extension/models/specification_handlers/checkout_post_purchase.rb +19 -0
  38. data/lib/project_types/extension/models/specification_handlers/default.rb +67 -0
  39. data/lib/project_types/extension/models/specifications.rb +77 -0
  40. data/lib/project_types/extension/tasks/configure_features.rb +52 -0
  41. data/lib/project_types/extension/tasks/fetch_specifications.rb +38 -0
  42. data/lib/project_types/node/cli.rb +4 -1
  43. data/lib/project_types/node/commands/connect.rb +15 -0
  44. data/lib/project_types/node/commands/create.rb +10 -4
  45. data/lib/project_types/node/commands/generate.rb +2 -11
  46. data/lib/project_types/node/messages/messages.rb +16 -50
  47. data/lib/project_types/rails/cli.rb +4 -1
  48. data/lib/project_types/rails/commands/connect.rb +15 -0
  49. data/lib/project_types/rails/commands/create.rb +15 -12
  50. data/lib/project_types/rails/forms/create.rb +1 -1
  51. data/lib/project_types/rails/gem.rb +1 -1
  52. data/lib/project_types/rails/messages/messages.rb +8 -5
  53. data/lib/project_types/script/cli.rb +9 -5
  54. data/lib/project_types/script/commands/create.rb +6 -4
  55. data/lib/project_types/script/commands/enable.rb +12 -4
  56. data/lib/project_types/script/commands/push.rb +5 -13
  57. data/lib/project_types/script/config/extension_points.yml +17 -12
  58. data/lib/project_types/script/errors.rb +21 -0
  59. data/lib/project_types/script/forms/create.rb +26 -2
  60. data/lib/project_types/script/graphql/app_script_update_or_create.graphql +10 -1
  61. data/lib/project_types/script/layers/application/build_script.rb +18 -17
  62. data/lib/project_types/script/layers/application/create_script.rb +12 -10
  63. data/lib/project_types/script/layers/application/extension_points.rb +24 -0
  64. data/lib/project_types/script/layers/application/push_script.rb +18 -16
  65. data/lib/project_types/script/layers/domain/errors.rb +7 -0
  66. data/lib/project_types/script/layers/domain/extension_point.rb +62 -7
  67. data/lib/project_types/script/layers/domain/metadata.rb +55 -0
  68. data/lib/project_types/script/layers/domain/push_package.rb +25 -6
  69. data/lib/project_types/script/layers/infrastructure/assemblyscript_project_creator.rb +17 -52
  70. data/lib/project_types/script/layers/infrastructure/assemblyscript_task_runner.rb +42 -11
  71. data/lib/project_types/script/layers/infrastructure/errors.rb +16 -0
  72. data/lib/project_types/script/layers/infrastructure/extension_point_repository.rb +10 -4
  73. data/lib/project_types/script/layers/infrastructure/project_creator.rb +2 -1
  74. data/lib/project_types/script/layers/infrastructure/push_package_repository.rb +25 -13
  75. data/lib/project_types/script/layers/infrastructure/rust_project_creator.rb +72 -0
  76. data/lib/project_types/script/layers/infrastructure/rust_task_runner.rb +59 -0
  77. data/lib/project_types/script/layers/infrastructure/script_service.rb +9 -1
  78. data/lib/project_types/script/layers/infrastructure/task_runner.rb +4 -3
  79. data/lib/project_types/script/messages/messages.rb +55 -4
  80. data/lib/project_types/script/script_project.rb +25 -16
  81. data/lib/project_types/script/ui/error_handler.rb +59 -1
  82. data/lib/project_types/theme/cli.rb +40 -0
  83. data/lib/project_types/theme/commands/connect.rb +54 -0
  84. data/lib/project_types/theme/commands/create.rb +48 -0
  85. data/lib/project_types/theme/commands/deploy.rb +38 -0
  86. data/lib/project_types/theme/commands/generate.rb +20 -0
  87. data/lib/project_types/theme/commands/generate/env.rb +79 -0
  88. data/lib/project_types/theme/commands/push.rb +55 -0
  89. data/lib/project_types/theme/commands/serve.rb +31 -0
  90. data/lib/project_types/theme/forms/connect.rb +34 -0
  91. data/lib/project_types/theme/forms/create.rb +22 -0
  92. data/lib/project_types/theme/messages/messages.rb +147 -0
  93. data/lib/project_types/theme/tasks/ensure_themekit_installed.rb +78 -0
  94. data/lib/project_types/theme/themekit.rb +113 -0
  95. data/lib/shopify-cli/admin_api.rb +42 -2
  96. data/lib/shopify-cli/api.rb +34 -33
  97. data/lib/shopify-cli/commands/config.rb +24 -0
  98. data/lib/shopify-cli/commands/connect.rb +32 -15
  99. data/lib/shopify-cli/commands/system.rb +10 -1
  100. data/lib/shopify-cli/context.rb +23 -2
  101. data/lib/shopify-cli/core/entry_point.rb +1 -1
  102. data/lib/shopify-cli/core/monorail.rb +6 -4
  103. data/lib/shopify-cli/feature.rb +0 -2
  104. data/lib/shopify-cli/http_request.rb +27 -0
  105. data/lib/shopify-cli/js_deps.rb +1 -1
  106. data/lib/shopify-cli/messages/messages.rb +31 -7
  107. data/lib/shopify-cli/method_object.rb +104 -0
  108. data/lib/shopify-cli/partners_api.rb +25 -3
  109. data/lib/shopify-cli/process_supervision.rb +1 -1
  110. data/lib/shopify-cli/project.rb +12 -8
  111. data/lib/shopify-cli/project_type.rb +18 -2
  112. data/lib/shopify-cli/resolve_constant.rb +25 -0
  113. data/lib/shopify-cli/result.rb +432 -0
  114. data/lib/shopify-cli/shopifolk.rb +87 -0
  115. data/lib/shopify-cli/task.rb +8 -0
  116. data/lib/shopify-cli/tasks/create_api_client.rb +13 -2
  117. data/lib/shopify-cli/tasks/ensure_env.rb +3 -0
  118. data/lib/shopify-cli/tasks/select_org_and_shop.rb +10 -5
  119. data/lib/shopify-cli/tunnel.rb +8 -2
  120. data/lib/shopify-cli/version.rb +1 -1
  121. data/lib/shopify_cli.rb +5 -1
  122. data/shopify.fish +1 -1
  123. data/shopify.sh +1 -1
  124. data/vendor/deps/cli-kit/REVISION +1 -1
  125. data/vendor/deps/cli-kit/lib/cli/kit/logger.rb +2 -2
  126. data/vendor/deps/cli-kit/lib/cli/kit/system.rb +3 -3
  127. data/vendor/deps/cli-ui/REVISION +1 -1
  128. data/vendor/deps/cli-ui/lib/cli/ui.rb +26 -22
  129. data/vendor/deps/cli-ui/lib/cli/ui/ansi.rb +4 -6
  130. data/vendor/deps/cli-ui/lib/cli/ui/frame.rb +3 -3
  131. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_stack.rb +8 -9
  132. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style.rb +1 -1
  133. data/vendor/deps/cli-ui/lib/cli/ui/glyph.rb +1 -0
  134. data/vendor/deps/cli-ui/lib/cli/ui/printer.rb +15 -3
  135. data/vendor/deps/cli-ui/lib/cli/ui/prompt/interactive_options.rb +4 -11
  136. data/vendor/deps/cli-ui/lib/cli/ui/spinner.rb +3 -5
  137. data/vendor/deps/cli-ui/lib/cli/ui/terminal.rb +10 -10
  138. data/vendor/deps/cli-ui/lib/cli/ui/version.rb +1 -1
  139. data/vendor/deps/cli-ui/lib/cli/ui/wrap.rb +56 -0
  140. data/vendor/deps/webrick/.gitignore +9 -0
  141. data/vendor/deps/webrick/Gemfile +3 -0
  142. data/vendor/deps/webrick/LICENSE.txt +22 -0
  143. data/vendor/deps/webrick/README.md +61 -0
  144. data/vendor/deps/webrick/Rakefile +10 -0
  145. data/vendor/deps/webrick/lib/webrick.rb +232 -0
  146. data/vendor/deps/webrick/lib/webrick/accesslog.rb +157 -0
  147. data/vendor/deps/webrick/lib/webrick/cgi.rb +313 -0
  148. data/vendor/deps/webrick/lib/webrick/compat.rb +36 -0
  149. data/vendor/deps/webrick/lib/webrick/config.rb +158 -0
  150. data/vendor/deps/webrick/lib/webrick/cookie.rb +172 -0
  151. data/vendor/deps/webrick/lib/webrick/htmlutils.rb +30 -0
  152. data/vendor/deps/webrick/lib/webrick/httpauth.rb +96 -0
  153. data/vendor/deps/webrick/lib/webrick/httpauth/authenticator.rb +117 -0
  154. data/vendor/deps/webrick/lib/webrick/httpauth/basicauth.rb +116 -0
  155. data/vendor/deps/webrick/lib/webrick/httpauth/digestauth.rb +395 -0
  156. data/vendor/deps/webrick/lib/webrick/httpauth/htdigest.rb +132 -0
  157. data/vendor/deps/webrick/lib/webrick/httpauth/htgroup.rb +97 -0
  158. data/vendor/deps/webrick/lib/webrick/httpauth/htpasswd.rb +158 -0
  159. data/vendor/deps/webrick/lib/webrick/httpauth/userdb.rb +53 -0
  160. data/vendor/deps/webrick/lib/webrick/httpproxy.rb +354 -0
  161. data/vendor/deps/webrick/lib/webrick/httprequest.rb +636 -0
  162. data/vendor/deps/webrick/lib/webrick/httpresponse.rb +564 -0
  163. data/vendor/deps/webrick/lib/webrick/https.rb +152 -0
  164. data/vendor/deps/webrick/lib/webrick/httpserver.rb +294 -0
  165. data/vendor/deps/webrick/lib/webrick/httpservlet.rb +23 -0
  166. data/vendor/deps/webrick/lib/webrick/httpservlet/abstract.rb +152 -0
  167. data/vendor/deps/webrick/lib/webrick/httpservlet/cgi_runner.rb +47 -0
  168. data/vendor/deps/webrick/lib/webrick/httpservlet/cgihandler.rb +126 -0
  169. data/vendor/deps/webrick/lib/webrick/httpservlet/erbhandler.rb +88 -0
  170. data/vendor/deps/webrick/lib/webrick/httpservlet/filehandler.rb +552 -0
  171. data/vendor/deps/webrick/lib/webrick/httpservlet/prochandler.rb +47 -0
  172. data/vendor/deps/webrick/lib/webrick/httpstatus.rb +194 -0
  173. data/vendor/deps/webrick/lib/webrick/httputils.rb +512 -0
  174. data/vendor/deps/webrick/lib/webrick/httpversion.rb +76 -0
  175. data/vendor/deps/webrick/lib/webrick/log.rb +156 -0
  176. data/vendor/deps/webrick/lib/webrick/server.rb +381 -0
  177. data/vendor/deps/webrick/lib/webrick/ssl.rb +215 -0
  178. data/vendor/deps/webrick/lib/webrick/utils.rb +265 -0
  179. data/vendor/deps/webrick/lib/webrick/version.rb +18 -0
  180. data/vendor/deps/webrick/webrick.gemspec +74 -0
  181. metadata +77 -27
  182. data/docs/Gemfile +0 -5
  183. data/docs/Gemfile.lock +0 -258
  184. data/docs/_data/nav.yml +0 -35
  185. data/docs/_includes/footer.html +0 -15
  186. data/docs/_includes/head.html +0 -19
  187. data/docs/_includes/sidebar_nav.html +0 -22
  188. data/docs/_includes/toc.html +0 -112
  189. data/docs/_layouts/default.html +0 -79
  190. data/docs/css/docs.css +0 -157
  191. data/docs/images/header.png +0 -0
  192. data/docs/installing-ruby.md +0 -28
  193. data/lib/project_types/extension/features/argo/admin.rb +0 -20
  194. data/lib/project_types/extension/features/argo/base.rb +0 -129
  195. data/lib/project_types/extension/features/argo/checkout.rb +0 -20
  196. data/lib/project_types/extension/models/type.rb +0 -81
  197. data/lib/project_types/extension/models/types/checkout_post_purchase.rb +0 -23
  198. data/lib/project_types/extension/models/types/product_subscription.rb +0 -24
  199. data/lib/project_types/node/commands/generate/billing.rb +0 -39
  200. data/lib/project_types/node/commands/generate/page.rb +0 -59
  201. data/lib/project_types/node/commands/generate/webhook.rb +0 -37
  202. data/lib/project_types/script/layers/domain/script.rb +0 -18
  203. data/lib/project_types/script/layers/infrastructure/assemblyscript_tsconfig.rb +0 -38
  204. data/lib/project_types/script/layers/infrastructure/script_repository.rb +0 -59
  205. data/lib/project_types/script/templates/ts/as-pect.config.js +0 -27
  206. data/lib/project_types/script/templates/ts/as-pect.d.ts +0 -1
@@ -45,8 +45,11 @@ module ShopifyCli
45
45
  File.join(ShopifyCli::PROJECT_TYPES_DIR, project_type.to_s, path)
46
46
  end
47
47
 
48
- def creator(name, command_const)
48
+ def title(name)
49
49
  @project_name = name
50
+ end
51
+
52
+ def creator(command_const)
50
53
  @project_creator_command_class = command_const
51
54
  ShopifyCli::Commands::Create.subcommand(command_const, @project_type)
52
55
  end
@@ -55,6 +58,19 @@ module ShopifyCli
55
58
  const_get(@project_creator_command_class)
56
59
  end
57
60
 
61
+ def connector(command_const)
62
+ @project_connector_command_class = command_const
63
+ ShopifyCli::Commands::Connect.subcommand(command_const, @project_type)
64
+ end
65
+
66
+ def connect_command
67
+ if @project_connector_command_class.nil?
68
+ nil
69
+ else
70
+ const_get(@project_connector_command_class)
71
+ end
72
+ end
73
+
58
74
  def register_command(const, cmd)
59
75
  return if project_load_shallow
60
76
  Context.new.abort(
@@ -65,7 +81,7 @@ module ShopifyCli
65
81
 
66
82
  def register_task(task, name)
67
83
  return if project_load_shallow
68
- ShopifyCli::Task.register(task, name)
84
+ ShopifyCli::Tasks.register(task, name)
69
85
  end
70
86
 
71
87
  def register_messages(messages)
@@ -0,0 +1,25 @@
1
+ ##
2
+ # `ResolveConstant` implements constant resolution. It is implemented as a
3
+ # `MethodObject` and therefore returns a result object. By default, constants
4
+ # are resolved relative to `Kernal`, but the top-level namespace is
5
+ # configurable:
6
+ #
7
+ # ShopifyCli::Resolve.call(:object).value # => Object
8
+ # ShopifyCli::Resolve.call('minitest/test').value # => MiniTest::Test
9
+ # ShopifyCli::Resolve.call(:test, namespace: MiniTest) # => MiniTest::Test
10
+ #
11
+ module ShopifyCli
12
+ class ResolveConstant
13
+ include ShopifyCli::MethodObject
14
+ property! :namespace, accepts: ->(ns) { ns.respond_to?(:const_get) }, default: Kernel
15
+
16
+ def call(name)
17
+ name
18
+ .to_s
19
+ .split(%r{/|:{2}})
20
+ .map { |const_name| const_name.split(/[_-]/).map(&:capitalize).join("") }
21
+ .join("::")
22
+ .yield_self { |const_name| namespace.const_get(const_name) }
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,432 @@
1
+ module ShopifyCli
2
+ ##
3
+ # This module defines two containers for wrapping the result of an action. One
4
+ # for signifying the successful execution of an action and one for signifying
5
+ # a failure. Both containers implement the same API, which has been designed
6
+ # to simplify transforming a result through a series of steps and centralize
7
+ # the error handling in one place. The implementation is heavily inspired by a
8
+ # concept known as result monads in other languages. Consider the following
9
+ # example that uses lambda expressions as stand-ins for more complex method
10
+ # objects:
11
+ #
12
+ # require 'open-uri'
13
+ # Todo = Struct.new(:title, :completed)
14
+ #
15
+ # fetch_data = ->(url) { open(url) }
16
+ # parse_data = ->(json) { JSON.parse(json) }
17
+ # build_todo = ->(attrs) do
18
+ # Todo.new(attrs.fetch(:title), attrs.fetch(:completed))
19
+ # end
20
+ #
21
+ # Result.wrap(&fetch_data)
22
+ # .call("https://jsonplaceholder.typicode.com/todos/1")
23
+ # .then(&parse_data)
24
+ # .then(&build_todo)
25
+ # .map(&:title)
26
+ # .unwrap(nil) # => String | nil
27
+ #
28
+ # If everything goes well, this code returns the title of the to do that is
29
+ # being fetched from `https://jsonplaceholder.typicode.com/todos/1`. However,
30
+ # there are several possible failure scenarios:
31
+ #
32
+ # * fetching the data could fail due to a network error,
33
+ # * the data returned from the server might not be valid JSON, or
34
+ # * the data is valid but does not have the right shape.
35
+ #
36
+ # If any of these scenarios arises, all subsequent `then` and `map` blocks are
37
+ # skipped until the result is either unwrapped or we manually recover from the
38
+ # failure by specifying a `rescue` clause:
39
+ #
40
+ # Result.wrap { raise "Boom!" }
41
+ # .rescue { |e| e.message.upcase }
42
+ # .unwrap(nil) # => "BOOM!"
43
+ #
44
+ # In the event of a failure that hasn't been rescued from,
45
+ # `unwrap` returns the fallback value specified by the caller:
46
+ #
47
+ # Result.wrap { raise "Boom!" }.unwrap(nil) # => nil
48
+ # Result.wrap { raise "Boom!" }.unwrap { |e| e.message } # => "Boom!"
49
+ #
50
+ module Result
51
+ class Error < RuntimeError; end
52
+ class UnexpectedSuccess < Error; end
53
+ class UnexpectedFailure < Error; end
54
+
55
+ ##
56
+ # Implements a container for wrapping a success value. The main purpose of
57
+ # the container is to support further transformations of the result and
58
+ # centralize error handling should any of the subsequent transformations
59
+ # fail:
60
+ #
61
+ # result = Result
62
+ # .new("{}")
63
+ # .then { |json| JSON.parse(json) }
64
+ # .tap do |result|
65
+ # result.success? # => true
66
+ # result.value # => {}
67
+ # .then { |data| data.fetch(:firstname) }
68
+ # .tap do |result|
69
+ # result.failure? # => true
70
+ # result.error # => KeyError
71
+ # end
72
+ #
73
+ # `Success` implements two transformation functions: `then` and `map`. The
74
+ # former makes no assumption regarding the return value of the
75
+ # transformation. The latter on the other hand expects the transformation to
76
+ # be successful. If this assumption is violated, program execution is
77
+ # interrupted and an error is raised. As the purpose of result objects is to
78
+ # guard against exactly that. This is generally a flaw and requires the code
79
+ # to either be hardened or to substitute the call to `map` with a call to
80
+ # `then`. `map` should only be used for transformations that cannot fail and
81
+ # when the caller wants to state exactly that fact.
82
+ #
83
+ class Success
84
+ attr_reader :value
85
+
86
+ ##
87
+ # initializes a new `Success` from an arbitrary value.
88
+ def initialize(value)
89
+ @value = value
90
+ end
91
+
92
+ ##
93
+ # always returns true to indicate that this result represents a success.
94
+ #
95
+ def success?
96
+ true
97
+ end
98
+
99
+ ##
100
+ # always returns false to indicate that this result represents a success.
101
+ #
102
+ def failure?
103
+ false
104
+ end
105
+
106
+ ##
107
+ # raises an `UnexpectedSuccess` as a `Failure` does not carry an error
108
+ # value.
109
+ #
110
+ def error
111
+ raise UnexpectedSuccess
112
+ end
113
+
114
+ ##
115
+ # returns a new `Success` wrapping the result of the given block. The
116
+ # block is called with the current value. If the block raises an exception
117
+ # or returns a `Failure`, an exception is raised. `map` assumes any
118
+ # transformation to succeed. Transformations that are expected to fail under
119
+ # certain conditions should only be transformed using `then`:
120
+ #
121
+ # Success
122
+ # .new(nil)
123
+ # .map { |n| n + 1 } # => raises NoMethodError
124
+ #
125
+ # Therefore, map should only be used here if the previous success value is
126
+ # guaranteed to be a number or if the block handles nil cases properly:
127
+ #
128
+ # Success
129
+ # .new(nil)
130
+ # .map { |n| (n || 0) + 1 }
131
+ # .value # => 1
132
+ #
133
+ def map(&block)
134
+ self.then(&block).tap do |result|
135
+ return result if result.success?
136
+
137
+ result.unwrap { |error| error }.tap do |error|
138
+ case error
139
+ when Exception
140
+ raise error
141
+ else
142
+ raise UnexpectedFailure, error
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ ##
149
+ # returns a new result by wrapping the return value of the block. The
150
+ # block is invoked with the current success value. The result can either
151
+ # be a `Success` or a `Failure`. The former is the default. The latter
152
+ # occurs when executing the block either
153
+ #
154
+ # - raised an exception,
155
+ # - returned an instance of a subclass of `Exception`, or
156
+ # - returned a `Failure`.
157
+ #
158
+ # The example below illustrates this behavior:
159
+ #
160
+ # result = Success
161
+ # .new(1)
162
+ # .then { |n| n + 1 }
163
+ # .tap do |result|
164
+ # result.success? # => true
165
+ # result.value # => 2
166
+ # end
167
+ #
168
+ # result.then { |n| n / 0 }.error # => ZeroDivisionError
169
+ # result.then { RuntimeError.new }.error # => RuntimeError
170
+ # result.then { Failure.new("Boom!") }.error # => "Boom!"
171
+ #
172
+ def then(&block)
173
+ Result.wrap(&block).call(@value)
174
+ end
175
+
176
+ ##
177
+ # is a no-op and simply returns itself. Only a `Failure` can be
178
+ # transformed using `rescue`.
179
+ #
180
+ def rescue
181
+ self
182
+ end
183
+
184
+ ##
185
+ # returns the success value and ignores the fallback value that was either
186
+ # provided as a method argument or by passing a block. However, the caller
187
+ # is still required to specify a fallback value to ensure that in the
188
+ # event of a `Failure` program execution can continue in a controlled
189
+ # manner:
190
+ #
191
+ # Success.new(1).unwrap(0) => 1
192
+ #
193
+ def unwrap(*args, &block)
194
+ raise ArgumentError, "expected either a fallback value or a block" unless args.one? ^ block
195
+ @value
196
+ end
197
+ end
198
+
199
+ ##
200
+ # Implements a container for wrapping an error value. In many cases, the
201
+ # error value is going to be an exception but other values are fully
202
+ # supported:
203
+ #
204
+ # Failure
205
+ # .new(RuntimeError.new("Something went wrong"))
206
+ # .error # => RuntimeError.new
207
+ #
208
+ # Failure
209
+ # .new("Something went wrong")
210
+ # .error # => "Something went wrong"
211
+ #
212
+ # `Failure` does not support transformations with `then` and `map`. When any
213
+ # of these two methods is invoked on a `Failure`, the `Failure` itself is
214
+ # returned unless it is rescued from or unwrapped. This enables the caller to
215
+ # build optimistic transformation chains and defer error handling:
216
+ #
217
+ # Failure
218
+ # .new(nil)
219
+ # .then { |json| JSON.parse(json) } # Ignored
220
+ # .then(&:with_indifferent_access) # Ignored
221
+ # .then { |data| data.values_at(:firstname, :lastname) } # Ignored
222
+ # .unwrap(Person.new("John", "Doe")) # => Person
223
+ #
224
+ # Alternatively, we could resucue from the error and then proceed with the
225
+ # remanining transformations:
226
+ #
227
+ # Person = Struct.new(:firstname, :lastname)
228
+ # Failure
229
+ # .new(nil)
230
+ # .then { |json| JSON.parse(json) } # Ignored
231
+ # .then(&:with_indifferent_access) # Ignored
232
+ # .rescue { {firstname: "John", lastname: "Doe" }}
233
+ # .then { |data| data.values_at(:firstname, :lastname) } # Executed
234
+ # .then { |members| Person.new(*members) } # Executed
235
+ # .unwrap(nil) # => Person
236
+ #
237
+ class Failure
238
+ attr_reader :error
239
+
240
+ ##
241
+ # initializes a new `Failure` from an arbitrary value. In many cases, this
242
+ # value is going to be an instance of a subclass of `Exception` but any
243
+ # type is supported.
244
+ #
245
+ def initialize(error)
246
+ @error = error
247
+ end
248
+
249
+ ##
250
+ # always returns `false` to indicate that this result represents a failure.
251
+ #
252
+ def success?
253
+ false
254
+ end
255
+
256
+ ##
257
+ # Always returns `true` to indicate that this result represents a failure.
258
+ #
259
+ def failure?
260
+ true
261
+ end
262
+
263
+ ##
264
+ # raises an `ShopifyCli::Result::UnexpectedError` as a
265
+ # `ShopifyCli::Result::Failure` does not carry a success value.
266
+ #
267
+ def value
268
+ raise UnexpectedFailure
269
+ end
270
+
271
+ ##
272
+ # is a no-op and simply returns itself. This is essential to skip
273
+ # transformation steps in a chain once an error has occurred.
274
+ #
275
+ def map
276
+ self
277
+ end
278
+
279
+ ##
280
+ # is a no-op and simply returns itself. This is essential to skip
281
+ # transformation steps in a chain once an error has occurred.
282
+ #
283
+ def then
284
+ self
285
+ end
286
+
287
+ ##
288
+ # can be used to recover from a failure or produce a new failure with a
289
+ # different error.
290
+ #
291
+ # Failure
292
+ # .new("Something went wrong")
293
+ # .rescue { |msg| [msg, "but we fixed it!"].join(" "") }
294
+ # .tap do |result|
295
+ # result.success? # => true
296
+ # result.value # => "Something went wrong but we fixed it!"
297
+ # end
298
+ #
299
+ # `rescue` is opinionated when it comes to the return value of the block.
300
+ # If the return value is an `Exception` – either one that was raised or an
301
+ # instance of a subclass of `Exception` – a `Failure` is returned. Any
302
+ # other value results in a `Success` unless the value has been explicitly
303
+ # wrapped in a `Failure`:
304
+ #
305
+ # Failure
306
+ # .new(RuntimeError.new)
307
+ # .resuce { "All good! "}
308
+ # .success? # => true
309
+ #
310
+ # Failure
311
+ # .new(RuntimeError.new)
312
+ # .resuce { Failure.new("Still broken!") }
313
+ # .success? # => false
314
+ #
315
+ def rescue(&block)
316
+ Result.wrap(&block).call(@error)
317
+ end
318
+
319
+ ##
320
+ # returns the fallback value specified by the caller. The fallback value
321
+ # can be provided as a method argument or as a block. If a block is given,
322
+ # it receives the error as its first and only argument:
323
+ #
324
+ # failure = Failure.new(RuntimeError.new("Something went wrong!"))
325
+ #
326
+ # failure.unwrap(nil) # => nil
327
+ # failure.unwrap { |e| e.message } # => "Something went wrong!"
328
+ #
329
+ # #### Parameters
330
+ #
331
+ # * `*args` should be an `Array` with zero or one element
332
+ # * `&block` should be a Proc that takes zero or one argument
333
+ #
334
+ # #### Raises
335
+ #
336
+ # * `ArgumentError` if both a fallback argument and a block is provided
337
+ #
338
+ def unwrap(*args, &block)
339
+ raise ArgumentError, "expected either a fallback value or a block" unless args.one? ^ block
340
+ block ? block.call(@error) : args.pop
341
+ end
342
+ end
343
+
344
+ ##
345
+ # wraps the given value into a `ShopifyCli::Result::Success` container
346
+ #
347
+ # #### Parameters
348
+ #
349
+ # * `value` a value of arbitrary type
350
+ #
351
+ def self.success(value)
352
+ Result::Success.new(value)
353
+ end
354
+
355
+ ##
356
+ # wraps the given value into a `ShopifyCli::Result::Failure` container
357
+ #
358
+ # #### Parameters
359
+ #
360
+ # * `error` a value of arbitrary type
361
+ #
362
+ def self.failure(error)
363
+ Result::Failure.new(error)
364
+ end
365
+
366
+ ##
367
+ # takes either a value or a block and chooses the appropriate result
368
+ # container based on the type of the value or the type of the block's return
369
+ # value. If the type is an exception, it is wrapped in a
370
+ # `ShopifyCli::Result::Failure` and otherwise in a
371
+ # `ShopifyCli::Result::Success`. If a block was provided instead of value, a
372
+ # `Proc` is returned and the result wrapping doesn't occur until the block
373
+ # is invoked.
374
+ #
375
+ # #### Parameters
376
+ #
377
+ # * `*args` should be an `Array` with zero or one element
378
+ # * `&block` should be a `Proc` that takes zero or one argument
379
+ #
380
+ # #### Returns
381
+ #
382
+ # Returns either a `Result::Success`, `Result::Failure` or a `Proc` that
383
+ # produces one of the former when invoked.
384
+ #
385
+ # #### Examples
386
+ #
387
+ # Result.wrap(1) # => ShopifyCli::Result::Success
388
+ # Result.wrap(RuntimeError.new) # => ShopifyCli::Result::Failure
389
+ #
390
+ # Result.wrap { 1 } # => Proc
391
+ # Result.wrap { 1 }.call # => ShopifyCli::Result::Success
392
+ # Result.wrap { raise }.call # => ShopifyCli::Result::Failure
393
+ #
394
+ # Result.wrap { |s| s.upcase }.call("hello").tap do |result|
395
+ # result # => Result::Success
396
+ # result.value # => "HELLO"
397
+ # end
398
+ #
399
+ def self.wrap(*values, &block)
400
+ raise ArgumentError, "expected either a value or a block" unless values.one? ^ block
401
+
402
+ if values.one?
403
+ values.pop.yield_self do |value|
404
+ case value
405
+ when Result::Success, Result::Failure
406
+ value
407
+ when Exception
408
+ Result.failure(value)
409
+ else
410
+ Result.success(value)
411
+ end
412
+ end
413
+ else
414
+ ->(*args) do
415
+ begin
416
+ wrap(block.call(*args))
417
+ rescue => error
418
+ wrap(error)
419
+ end
420
+ end
421
+ end
422
+ end
423
+
424
+ ##
425
+ # Wraps the given block and invokes it with the passed arguments.
426
+ #
427
+ def self.call(*args, &block)
428
+ raise ArgumentError, "expected a block" unless block
429
+ wrap(&block).call(*args)
430
+ end
431
+ end
432
+ end