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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +36 -0
- data/Rakefile +8 -0
- data/app/validators/unique_items_validator.rb +40 -0
- data/config/locales/custom-predicates.en.yml +70 -0
- data/config/locales/dry-validation-errors.en.yml +113 -0
- data/config/locales/en.yml +14 -0
- data/config/routes.rb +4 -0
- data/lib/boot/setup.rb +151 -0
- data/lib/generators/hotstepper/service/USAGE +17 -0
- data/lib/generators/hotstepper/service/service_generator.rb +137 -0
- data/lib/generators/hotstepper/service/templates/operation.rb.tt +10 -0
- data/lib/generators/hotstepper/service/templates/service.rb.tt +39 -0
- data/lib/generators/hotstepper/typespace/USAGE +8 -0
- data/lib/generators/hotstepper/typespace/templates/types.rb.tt +5 -0
- data/lib/generators/hotstepper/typespace/typespace_generator.rb +25 -0
- data/lib/hotstepper/arel_ext/array_encoding.rb +66 -0
- data/lib/hotstepper/arel_ext/base_helpers.rb +220 -0
- data/lib/hotstepper/arel_ext/helpers.rb +17 -0
- data/lib/hotstepper/arel_ext/json_encoding.rb +49 -0
- data/lib/hotstepper/arel_ext/ltree_encoding.rb +52 -0
- data/lib/hotstepper/arel_ext/postgres_helpers.rb +34 -0
- data/lib/hotstepper/arel_ext/types.rb +37 -0
- data/lib/hotstepper/arel_ext/value_select_query.rb +142 -0
- data/lib/hotstepper/common.rb +52 -0
- data/lib/hotstepper/engine.rb +38 -0
- data/lib/hotstepper/models/association_helpers.rb +141 -0
- data/lib/hotstepper/models/checks_before_destruction.rb +35 -0
- data/lib/hotstepper/models/id_quoting.rb +23 -0
- data/lib/hotstepper/models/lazy_ordering.rb +57 -0
- data/lib/hotstepper/models/limit_to_one.rb +35 -0
- data/lib/hotstepper/models/lookup_helpers.rb +21 -0
- data/lib/hotstepper/models/materialized_view.rb +39 -0
- data/lib/hotstepper/models/model.rb +40 -0
- data/lib/hotstepper/models/querying.rb +55 -0
- data/lib/hotstepper/models/timestamp_scopes.rb +26 -0
- data/lib/hotstepper/models/view.rb +14 -0
- data/lib/hotstepper/models/where_matches.rb +67 -0
- data/lib/hotstepper/monadic/coercion.rb +111 -0
- data/lib/hotstepper/monadic/decorators.rb +61 -0
- data/lib/hotstepper/monadic/do_for.rb +22 -0
- data/lib/hotstepper/monadic.rb +8 -0
- data/lib/hotstepper/operation.rb +41 -0
- data/lib/hotstepper/patterns.rb +20 -0
- data/lib/hotstepper/query_building.rb +142 -0
- data/lib/hotstepper/service.rb +84 -0
- data/lib/hotstepper/steps/abstract_module.rb +21 -0
- data/lib/hotstepper/steps/implementation.rb +39 -0
- data/lib/hotstepper/steps/step.rb +26 -0
- data/lib/hotstepper/steps/watcher.rb +23 -0
- data/lib/hotstepper/steps/wrapper.rb +16 -0
- data/lib/hotstepper/struct.rb +30 -0
- data/lib/hotstepper/support/file_wrapper.rb +75 -0
- data/lib/hotstepper/support/generators/common.rb +39 -0
- data/lib/hotstepper/support/generators/namespaced.rb +23 -0
- data/lib/hotstepper/support/generators/service_operations.rb +14 -0
- data/lib/hotstepper/support/generators/step_definition.rb +45 -0
- data/lib/hotstepper/support/namespace_wrapper.rb +73 -0
- data/lib/hotstepper/support/null_service.rb +14 -0
- data/lib/hotstepper/support/types.rb +26 -0
- data/lib/hotstepper/testing/do_for.rb +23 -0
- data/lib/hotstepper/testing/operations.rb +88 -0
- data/lib/hotstepper/testing/typing.rb +18 -0
- data/lib/hotstepper/testing.rb +24 -0
- data/lib/hotstepper/typespace.rb +108 -0
- data/lib/hotstepper/typing.rb +69 -0
- data/lib/hotstepper/version.rb +5 -0
- data/lib/hotstepper.rb +56 -0
- data/lib/tasks/hotstepper_tasks.rake +6 -0
- data/lib/testing/contexts/operations.rb +13 -0
- data/lib/testing/matchers/00_disable_with.rb +22 -0
- data/lib/testing/matchers/dry_validation.rb +94 -0
- data/lib/testing/matchers/monadic.rb +107 -0
- data/lib/testing/matchers/negated.rb +8 -0
- 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,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
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 -%>
|