hotstepper 0.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 (76) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +36 -0
  4. data/Rakefile +8 -0
  5. data/app/validators/unique_items_validator.rb +40 -0
  6. data/config/locales/custom-predicates.en.yml +70 -0
  7. data/config/locales/dry-validation-errors.en.yml +113 -0
  8. data/config/locales/en.yml +14 -0
  9. data/config/routes.rb +4 -0
  10. data/lib/boot/setup.rb +151 -0
  11. data/lib/generators/hotstepper/service/USAGE +17 -0
  12. data/lib/generators/hotstepper/service/service_generator.rb +137 -0
  13. data/lib/generators/hotstepper/service/templates/operation.rb.tt +10 -0
  14. data/lib/generators/hotstepper/service/templates/service.rb.tt +39 -0
  15. data/lib/generators/hotstepper/typespace/USAGE +8 -0
  16. data/lib/generators/hotstepper/typespace/templates/types.rb.tt +5 -0
  17. data/lib/generators/hotstepper/typespace/typespace_generator.rb +25 -0
  18. data/lib/hotstepper/arel_ext/array_encoding.rb +66 -0
  19. data/lib/hotstepper/arel_ext/base_helpers.rb +220 -0
  20. data/lib/hotstepper/arel_ext/helpers.rb +17 -0
  21. data/lib/hotstepper/arel_ext/json_encoding.rb +49 -0
  22. data/lib/hotstepper/arel_ext/ltree_encoding.rb +52 -0
  23. data/lib/hotstepper/arel_ext/postgres_helpers.rb +34 -0
  24. data/lib/hotstepper/arel_ext/types.rb +37 -0
  25. data/lib/hotstepper/arel_ext/value_select_query.rb +142 -0
  26. data/lib/hotstepper/common.rb +52 -0
  27. data/lib/hotstepper/engine.rb +38 -0
  28. data/lib/hotstepper/models/association_helpers.rb +141 -0
  29. data/lib/hotstepper/models/checks_before_destruction.rb +35 -0
  30. data/lib/hotstepper/models/id_quoting.rb +23 -0
  31. data/lib/hotstepper/models/lazy_ordering.rb +57 -0
  32. data/lib/hotstepper/models/limit_to_one.rb +35 -0
  33. data/lib/hotstepper/models/lookup_helpers.rb +21 -0
  34. data/lib/hotstepper/models/materialized_view.rb +39 -0
  35. data/lib/hotstepper/models/model.rb +40 -0
  36. data/lib/hotstepper/models/querying.rb +55 -0
  37. data/lib/hotstepper/models/timestamp_scopes.rb +26 -0
  38. data/lib/hotstepper/models/view.rb +14 -0
  39. data/lib/hotstepper/models/where_matches.rb +67 -0
  40. data/lib/hotstepper/monadic/coercion.rb +111 -0
  41. data/lib/hotstepper/monadic/decorators.rb +61 -0
  42. data/lib/hotstepper/monadic/do_for.rb +22 -0
  43. data/lib/hotstepper/monadic.rb +8 -0
  44. data/lib/hotstepper/operation.rb +41 -0
  45. data/lib/hotstepper/patterns.rb +20 -0
  46. data/lib/hotstepper/query_building.rb +142 -0
  47. data/lib/hotstepper/service.rb +84 -0
  48. data/lib/hotstepper/steps/abstract_module.rb +21 -0
  49. data/lib/hotstepper/steps/implementation.rb +39 -0
  50. data/lib/hotstepper/steps/step.rb +26 -0
  51. data/lib/hotstepper/steps/watcher.rb +23 -0
  52. data/lib/hotstepper/steps/wrapper.rb +16 -0
  53. data/lib/hotstepper/struct.rb +30 -0
  54. data/lib/hotstepper/support/file_wrapper.rb +75 -0
  55. data/lib/hotstepper/support/generators/common.rb +39 -0
  56. data/lib/hotstepper/support/generators/namespaced.rb +23 -0
  57. data/lib/hotstepper/support/generators/service_operations.rb +14 -0
  58. data/lib/hotstepper/support/generators/step_definition.rb +45 -0
  59. data/lib/hotstepper/support/namespace_wrapper.rb +73 -0
  60. data/lib/hotstepper/support/null_service.rb +14 -0
  61. data/lib/hotstepper/support/types.rb +26 -0
  62. data/lib/hotstepper/testing/do_for.rb +23 -0
  63. data/lib/hotstepper/testing/operations.rb +88 -0
  64. data/lib/hotstepper/testing/typing.rb +18 -0
  65. data/lib/hotstepper/testing.rb +24 -0
  66. data/lib/hotstepper/typespace.rb +108 -0
  67. data/lib/hotstepper/typing.rb +69 -0
  68. data/lib/hotstepper/version.rb +5 -0
  69. data/lib/hotstepper.rb +56 -0
  70. data/lib/tasks/hotstepper_tasks.rake +6 -0
  71. data/lib/testing/contexts/operations.rb +13 -0
  72. data/lib/testing/matchers/00_disable_with.rb +22 -0
  73. data/lib/testing/matchers/dry_validation.rb +94 -0
  74. data/lib/testing/matchers/monadic.rb +107 -0
  75. data/lib/testing/matchers/negated.rb +8 -0
  76. metadata +377 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0b07ab5ad5f621350aee538dc060af2fc9d979834b5c2d9fc39ff5bb7f710194
4
+ data.tar.gz: 5c05a8274192ab24505ab8f5e2badf3a87c04876d7d2694152747640c25b722f
5
+ SHA512:
6
+ metadata.gz: b8ff6783c1f8d2055281bf584ee022b86556637bab39e8dd354526e12fa3f8f1c7cff83e63a1009c4438c183aa1396255fc68cde8fdcc626bea2adf735ae98f4
7
+ data.tar.gz: 9f3dea435adb87f761e4f6a00e807b1568248eae2a990e6359c06457a4ebbdc3b3623e445fa1fd484964faf8d42bd5e5492706bb67a9108f160201b26cf78108
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright Alexa Grey
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # Hotstepper
2
+
3
+ Hotstepper is a slightly opinionated framework that is built atop Rails and
4
+ combines several [dry-rb](https://dry-rb.org/) libraries in order to simplify
5
+ certain service-oriented design patterns. These are patterns I've developed
6
+ over years and like Rails itself, hotstepper been extracted from use in
7
+ real-world applications.
8
+
9
+ ## Usage
10
+
11
+ While the code in here is stable and tested, this is not really intended for public consumption yet.
12
+
13
+ Documentation will come at a later date.
14
+
15
+ ## Installation
16
+ Add this line to your application's Gemfile:
17
+
18
+ ```ruby
19
+ gem "hotstepper"
20
+ ```
21
+
22
+ And then execute:
23
+ ```bash
24
+ $ bundle
25
+ ```
26
+
27
+ Or install it yourself as:
28
+ ```bash
29
+ $ gem install hotstepper
30
+ ```
31
+
32
+ ## Contributing
33
+ Contribution directions go here.
34
+
35
+ ## License
36
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+
5
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
6
+ load "rails/tasks/engine.rake"
7
+
8
+ require "bundler/gem_tasks"
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A validator for ensuring that a given array has only unique items.
4
+ #
5
+ # It relies on [`Array#uniq`](https://ruby-doc.org/core-3.0.2/Array.html#method-i-uniq),
6
+ # so if checking an array of POROs, ensure that their `#eql?` methods are implemented
7
+ # correctly. (`Dry::Core::Equalizer` provides this out-of-the-box for most use cases).
8
+ class UniqueItemsValidator < ActiveModel::EachValidator
9
+ # @param [ActiveModel::Validations, #errors] record
10
+ # @param [Symbol] attribute
11
+ # @param [Array, #to_a] value
12
+ # @return [void]
13
+ def validate_each(record, attribute, value)
14
+ # :nocov:
15
+ return if value.blank?
16
+ # :nocov:
17
+
18
+ arr = Array(value)
19
+
20
+ unique_count = arr.uniq.size
21
+
22
+ total_count = arr.size
23
+
24
+ diff = total_count - unique_count
25
+
26
+ record.errors.add attribute, :duplicate_items, count: diff unless diff.zero?
27
+ end
28
+
29
+ # @api private
30
+ # @note Used in tests.
31
+ class TestModel
32
+ include ActiveModel::Model
33
+ include ActiveModel::Validations
34
+
35
+ # @return [Array]
36
+ attr_accessor :items
37
+
38
+ validates :items, unique_items: true
39
+ end
40
+ end
@@ -0,0 +1,70 @@
1
+ # custom predicates implemented for dry-logic
2
+ # we can't use YAML aliases because psych requires it to be turned on somewhere else
3
+ # than the Rails engine.
4
+ en:
5
+ dry_schema:
6
+ errors:
7
+ any_uuid?: "must be a UUID"
8
+
9
+ email?: "must be an email"
10
+
11
+ global_id?: "must be a GlobalID"
12
+
13
+ global_id_uri?: "must be a GlobalID URI"
14
+
15
+ has_model_name?: "must be a named ActiveModel object"
16
+
17
+ http_uri?: "must be a valid HTTP or HTTPS url"
18
+
19
+ https_uri?: "must be a valid HTTPS url"
20
+
21
+ instance_named?: "must be an instance of %{instance_name}"
22
+
23
+ model?: "must be a model"
24
+
25
+ model_class?: "must be a model class"
26
+
27
+ model_class_list?: "must be an array of model classes"
28
+
29
+ model_list?: "must be an array of models"
30
+
31
+ model_named?: "must be a model of type %{model_name}"
32
+
33
+ rails_blank?: "must be blank"
34
+
35
+ rails_present?: "cannot be blank"
36
+
37
+ uri?: "must be a valid URI"
38
+ dry_validation:
39
+ errors:
40
+ any_uuid?: "must be a UUID"
41
+
42
+ email?: "must be an email"
43
+
44
+ global_id?: "must be a GlobalID"
45
+
46
+ global_id_uri?: "must be a GlobalID URI"
47
+
48
+ has_model_name?: "must be a named ActiveModel object"
49
+
50
+ http_uri?: "must be a valid HTTP or HTTPS url"
51
+
52
+ https_uri?: "must be a valid HTTPS url"
53
+
54
+ instance_named?: "must be an instance of %{instance_name}"
55
+
56
+ model?: "must be a model"
57
+
58
+ model_class?: "must be a model class"
59
+
60
+ model_class_list?: "must be an array of model classes"
61
+
62
+ model_list?: "must be an array of models"
63
+
64
+ model_named?: "must be a model of type %{model_name}"
65
+
66
+ rails_blank?: "must be blank"
67
+
68
+ rails_present?: "cannot be blank"
69
+
70
+ uri?: "must be a valid URI"
@@ -0,0 +1,113 @@
1
+ en:
2
+ dry_validation:
3
+ or: "or"
4
+
5
+ errors:
6
+ acceptance: "must accept %{key}"
7
+
8
+ unexpected_key: "is not allowed"
9
+
10
+ array?: "must be an array"
11
+
12
+ empty?: "must be empty"
13
+
14
+ excludes?: "must not include %{value}"
15
+
16
+ excluded_from?:
17
+ arg:
18
+ default: "must not be one of: %{list}"
19
+ range: "must not be one of: %{list_left} - %{list_right}"
20
+
21
+ exclusion?: "must not be one of: %{list}"
22
+
23
+ eql?: "must be equal to %{left}"
24
+
25
+ not_eql?: "must not be equal to %{left}"
26
+
27
+ filled?: "must be filled"
28
+
29
+ format?: "is in invalid format"
30
+
31
+ number?: "must be a number"
32
+
33
+ odd?: "must be odd"
34
+
35
+ even?: "must be even"
36
+
37
+ gt?: "must be greater than %{num}"
38
+
39
+ gteq?: "must be greater than or equal to %{num}"
40
+
41
+ hash?: "must be a hash"
42
+
43
+ included_in?:
44
+ arg:
45
+ default: "must be one of: %{list}"
46
+ range: "must be one of: %{list_left} - %{list_right}"
47
+
48
+ inclusion?: "must be one of: %{list}"
49
+
50
+ includes?: "must include %{value}"
51
+
52
+ bool?: "must be boolean"
53
+
54
+ true?: "must be true"
55
+
56
+ false?: "must be false"
57
+
58
+ int?: "must be an integer"
59
+
60
+ float?: "must be a float"
61
+
62
+ decimal?: "must be a decimal"
63
+
64
+ date?: "must be a date"
65
+
66
+ date_time?: "must be a date time"
67
+
68
+ time?: "must be a time"
69
+
70
+ key?: "is missing"
71
+
72
+ attr?: "is missing"
73
+
74
+ lt?: "must be less than %{num}"
75
+
76
+ lteq?: "must be less than or equal to %{num}"
77
+
78
+ max_size?: "size cannot be greater than %{num}"
79
+
80
+ max_bytesize?: "bytesize cannot be greater than %{num}"
81
+
82
+ min_size?: "size cannot be less than %{num}"
83
+
84
+ min_bytesize?: "bytesize cannot be less than %{num}"
85
+
86
+ nil?: "cannot be defined"
87
+
88
+ str?: "must be a string"
89
+
90
+ type?: "must be %{type}"
91
+
92
+ respond_to?: "must respond to %{method}"
93
+
94
+ size?:
95
+ arg:
96
+ default: "size must be %{size}"
97
+ range: "size must be within %{size_left} - %{size_right}"
98
+
99
+ value:
100
+ string:
101
+ arg:
102
+ default: "length must be %{size}"
103
+ range: "length must be within %{size_left} - %{size_right}"
104
+
105
+ bytesize?:
106
+ arg:
107
+ default: "must be %{size} bytes long"
108
+ range: "must be within %{size_left} - %{size_right} bytes long"
109
+
110
+ uuid_v4?: "is not a valid UUID"
111
+
112
+ not:
113
+ empty?: "cannot be empty"
@@ -0,0 +1,14 @@
1
+ ---
2
+ en:
3
+ activemodel:
4
+ errors:
5
+ models:
6
+ hotstepper/support/generators/step_definition:
7
+ attributes:
8
+ name:
9
+ invalid: "must be all lowercase, starts with a letter, and can only contain letters, numbers, and underscores"
10
+ errors:
11
+ messages:
12
+ duplicate_items:
13
+ one: "has a duplicate item"
14
+ other: "has %{count} duplicate items"
data/config/routes.rb ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ Rails.application.routes.draw do
4
+ end
data/lib/boot/setup.rb ADDED
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/all"
4
+ require "active_model"
5
+ require "active_model/callbacks"
6
+ require "active_model/model"
7
+ require "active_model/naming"
8
+ require "active_model/validations"
9
+ require "active_record"
10
+
11
+ require "dry/auto_inject"
12
+ require "dry/configurable"
13
+ require "dry/container"
14
+ require "dry/core/class_attributes"
15
+ require "dry/core/constants"
16
+ require "dry/initializer"
17
+ require "dry/matcher/result_matcher"
18
+ require "dry/monads/all"
19
+ require "dry/schema"
20
+ require "dry/struct"
21
+ require "dry/types"
22
+ require "dry/validation"
23
+
24
+ require "pathname"
25
+
26
+ require "rails/engine"
27
+
28
+ require "uri"
29
+
30
+ require "zeitwerk"
31
+
32
+ Dry::Schema.load_extensions :hints
33
+ Dry::Schema.load_extensions :info
34
+ Dry::Schema.load_extensions :monads
35
+ Dry::Types.load_extensions :monads
36
+ Dry::Validation.load_extensions :monads
37
+ Dry::Validation.load_extensions :predicates_as_macros
38
+
39
+ Dry::Logic::Predicates.predicate :any_uuid? do |input|
40
+ str?(input) && (
41
+ uuid_v1?(input) ||
42
+ uuid_v2?(input) ||
43
+ uuid_v3?(input) ||
44
+ uuid_v4?(input) ||
45
+ uuid_v5?(input) ||
46
+ uuid_v6?(input) ||
47
+ uuid_v7?(input) ||
48
+ uuid_v8?(input)
49
+ )
50
+ end
51
+
52
+ # A constraint for dry types to be able to match dry types,
53
+ # for when you need to type types typingly.
54
+ Dry::Logic::Predicates.predicate :dry_type? do |input|
55
+ type?(Dry::Types::Type, input)
56
+ end
57
+
58
+ # `#blank?` in constraints.
59
+ Dry::Logic::Predicates.predicate :rails_blank? do |input|
60
+ input.blank?
61
+ end
62
+
63
+ # `#present?` in constraints.
64
+ Dry::Logic::Predicates.predicate :rails_present? do |input|
65
+ input.present?
66
+ end
67
+
68
+ # Override the built-in `filled?` predicate to use Rails' definition of presence,
69
+ # which is more strict than Dry's default.
70
+ Dry::Logic::Predicates.predicate :filled? do |input|
71
+ rails_present?(input)
72
+ end
73
+
74
+ # This is pretty loose, since it allows things like `foo@bar`,
75
+ # but it catches most things.
76
+ Dry::Logic::Predicates.predicate :email? do |input|
77
+ str?(input) && format?(Hotstepper::Patterns::EMAIL, input)
78
+ end
79
+
80
+ # A matcher for a GlobalID URI string, like `gid://app/User/1`.
81
+ Dry::Logic::Predicates.predicate :global_id_uri? do |input|
82
+ str?(input) && format?(Hotstepper::Patterns::GlobalID, input)
83
+ end
84
+
85
+ # A matcher for a GlobalID string or instance.
86
+ Dry::Logic::Predicates.predicate :global_id? do |input|
87
+ return false if rails_blank?(input) || !(global_id_uri?(input) || type?(::GlobalID, input))
88
+
89
+ gid = GlobalID.parse input
90
+
91
+ return false if gid.blank?
92
+
93
+ begin
94
+ gid.model_class.present?
95
+ rescue NameError
96
+ false
97
+ end
98
+ end
99
+
100
+ # A matcher for an HTTP (or HTTPS) URI string, like `http://example.com`.
101
+ Dry::Logic::Predicates.predicate :http_uri? do |input|
102
+ str?(input) && format?(Hotstepper::Patterns::HTTP, input)
103
+ end
104
+
105
+ # A matcher for an HTTPS URI string, like `https://example.com`.
106
+ Dry::Logic::Predicates.predicate :https_uri? do |input|
107
+ str?(input) && format?(Hotstepper::Patterns::HTTPS, input)
108
+ end
109
+
110
+ Dry::Logic::Predicates.predicate :inherits? do |parent, input|
111
+ type?(Class, input) && input < parent
112
+ end
113
+
114
+ Dry::Logic::Predicates.predicate :model? do |input|
115
+ type?(ActiveRecord::Base, input)
116
+ end
117
+
118
+ Dry::Logic::Predicates.predicate :model_list? do |input|
119
+ array?(input) && input.all? { |elm| model? elm }
120
+ end
121
+
122
+ Dry::Logic::Predicates.predicate :model_class? do |input|
123
+ inherits?(ActiveRecord::Base, input)
124
+ end
125
+
126
+ Dry::Logic::Predicates.predicate :model_class_list? do |input|
127
+ array?(input) && input.all? { |elm| model_class?(input) }
128
+ end
129
+
130
+ Dry::Logic::Predicates.predicate :has_model_name? do |input|
131
+ attr?(:model_name, input) && !none?(input.model_name) && type?(ActiveModel::Name, input.model_name)
132
+ end
133
+
134
+ Dry::Logic::Predicates.predicate :relation? do |input|
135
+ type?(ActiveRecord::Relation, input)
136
+ end
137
+
138
+ Dry::Logic::Predicates.predicate :relation_for? do |model_klass, input|
139
+ relation?(input) && (
140
+ (model_class?(model_klass) && input.model <= model_klass) ||
141
+ (dry_type?(model_klass) && model_klass.valid?(input.model))
142
+ )
143
+ end
144
+
145
+ Dry::Logic::Predicates.predicate :instance_named? do |name, input|
146
+ input.class.name == name
147
+ end
148
+
149
+ Dry::Logic::Predicates.predicate :model_named? do |model_name, input|
150
+ has_model_name?(input) && input.model_name == model_name
151
+ end
@@ -0,0 +1,17 @@
1
+ Description:
2
+ Create a service and operation pair using Hotstepper conventions.
3
+
4
+ The operation class will be a Hotstepper::Operation that
5
+ forwards all arguments passed to `#call` on to the wrapped
6
+ service, and then calls the service instance's own `#call` method.
7
+
8
+ The service will inherit from `Hotstepper::Service` by default,
9
+ and execute its defined steps with the ability to halt execution
10
+ based on the monadic result of each step.
11
+
12
+ Example:
13
+ bin/rails generate hotstepper:service Some::Namespace Create Creator
14
+
15
+ This will create:
16
+ app/operations/some/namespace/create.rb
17
+ app/services/some/namespace/creator.rb
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hotstepper
4
+ # Generate a service and operation pair using Hotstepper conventions.
5
+ class ServiceGenerator < Rails::Generators::Base
6
+ include Hotstepper::Support::Generators::Common
7
+ include Hotstepper::Support::Generators::Namespaced
8
+
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ DEFAULT_PARENT = "Hotstepper::Service"
12
+
13
+ DEFAULT_STEPS = ["prepare"].freeze
14
+
15
+ argument :operation_name, type: :string, required: true,
16
+ desc: "a verb, e.g. Create"
17
+
18
+ argument :service_name, type: :string, required: true,
19
+ desc: "a noun, e.g. Creator"
20
+
21
+ class_option :abstract, type: :boolean, default: false,
22
+ desc: <<~TEXT.strip.squish
23
+ Whether to generate an abstract service and skip generating the operation. Implies --skip-operation and --skip-service-implementation.
24
+ TEXT
25
+
26
+ class_option :parent, type: :string, required: true,
27
+ default: DEFAULT_PARENT,
28
+ desc: "The abstract class to inherit from."
29
+
30
+ class_option :operation, type: :boolean,
31
+ desc: "Whether to generate the operation."
32
+
33
+ class_option :service_implementation, type: :boolean,
34
+ desc: "Whether to generate the service implementation. Defaults to false if --abstract or if --parent is not the default."
35
+
36
+ class_option :steps, type: :array,
37
+ desc: %{The steps to define on the service. Defaults to `["prepare"]` unless --abstract.}
38
+
39
+ class_option :matcher, type: :boolean, default: false,
40
+ desc: "Whether to activate dry-matcher on the operation."
41
+
42
+ def prepare!
43
+ set_up_namespace!
44
+
45
+ @parent_service_class = options[:parent].presence || DEFAULT_PARENT
46
+
47
+ @skip_operation = determine_whether_to_skip_operation
48
+
49
+ @skip_service_implementation = determine_whether_to_skip_service_implementation
50
+
51
+ @steps = parse_steps(options[:steps].presence || default_steps)
52
+
53
+ @operation = namespaced_file(operation_name)
54
+
55
+ @service = namespaced_file(service_name)
56
+ end
57
+
58
+ def generate_files!
59
+ validate_steps!
60
+ rescue StepsInvalid => e
61
+ say_status :error, e.message
62
+ else
63
+ unless skip_operation?
64
+ template "operation.rb", operation.to_operation_path
65
+ end
66
+
67
+ template "service.rb", service.to_service_path
68
+ end
69
+
70
+ private
71
+
72
+ def abstract? = options[:abstract]
73
+
74
+ def default_steps = inheriting_from_other_parent? ? [] : DEFAULT_STEPS
75
+
76
+ # @param [<String>] raw_steps
77
+ # @return [<Hotstepper::Support::Generators::StepDefinition>]
78
+ def parse_steps(raw_steps) = Hotstepper::Support::Generators::StepDefinition.parse_many(raw_steps)
79
+
80
+ # @return [Boolean]
81
+ def determine_whether_to_skip_operation
82
+ return options[:operation].blank? if options.key?(:operation)
83
+
84
+ abstract?
85
+ end
86
+
87
+ # @return [Boolean]
88
+ def determine_whether_to_skip_service_implementation
89
+ return options[:service_implementation].blank? if options.key?(:service_implementation)
90
+
91
+ inheriting_from_other_parent?
92
+ end
93
+
94
+ def inheriting_from_other_parent? = parent_service_class != DEFAULT_PARENT
95
+
96
+ # @return [String]
97
+ attr_reader :parent_service_class
98
+
99
+ # @return [Hotstepper::Support::FileWrapper]
100
+ attr_reader :operation
101
+
102
+ # @return [Hotstepper::Support::FileWrapper]
103
+ attr_reader :service
104
+
105
+ # @return [Boolean]
106
+ attr_reader :skip_operation
107
+
108
+ alias skip_operation? skip_operation
109
+
110
+ # @return [Boolean]
111
+ attr_reader :skip_service_implementation
112
+
113
+ alias skip_service_implementation? skip_service_implementation
114
+
115
+ # @return [<Hotstepper::Support::Generators::StepDefinition>]
116
+ attr_reader :steps
117
+
118
+ def use_dry_matcher? = options[:matcher]
119
+
120
+ def validate_steps!
121
+ _, invalid = steps.partition(&:valid?)
122
+
123
+ return if invalid.empty?
124
+
125
+ invalid.each do |step|
126
+ say_status :error, "Invalid step: #{step.name.inspect}"
127
+ step.errors.full_messages.each do |message|
128
+ say_status :error, " #{message}"
129
+ end
130
+ end
131
+
132
+ raise StepsInvalid, "One or more step definitions were invalid. See above for details."
133
+ end
134
+
135
+ class StepsInvalid < StandardError; end
136
+ end
137
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ <%- nest_under_namespace do -%>
4
+ # @see <%= service.full_name %>
5
+ # @!method call
6
+ # @return [Dry::Monads::Result]
7
+ class <%= operation.name %> < Hotstepper::Operation
8
+ wraps_service! ::<%= service.full_name %><%- if use_dry_matcher? -%>, with_matcher: true<%- end %>
9
+ end
10
+ <%- end -%>
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ <%- nest_under_namespace do -%>
4
+ <%- if abstract? -%>
5
+ # @abstract
6
+ <%- end -%>
7
+ <%- unless skip_operation? -%>
8
+ # @see <%= operation.full_name %>
9
+ <%- end -%>
10
+ class <%= service.name %> < <%= parent_service_class %>
11
+ <%- unless skip_service_implementation? -%>
12
+ include Dry::Initializer[undefined: false].define -> do
13
+ end
14
+
15
+ # @return [Dry::Monads::Result]
16
+ def call
17
+ run_callbacks :execute do
18
+ <%- steps.each do |step| -%>
19
+ yield <%= step.name! %>
20
+ <%- end -%>
21
+ end
22
+
23
+ Success()
24
+ end
25
+ <%- end -%>
26
+ <%- steps.each do |step| -%>
27
+ <%- unless skip_service_implementation? && step.index == 0 -%>
28
+
29
+ <%- end -%>
30
+ <%- if step.abstract? -%>
31
+ step! <%= step.name.inspect %>
32
+ <%- else -%>
33
+ step! def <%= step.name %>
34
+ Success()
35
+ end
36
+ <%- end -%>
37
+ <%- end -%>
38
+ end
39
+ <%- end -%>
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Generate a typespace within a namespace for a subdivision of the application.
3
+
4
+ Example:
5
+ bin/rails generate typespace Things
6
+
7
+ This will create:
8
+ app/services/things/types.rb
@@ -0,0 +1,5 @@
1
+ <%- nest_under_namespace do -%>
2
+ module <%= types.name %>
3
+ extend Hotstepper::Typespace
4
+ end
5
+ <%- end -%>