shopify-cli 0.9.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 (273) hide show
  1. checksums.yaml +7 -0
  2. data/.github/CODEOWNERS +1 -0
  3. data/.github/CODE_OF_CONDUCT.md +73 -0
  4. data/.github/CONTRIBUTING.md +51 -0
  5. data/.github/DESIGN.md +153 -0
  6. data/.github/ISSUE_TEMPLATE.md +38 -0
  7. data/.github/PULL_REQUEST_TEMPLATE.md +22 -0
  8. data/.github/probots.yml +3 -0
  9. data/.gitignore +19 -0
  10. data/.rubocop.yml +47 -0
  11. data/.ruby-version +1 -0
  12. data/.travis.yml +12 -0
  13. data/Gemfile +22 -0
  14. data/Gemfile.lock +77 -0
  15. data/LICENSE.md +7 -0
  16. data/README.md +13 -0
  17. data/Rakefile +101 -0
  18. data/SECURITY.md +59 -0
  19. data/Vagrantfile +17 -0
  20. data/bin/load_shopify.rb +20 -0
  21. data/bin/shopify +32 -0
  22. data/dev.yml +17 -0
  23. data/docs/Gemfile +5 -0
  24. data/docs/Gemfile.lock +248 -0
  25. data/docs/_config.yml +16 -0
  26. data/docs/_data/nav.yml +26 -0
  27. data/docs/_includes/footer.html +15 -0
  28. data/docs/_includes/head.html +19 -0
  29. data/docs/_includes/sidebar_nav.html +22 -0
  30. data/docs/_includes/toc.html +112 -0
  31. data/docs/_layouts/default.html +79 -0
  32. data/docs/app/node/commands/index.md +82 -0
  33. data/docs/app/node/index.md +35 -0
  34. data/docs/app/rails/commands/index.md +80 -0
  35. data/docs/app/rails/index.md +36 -0
  36. data/docs/core/index.md +70 -0
  37. data/docs/css/docs.css +157 -0
  38. data/docs/getting-started/index.md +61 -0
  39. data/docs/help/start-app/index.md +6 -0
  40. data/docs/images/header.png +0 -0
  41. data/docs/index.md +27 -0
  42. data/docs/installing-ruby.md +28 -0
  43. data/ext/shopify-cli/extconf.rb +27 -0
  44. data/install.sh +7 -0
  45. data/lib/docgen/class_template.md.erb +81 -0
  46. data/lib/docgen/index_template.md.erb +5 -0
  47. data/lib/docgen/markdown.rb +101 -0
  48. data/lib/graphql/admin_introspection.graphql +87 -0
  49. data/lib/graphql/all_organizations.graphql +19 -0
  50. data/lib/graphql/all_orgs_with_apps.graphql +30 -0
  51. data/lib/graphql/api_versions.graphql +6 -0
  52. data/lib/graphql/convert_dev_to_test_store.graphql +10 -0
  53. data/lib/graphql/create_app.graphql +20 -0
  54. data/lib/graphql/create_customer.graphql +9 -0
  55. data/lib/graphql/create_draft_order.graphql +8 -0
  56. data/lib/graphql/create_product.graphql +9 -0
  57. data/lib/graphql/extension_create.graphql +21 -0
  58. data/lib/graphql/extension_update_draft.graphql +18 -0
  59. data/lib/graphql/find_organization.graphql +17 -0
  60. data/lib/graphql/get_app_urls.graphql +6 -0
  61. data/lib/graphql/update_dashboard_urls.graphql +8 -0
  62. data/lib/project_types/extension/cli.rb +71 -0
  63. data/lib/project_types/extension/commands/build.rb +29 -0
  64. data/lib/project_types/extension/commands/create.rb +49 -0
  65. data/lib/project_types/extension/commands/extension_command.rb +22 -0
  66. data/lib/project_types/extension/commands/push.rb +69 -0
  67. data/lib/project_types/extension/commands/register.rb +78 -0
  68. data/lib/project_types/extension/commands/serve.rb +24 -0
  69. data/lib/project_types/extension/commands/tunnel.rb +69 -0
  70. data/lib/project_types/extension/extension_project.rb +85 -0
  71. data/lib/project_types/extension/extension_project_keys.rb +10 -0
  72. data/lib/project_types/extension/features/argo.rb +48 -0
  73. data/lib/project_types/extension/features/argo_dependencies.rb +28 -0
  74. data/lib/project_types/extension/features/argo_setup.rb +54 -0
  75. data/lib/project_types/extension/features/argo_setup_step.rb +31 -0
  76. data/lib/project_types/extension/features/argo_setup_steps.rb +53 -0
  77. data/lib/project_types/extension/features/tunnel_url.rb +20 -0
  78. data/lib/project_types/extension/forms/create.rb +52 -0
  79. data/lib/project_types/extension/forms/register.rb +48 -0
  80. data/lib/project_types/extension/messages/message_loading.rb +37 -0
  81. data/lib/project_types/extension/messages/messages.rb +126 -0
  82. data/lib/project_types/extension/models/app.rb +14 -0
  83. data/lib/project_types/extension/models/registration.rb +19 -0
  84. data/lib/project_types/extension/models/type.rb +76 -0
  85. data/lib/project_types/extension/models/types/checkout_post_purchase.rb +20 -0
  86. data/lib/project_types/extension/models/types/subscription_management.rb +20 -0
  87. data/lib/project_types/extension/models/validation_error.rb +17 -0
  88. data/lib/project_types/extension/models/version.rb +15 -0
  89. data/lib/project_types/extension/tasks/converters/registration_converter.rb +26 -0
  90. data/lib/project_types/extension/tasks/converters/validation_error_converter.rb +25 -0
  91. data/lib/project_types/extension/tasks/converters/version_converter.rb +28 -0
  92. data/lib/project_types/extension/tasks/create_extension.rb +31 -0
  93. data/lib/project_types/extension/tasks/get_apps.rb +34 -0
  94. data/lib/project_types/extension/tasks/update_draft.rb +29 -0
  95. data/lib/project_types/extension/tasks/user_errors.rb +45 -0
  96. data/lib/project_types/node/cli.rb +37 -0
  97. data/lib/project_types/node/commands/create.rb +117 -0
  98. data/lib/project_types/node/commands/deploy.rb +22 -0
  99. data/lib/project_types/node/commands/deploy/heroku.rb +91 -0
  100. data/lib/project_types/node/commands/generate.rb +51 -0
  101. data/lib/project_types/node/commands/generate/billing.rb +37 -0
  102. data/lib/project_types/node/commands/generate/page.rb +55 -0
  103. data/lib/project_types/node/commands/generate/webhook.rb +33 -0
  104. data/lib/project_types/node/commands/open.rb +16 -0
  105. data/lib/project_types/node/commands/populate.rb +23 -0
  106. data/lib/project_types/node/commands/populate/customer.rb +31 -0
  107. data/lib/project_types/node/commands/populate/draft_order.rb +28 -0
  108. data/lib/project_types/node/commands/populate/product.rb +30 -0
  109. data/lib/project_types/node/commands/serve.rb +45 -0
  110. data/lib/project_types/node/commands/tunnel.rb +39 -0
  111. data/lib/project_types/node/forms/create.rb +87 -0
  112. data/lib/project_types/node/messages/messages.rb +260 -0
  113. data/lib/project_types/rails/cli.rb +41 -0
  114. data/lib/project_types/rails/commands/create.rb +126 -0
  115. data/lib/project_types/rails/commands/deploy.rb +22 -0
  116. data/lib/project_types/rails/commands/deploy/heroku.rb +113 -0
  117. data/lib/project_types/rails/commands/generate.rb +49 -0
  118. data/lib/project_types/rails/commands/generate/webhook.rb +39 -0
  119. data/lib/project_types/rails/commands/open.rb +16 -0
  120. data/lib/project_types/rails/commands/populate.rb +23 -0
  121. data/lib/project_types/rails/commands/populate/customer.rb +31 -0
  122. data/lib/project_types/rails/commands/populate/draft_order.rb +28 -0
  123. data/lib/project_types/rails/commands/populate/product.rb +30 -0
  124. data/lib/project_types/rails/commands/serve.rb +47 -0
  125. data/lib/project_types/rails/commands/tunnel.rb +39 -0
  126. data/lib/project_types/rails/forms/create.rb +116 -0
  127. data/lib/project_types/rails/gem.rb +56 -0
  128. data/lib/project_types/rails/messages/messages.rb +283 -0
  129. data/lib/project_types/rails/ruby.rb +17 -0
  130. data/lib/project_types/script/cli.rb +76 -0
  131. data/lib/project_types/script/commands/create.rb +45 -0
  132. data/lib/project_types/script/commands/disable.rb +36 -0
  133. data/lib/project_types/script/commands/enable.rb +46 -0
  134. data/lib/project_types/script/commands/push.rb +39 -0
  135. data/lib/project_types/script/config/extension_points.yml +18 -0
  136. data/lib/project_types/script/errors.rb +16 -0
  137. data/lib/project_types/script/forms/create.rb +29 -0
  138. data/lib/project_types/script/forms/enable.rb +24 -0
  139. data/lib/project_types/script/forms/push.rb +19 -0
  140. data/lib/project_types/script/forms/script_form.rb +66 -0
  141. data/lib/project_types/script/graphql/app_script_update_or_create.graphql +27 -0
  142. data/lib/project_types/script/graphql/script_service_proxy.graphql +8 -0
  143. data/lib/project_types/script/graphql/shop_script_delete.graphql +14 -0
  144. data/lib/project_types/script/graphql/shop_script_update_or_create.graphql +28 -0
  145. data/lib/project_types/script/layers/application/build_script.rb +43 -0
  146. data/lib/project_types/script/layers/application/create_script.rb +47 -0
  147. data/lib/project_types/script/layers/application/disable_script.rb +19 -0
  148. data/lib/project_types/script/layers/application/enable_script.rb +21 -0
  149. data/lib/project_types/script/layers/application/extension_points.rb +17 -0
  150. data/lib/project_types/script/layers/application/project_dependencies.rb +34 -0
  151. data/lib/project_types/script/layers/application/push_script.rb +30 -0
  152. data/lib/project_types/script/layers/domain/errors.rb +25 -0
  153. data/lib/project_types/script/layers/domain/extension_point.rb +29 -0
  154. data/lib/project_types/script/layers/domain/push_package.rb +29 -0
  155. data/lib/project_types/script/layers/domain/script.rb +18 -0
  156. data/lib/project_types/script/layers/infrastructure/assemblyscript_dependency_manager.rb +73 -0
  157. data/lib/project_types/script/layers/infrastructure/assemblyscript_tsconfig.rb +38 -0
  158. data/lib/project_types/script/layers/infrastructure/assemblyscript_wasm_builder.rb +39 -0
  159. data/lib/project_types/script/layers/infrastructure/dependency_manager.rb +36 -0
  160. data/lib/project_types/script/layers/infrastructure/errors.rb +38 -0
  161. data/lib/project_types/script/layers/infrastructure/extension_point_repository.rb +31 -0
  162. data/lib/project_types/script/layers/infrastructure/push_package_repository.rb +47 -0
  163. data/lib/project_types/script/layers/infrastructure/script_builder.rb +34 -0
  164. data/lib/project_types/script/layers/infrastructure/script_repository.rb +89 -0
  165. data/lib/project_types/script/layers/infrastructure/script_service.rb +165 -0
  166. data/lib/project_types/script/layers/infrastructure/test_suite_repository.rb +59 -0
  167. data/lib/project_types/script/messages/messages.rb +204 -0
  168. data/lib/project_types/script/script_project.rb +37 -0
  169. data/lib/project_types/script/templates/ts/as-pect.config.js +21 -0
  170. data/lib/project_types/script/ui/error_handler.rb +136 -0
  171. data/lib/project_types/script/ui/strict_spinner.rb +22 -0
  172. data/lib/rubygems_plugin.rb +18 -0
  173. data/lib/shopify-cli/admin_api.rb +99 -0
  174. data/lib/shopify-cli/admin_api/populate_resource_command.rb +165 -0
  175. data/lib/shopify-cli/admin_api/schema.rb +32 -0
  176. data/lib/shopify-cli/api.rb +104 -0
  177. data/lib/shopify-cli/command.rb +67 -0
  178. data/lib/shopify-cli/commands.rb +28 -0
  179. data/lib/shopify-cli/commands/connect.rb +108 -0
  180. data/lib/shopify-cli/commands/create.rb +50 -0
  181. data/lib/shopify-cli/commands/help.rb +79 -0
  182. data/lib/shopify-cli/commands/logout.rb +23 -0
  183. data/lib/shopify-cli/commands/system.rb +135 -0
  184. data/lib/shopify-cli/commands/version.rb +15 -0
  185. data/lib/shopify-cli/context.rb +372 -0
  186. data/lib/shopify-cli/core.rb +9 -0
  187. data/lib/shopify-cli/core/entry_point.rb +40 -0
  188. data/lib/shopify-cli/core/executor.rb +21 -0
  189. data/lib/shopify-cli/core/help_resolver.rb +20 -0
  190. data/lib/shopify-cli/core/monorail.rb +118 -0
  191. data/lib/shopify-cli/db.rb +114 -0
  192. data/lib/shopify-cli/form.rb +40 -0
  193. data/lib/shopify-cli/git.rb +141 -0
  194. data/lib/shopify-cli/helpers.rb +5 -0
  195. data/lib/shopify-cli/helpers/haikunator.rb +92 -0
  196. data/lib/shopify-cli/heroku.rb +97 -0
  197. data/lib/shopify-cli/js_deps.rb +110 -0
  198. data/lib/shopify-cli/js_system.rb +98 -0
  199. data/lib/shopify-cli/messages/messages.rb +287 -0
  200. data/lib/shopify-cli/oauth.rb +192 -0
  201. data/lib/shopify-cli/oauth/servlet.rb +61 -0
  202. data/lib/shopify-cli/options.rb +40 -0
  203. data/lib/shopify-cli/packager.rb +116 -0
  204. data/lib/shopify-cli/partners_api.rb +114 -0
  205. data/lib/shopify-cli/partners_api/organizations.rb +32 -0
  206. data/lib/shopify-cli/process_supervision.rb +187 -0
  207. data/lib/shopify-cli/project.rb +191 -0
  208. data/lib/shopify-cli/project_type.rb +83 -0
  209. data/lib/shopify-cli/resources.rb +5 -0
  210. data/lib/shopify-cli/resources/env_file.rb +96 -0
  211. data/lib/shopify-cli/sub_command.rb +15 -0
  212. data/lib/shopify-cli/task.rb +10 -0
  213. data/lib/shopify-cli/tasks.rb +32 -0
  214. data/lib/shopify-cli/tasks/create_api_client.rb +29 -0
  215. data/lib/shopify-cli/tasks/ensure_dev_store.rb +41 -0
  216. data/lib/shopify-cli/tasks/ensure_env.rb +31 -0
  217. data/lib/shopify-cli/tasks/ensure_loopback_url.rb +20 -0
  218. data/lib/shopify-cli/tasks/update_dashboard_urls.rb +44 -0
  219. data/lib/shopify-cli/tunnel.rb +154 -0
  220. data/lib/shopify-cli/version.rb +3 -0
  221. data/lib/shopify_cli.rb +132 -0
  222. data/shopify-cli.gemspec +40 -0
  223. data/shopify.fish +12 -0
  224. data/shopify.sh +11 -0
  225. data/vendor/deps/cli-kit/REVISION +1 -0
  226. data/vendor/deps/cli-kit/lib/cli/kit.rb +60 -0
  227. data/vendor/deps/cli-kit/lib/cli/kit/autocall.rb +21 -0
  228. data/vendor/deps/cli-kit/lib/cli/kit/base_command.rb +49 -0
  229. data/vendor/deps/cli-kit/lib/cli/kit/command_registry.rb +94 -0
  230. data/vendor/deps/cli-kit/lib/cli/kit/config.rb +133 -0
  231. data/vendor/deps/cli-kit/lib/cli/kit/error_handler.rb +115 -0
  232. data/vendor/deps/cli-kit/lib/cli/kit/executor.rb +81 -0
  233. data/vendor/deps/cli-kit/lib/cli/kit/ini.rb +102 -0
  234. data/vendor/deps/cli-kit/lib/cli/kit/levenshtein.rb +82 -0
  235. data/vendor/deps/cli-kit/lib/cli/kit/logger.rb +76 -0
  236. data/vendor/deps/cli-kit/lib/cli/kit/resolver.rb +60 -0
  237. data/vendor/deps/cli-kit/lib/cli/kit/ruby_backports/enumerable.rb +6 -0
  238. data/vendor/deps/cli-kit/lib/cli/kit/support.rb +9 -0
  239. data/vendor/deps/cli-kit/lib/cli/kit/support/test_helper.rb +244 -0
  240. data/vendor/deps/cli-kit/lib/cli/kit/system.rb +207 -0
  241. data/vendor/deps/cli-kit/lib/cli/kit/util.rb +189 -0
  242. data/vendor/deps/cli-kit/lib/cli/kit/version.rb +5 -0
  243. data/vendor/deps/cli-ui/REVISION +1 -0
  244. data/vendor/deps/cli-ui/lib/cli/ui.rb +187 -0
  245. data/vendor/deps/cli-ui/lib/cli/ui/ansi.rb +153 -0
  246. data/vendor/deps/cli-ui/lib/cli/ui/box.rb +15 -0
  247. data/vendor/deps/cli-ui/lib/cli/ui/color.rb +79 -0
  248. data/vendor/deps/cli-ui/lib/cli/ui/formatter.rb +179 -0
  249. data/vendor/deps/cli-ui/lib/cli/ui/frame.rb +310 -0
  250. data/vendor/deps/cli-ui/lib/cli/ui/glyph.rb +78 -0
  251. data/vendor/deps/cli-ui/lib/cli/ui/progress.rb +88 -0
  252. data/vendor/deps/cli-ui/lib/cli/ui/prompt.rb +248 -0
  253. data/vendor/deps/cli-ui/lib/cli/ui/prompt/interactive_options.rb +472 -0
  254. data/vendor/deps/cli-ui/lib/cli/ui/prompt/options_handler.rb +24 -0
  255. data/vendor/deps/cli-ui/lib/cli/ui/spinner.rb +48 -0
  256. data/vendor/deps/cli-ui/lib/cli/ui/spinner/async.rb +40 -0
  257. data/vendor/deps/cli-ui/lib/cli/ui/spinner/spin_group.rb +241 -0
  258. data/vendor/deps/cli-ui/lib/cli/ui/stdout_router.rb +227 -0
  259. data/vendor/deps/cli-ui/lib/cli/ui/terminal.rb +36 -0
  260. data/vendor/deps/cli-ui/lib/cli/ui/truncater.rb +102 -0
  261. data/vendor/deps/cli-ui/lib/cli/ui/version.rb +5 -0
  262. data/vendor/deps/smart_properties/REVISION +1 -0
  263. data/vendor/deps/smart_properties/lib/smart_properties.rb +174 -0
  264. data/vendor/deps/smart_properties/lib/smart_properties/errors.rb +114 -0
  265. data/vendor/deps/smart_properties/lib/smart_properties/property.rb +162 -0
  266. data/vendor/deps/smart_properties/lib/smart_properties/property_collection.rb +83 -0
  267. data/vendor/deps/smart_properties/lib/smart_properties/validations.rb +8 -0
  268. data/vendor/deps/smart_properties/lib/smart_properties/validations/ancestor.rb +27 -0
  269. data/vendor/deps/smart_properties/lib/smart_properties/version.rb +3 -0
  270. data/vendor/lib/semantic/LICENSE +20 -0
  271. data/vendor/lib/semantic/semantic.rb +4 -0
  272. data/vendor/lib/semantic/version.rb +180 -0
  273. metadata +374 -0
@@ -0,0 +1,36 @@
1
+ require 'cli/ui'
2
+ require 'io/console'
3
+
4
+ module CLI
5
+ module UI
6
+ module Terminal
7
+ DEFAULT_WIDTH = 80
8
+ DEFAULT_HEIGHT = 24
9
+
10
+ # Returns the width of the terminal, if possible
11
+ # Otherwise will return 80
12
+ #
13
+ def self.width
14
+ if console = IO.respond_to?(:console) && IO.console
15
+ width = console.winsize[1]
16
+ width.zero? ? DEFAULT_WIDTH : width
17
+ else
18
+ DEFAULT_WIDTH
19
+ end
20
+ rescue Errno::EIO
21
+ DEFAULT_WIDTH
22
+ end
23
+
24
+ def self.height
25
+ if console = IO.respond_to?(:console) && IO.console
26
+ height = console.winsize[0]
27
+ height.zero? ? DEFAULT_HEIGHT : height
28
+ else
29
+ DEFAULT_HEIGHT
30
+ end
31
+ rescue Errno::EIO
32
+ DEFAULT_HEIGHT
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cli/ui'
4
+
5
+ module CLI
6
+ module UI
7
+ # Truncater truncates a string to a provided printable width.
8
+ module Truncater
9
+ PARSE_ROOT = :root
10
+ PARSE_ANSI = :ansi
11
+ PARSE_ESC = :esc
12
+ PARSE_ZWJ = :zwj
13
+
14
+ ESC = 0x1b
15
+ LEFT_SQUARE_BRACKET = 0x5b
16
+ ZWJ = 0x200d # emojipedia.org/emoji-zwj-sequences
17
+ SEMICOLON = 0x3b
18
+
19
+ # EMOJI_RANGE in particular is super inaccurate. This is best-effort.
20
+ # If you need this to be more accurate, we'll almost certainly accept a
21
+ # PR improving it.
22
+ EMOJI_RANGE = 0x1f300..0x1f5ff
23
+ NUMERIC_RANGE = 0x30..0x39
24
+ LC_ALPHA_RANGE = 0x40..0x5a
25
+ UC_ALPHA_RANGE = 0x60..0x71
26
+
27
+ TRUNCATED = "\x1b[0m…"
28
+
29
+ class << self
30
+ def call(text, printing_width)
31
+ return text if text.size <= printing_width
32
+
33
+ width = 0
34
+ mode = PARSE_ROOT
35
+ truncation_index = nil
36
+
37
+ codepoints = text.codepoints
38
+ codepoints.each.with_index do |cp, index|
39
+ case mode
40
+ when PARSE_ROOT
41
+ case cp
42
+ when ESC # non-printable, followed by some more non-printables.
43
+ mode = PARSE_ESC
44
+ when ZWJ # non-printable, followed by another non-printable.
45
+ mode = PARSE_ZWJ
46
+ else
47
+ width += width(cp)
48
+ if width >= printing_width
49
+ truncation_index ||= index
50
+ # it looks like we could break here but we still want the
51
+ # width calculation for the rest of the characters.
52
+ end
53
+ end
54
+ when PARSE_ESC
55
+ case cp
56
+ when LEFT_SQUARE_BRACKET
57
+ mode = PARSE_ANSI
58
+ else
59
+ mode = PARSE_ROOT
60
+ end
61
+ when PARSE_ANSI
62
+ # ANSI escape codes preeeetty much have the format of:
63
+ # \x1b[0-9;]+[A-Za-z]
64
+ case cp
65
+ when NUMERIC_RANGE, SEMICOLON
66
+ when LC_ALPHA_RANGE, UC_ALPHA_RANGE
67
+ mode = PARSE_ROOT
68
+ else
69
+ # unexpected. let's just go back to the root state I guess?
70
+ mode = PARSE_ROOT
71
+ end
72
+ when PARSE_ZWJ
73
+ # consume any character and consider it as having no width
74
+ # width(x+ZWJ+y) = width(x).
75
+ mode = PARSE_ROOT
76
+ end
77
+ end
78
+
79
+ # Without the `width <= printing_width` check, we truncate
80
+ # "foo\x1b[0m" for a width of 3, but it should not be truncated.
81
+ # It's specifically for the case where we decided "Yes, this is the
82
+ # point at which we'd have to add a truncation!" but it's actually
83
+ # the end of the string.
84
+ return text if !truncation_index || width <= printing_width
85
+
86
+ codepoints[0...truncation_index].pack("U*") + TRUNCATED
87
+ end
88
+
89
+ private
90
+
91
+ def width(printable_codepoint)
92
+ case printable_codepoint
93
+ when EMOJI_RANGE
94
+ 2
95
+ else
96
+ 1
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,5 @@
1
+ module CLI
2
+ module UI
3
+ VERSION = "1.3.0"
4
+ end
5
+ end
@@ -0,0 +1 @@
1
+ 691028e47d022a29405b7f0ccc8683c9d062c502
@@ -0,0 +1,174 @@
1
+ ##
2
+ # {SmartProperties} can be used to easily build more full-fledged accessors
3
+ # for standard Ruby classes. In contrast to regular accessors,
4
+ # {SmartProperties} support validation and conversion of input data, as well
5
+ # as, the specification of default values. Additionally, individual
6
+ # {SmartProperties} can be marked as required. This causes the runtime to
7
+ # throw an +ArgumentError+ whenever a required property has not been
8
+ # specified.
9
+ #
10
+ # In order to use {SmartProperties}, simply include the {SmartProperties}
11
+ # module and use the {ClassMethods#property} method to define properties.
12
+ #
13
+ # @see ClassMethods#property
14
+ # More information on how to configure properties
15
+ #
16
+ # @example Definition of a property that makes use of all {SmartProperties} features.
17
+ #
18
+ # property :language_code, :accepts => [:de, :en],
19
+ # :converts => :to_sym,
20
+ # :default => :de,
21
+ # :required => true
22
+ #
23
+ module SmartProperties
24
+ module ClassMethods
25
+ ##
26
+ # Returns a class's smart properties. This includes the properties that
27
+ # have been defined in the parent classes.
28
+ #
29
+ # @return [Hash<String, Property>] A map of property names to property instances.
30
+ #
31
+ def properties
32
+ @_smart_properties ||= PropertyCollection.for(self)
33
+ end
34
+
35
+ ##
36
+ # Defines a new property from a name and a set of options. This results
37
+ # results in creating an accessor that has additional features:
38
+ #
39
+ # 1. Validation of input data by specifiying the +:accepts+ option:
40
+ # If you use a class as value for this option, the setter will check
41
+ # if the value it is about to assign is of this type. If you use an
42
+ # array, the setter will check if the value it is about to assign is
43
+ # included in this array. Finally, if you specify a block, it will
44
+ # invoke the block with the value it is about to assign and check if
45
+ # the block returns a thruthy value, meaning anything but +false+ and
46
+ # +nil+.
47
+ #
48
+ # 2. Conversion of input data by specifiying the +:converts+ option:
49
+ # If you use provide a symbol as value for this option, the setter will
50
+ # invoke this method on the object it is about to assign and take the
51
+ # result of this call instead. If you provide a block, it will invoke
52
+ # the block with the value it is about to assign and take the result
53
+ # of the block instead.
54
+ #
55
+ # 3. Providing a default value by specifiying the +:default+ option.
56
+ #
57
+ # 4. Forcing a property to be present by setting the +:required+ option
58
+ # to true.
59
+ #
60
+ #
61
+ # @param [Symbol] name the name of the property
62
+ #
63
+ # @param [Hash] options the list of options used to configure the property
64
+ # @option options [Array, Class, Proc] :accepts
65
+ # specifies how the validation is done
66
+ # @option options [Proc, Symbol] :converts
67
+ # specifies how the conversion is done
68
+ # @option options :default
69
+ # specifies the default value of the property
70
+ # @option options [true, false] :required
71
+ # specifies whether or not this property is required
72
+ #
73
+ # @return [Property] The defined property.
74
+ #
75
+ # @example Definition of a property that makes use of all {SmartProperties} features.
76
+ #
77
+ # property :language_code, :accepts => [:de, :en],
78
+ # :converts => :to_sym,
79
+ # :default => :de,
80
+ # :required => true
81
+ #
82
+ def property(name, options = {})
83
+ properties[name] = Property.define(self, name, options)
84
+ end
85
+ protected :property
86
+
87
+ def property!(name, options = {})
88
+ options[:required] = true
89
+ property(name, options)
90
+ end
91
+ protected :property!
92
+ end
93
+
94
+ class << self
95
+ private
96
+
97
+ ##
98
+ # Extends the class, which this module is included in, with a property
99
+ # method to define properties.
100
+ #
101
+ # @param [Class] base the class this module is included in
102
+ #
103
+ def included(base)
104
+ base.extend(ClassMethods)
105
+ end
106
+ end
107
+
108
+ ##
109
+ # Implements a key-value enabled constructor that acts as default
110
+ # constructor for all {SmartProperties}-enabled classes. Positional arguments
111
+ # or keyword arguments that do not correspond to a property are forwarded to
112
+ # the super class constructor.
113
+ #
114
+ # @param [Hash] attrs the set of attributes that is used for initialization
115
+ #
116
+ # @raise [SmartProperties::ConstructorArgumentForwardingError] when unknown arguments were supplied that could not be processed by the super class initializer either.
117
+ # @raise [SmartProperties::InitializationError] when incorrect values were supplied or required values weren't been supplied.
118
+ #
119
+ def initialize(*args, &block)
120
+ attrs = args.last.is_a?(Hash) ? args.pop.dup : {}
121
+ properties = self.class.properties
122
+
123
+ # Track missing properties
124
+ missing_properties = []
125
+
126
+ # Set values
127
+ properties.each do |name, property|
128
+ if attrs.key?(name)
129
+ property.set(self, attrs.delete(name))
130
+ elsif attrs.key?(name.to_s)
131
+ property.set(self, attrs.delete(name.to_s))
132
+ else
133
+ missing_properties.push(property)
134
+ end
135
+ end
136
+
137
+ # Call the super constructor and forward unprocessed arguments
138
+ begin
139
+ attrs.empty? ? super(*args) : super(*args.dup.push(attrs))
140
+ rescue ArgumentError
141
+ raise SmartProperties::ConstructorArgumentForwardingError.new(args, attrs)
142
+ end
143
+
144
+ # Execute configuration block
145
+ block.call(self) if block
146
+
147
+ # Set default values for missing properties
148
+ missing_properties.delete_if { |property| property.set_default(self) }
149
+
150
+ # Recheck - cannot be done while assigning default values because
151
+ # one property might depend on the default value of another property
152
+ missing_properties.delete_if { |property| property.present?(self) || property.optional?(self) }
153
+
154
+ raise SmartProperties::InitializationError.new(self, missing_properties) unless missing_properties.empty?
155
+ end
156
+
157
+ def [](name)
158
+ return if name.nil?
159
+ name = name.to_sym
160
+ reader = self.class.properties[name].reader
161
+ public_send(reader) if self.class.properties.key?(name)
162
+ end
163
+
164
+ def []=(name, value)
165
+ return if name.nil?
166
+ public_send(:"#{name.to_sym}=", value) if self.class.properties.key?(name)
167
+ end
168
+ end
169
+
170
+ require_relative 'smart_properties/property_collection'
171
+ require_relative 'smart_properties/property'
172
+ require_relative 'smart_properties/errors'
173
+ require_relative 'smart_properties/version'
174
+ require_relative 'smart_properties/validations'
@@ -0,0 +1,114 @@
1
+ module SmartProperties
2
+ class Error < ::ArgumentError; end
3
+ class ConfigurationError < Error; end
4
+
5
+ class AssignmentError < Error
6
+ attr_accessor :sender
7
+ attr_accessor :property
8
+
9
+ def initialize(sender, property, message)
10
+ @sender = sender
11
+ @property = property
12
+ super(message)
13
+ end
14
+ end
15
+
16
+ class ConstructorArgumentForwardingError < Error
17
+ def initialize(positional_arguments, keyword_arguments)
18
+ argument_description = [
19
+ generate_description("positional", positional_arguments.count),
20
+ generate_description("keyword", keyword_arguments.count)
21
+ ].compact
22
+
23
+ arguments = positional_arguments + keyword_arguments.map { |name, value| "#{name}: #{value}" }
24
+
25
+ super "Forwarding the following %s failed: %s" % [
26
+ argument_description.join(" and "),
27
+ arguments.join(", ")
28
+ ]
29
+ end
30
+
31
+ private
32
+
33
+ def generate_description(argument_type, argument_number)
34
+ case argument_number
35
+ when 0
36
+ nil
37
+ when 1
38
+ "#{argument_type} argument"
39
+ else
40
+ "#{argument_number} #{argument_type} arguments"
41
+ end
42
+ end
43
+ end
44
+
45
+ class MissingValueError < AssignmentError
46
+ def initialize(sender, property)
47
+ super(
48
+ sender,
49
+ property,
50
+ "%s requires the property %s to be set" % [
51
+ sender.class.name,
52
+ property.name
53
+ ]
54
+ )
55
+ end
56
+
57
+ def to_hash
58
+ Hash[property.name, "must be set"]
59
+ end
60
+ end
61
+
62
+ class InvalidValueError < AssignmentError
63
+ attr_accessor :value
64
+
65
+ def initialize(sender, property, value)
66
+ @value = value
67
+
68
+ super(
69
+ sender,
70
+ property,
71
+ "%s does not accept %s as value for the property %s. Only accepts: %s" % [
72
+ sender.class.name,
73
+ value.inspect,
74
+ property.name,
75
+ accepter_message(sender, property)
76
+ ]
77
+ )
78
+ end
79
+
80
+ def to_hash
81
+ Hash[property.name, "does not accept %s as value" % value.inspect]
82
+ end
83
+
84
+ private
85
+
86
+ def accepter_message(sender, property)
87
+ accepter = property.accepter
88
+ if accepter.is_a?(Proc)
89
+ return "Values passing lambda defined in #{accepter.source_location.join(' at line ')}"
90
+ end
91
+ accepter
92
+ end
93
+ end
94
+
95
+ class InitializationError < Error
96
+ attr_accessor :sender
97
+ attr_accessor :properties
98
+
99
+ def initialize(sender, properties)
100
+ @sender = sender
101
+ @properties = properties
102
+ super(
103
+ "%s requires the following properties to be set: %s" % [
104
+ sender.class.name,
105
+ properties.map(&:name).sort.join(', ')
106
+ ]
107
+ )
108
+ end
109
+
110
+ def to_hash
111
+ properties.each_with_object({}) { |property, errors| errors[property.name] = "must be set" }
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,162 @@
1
+ module SmartProperties
2
+ class Property
3
+ MODULE_REFERENCE = :"@_smart_properties_method_scope"
4
+
5
+ attr_reader :name
6
+ attr_reader :converter
7
+ attr_reader :accepter
8
+ attr_reader :reader
9
+ attr_reader :instance_variable_name
10
+ attr_reader :writable
11
+
12
+ def self.define(scope, name, options = {})
13
+ new(name, options).tap { |p| p.define(scope) }
14
+ end
15
+
16
+ def initialize(name, attrs = {})
17
+ attrs = attrs.dup
18
+
19
+ @name = name.to_sym
20
+ @default = attrs.delete(:default)
21
+ @converter = attrs.delete(:converts)
22
+ @accepter = attrs.delete(:accepts)
23
+ @required = attrs.delete(:required)
24
+ @reader = attrs.delete(:reader)
25
+ @writable = attrs.delete(:writable)
26
+ @reader ||= @name
27
+
28
+ @instance_variable_name = :"@#{name}"
29
+
30
+ unless attrs.empty?
31
+ raise ConfigurationError, "SmartProperties do not support the following configuration options: #{attrs.keys.map { |m| m.to_s }.sort.join(', ')}."
32
+ end
33
+ end
34
+
35
+ def required?(scope)
36
+ @required.kind_of?(Proc) ? scope.instance_exec(&@required) : !!@required
37
+ end
38
+
39
+ def optional?(scope)
40
+ !required?(scope)
41
+ end
42
+
43
+ def missing?(scope)
44
+ required?(scope) && !present?(scope)
45
+ end
46
+
47
+ def present?(scope)
48
+ !null_object?(get(scope))
49
+ end
50
+
51
+ def writable?
52
+ return true if @writable.nil?
53
+ @writable
54
+ end
55
+
56
+ def convert(scope, value)
57
+ return value unless converter
58
+ return value if null_object?(value)
59
+
60
+ case converter
61
+ when Symbol
62
+ converter.to_proc.call(value)
63
+ else
64
+ scope.instance_exec(value, &converter)
65
+ end
66
+ end
67
+
68
+ def default(scope)
69
+ @default.kind_of?(Proc) ? scope.instance_exec(&@default) : @default
70
+ end
71
+
72
+ def accepts?(value, scope)
73
+ return true unless accepter
74
+ return true if null_object?(value)
75
+
76
+ if accepter.respond_to?(:to_proc)
77
+ !!scope.instance_exec(value, &accepter)
78
+ else
79
+ Array(accepter).any? { |accepter| accepter === value }
80
+ end
81
+ end
82
+
83
+ def prepare(scope, value)
84
+ required = required?(scope)
85
+ raise MissingValueError.new(scope, self) if required && null_object?(value)
86
+ value = convert(scope, value)
87
+ raise MissingValueError.new(scope, self) if required && null_object?(value)
88
+ raise InvalidValueError.new(scope, self, value) unless accepts?(value, scope)
89
+ value
90
+ end
91
+
92
+ def define(klass)
93
+ property = self
94
+
95
+ scope =
96
+ if klass.instance_variable_defined?(MODULE_REFERENCE)
97
+ klass.instance_variable_get(MODULE_REFERENCE)
98
+ else
99
+ m = Module.new
100
+ klass.send(:include, m)
101
+ klass.instance_variable_set(MODULE_REFERENCE, m)
102
+ m
103
+ end
104
+
105
+ scope.send(:define_method, reader) do
106
+ property.get(self)
107
+ end
108
+
109
+ if writable?
110
+ scope.send(:define_method, :"#{name}=") do |value|
111
+ property.set(self, value)
112
+ end
113
+ end
114
+ end
115
+
116
+ def set(scope, value)
117
+ scope.instance_variable_set(instance_variable_name, prepare(scope, value))
118
+ end
119
+
120
+ def set_default(scope)
121
+ return false if present?(scope)
122
+
123
+ default_value = default(scope)
124
+ return false if null_object?(default_value)
125
+
126
+ set(scope, default_value)
127
+ true
128
+ end
129
+
130
+ def get(scope)
131
+ return nil unless scope.instance_variable_defined?(instance_variable_name)
132
+ scope.instance_variable_get(instance_variable_name)
133
+ end
134
+
135
+ def to_h
136
+ {
137
+ accepter: @accepter,
138
+ converter: @converter,
139
+ default: @default,
140
+ instance_variable_name: @instance_variable_name,
141
+ name: @name,
142
+ reader: @reader,
143
+ required: @required
144
+ }
145
+ end
146
+
147
+ private
148
+
149
+ def null_object?(object)
150
+ object.nil?
151
+ rescue NoMethodError => error
152
+ # BasicObject does not respond to #nil? by default, so we need to double
153
+ # check if somebody implemented it and it fails internally or if the
154
+ # error occured because the method is actually not present. In the former
155
+ # case, we want to raise the exception because there is something wrong
156
+ # with the implementation of object#nil?. In the latter case we treat the
157
+ # object as truthy because we don't know better.
158
+ raise error if (class << object; self; end).public_instance_methods.include?(:nil?)
159
+ false
160
+ end
161
+ end
162
+ end