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
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hotstepper
|
|
4
|
+
module Monadic
|
|
5
|
+
module Coercion
|
|
6
|
+
include Dry::Monads[:maybe, :result, :validated]
|
|
7
|
+
|
|
8
|
+
# @api private
|
|
9
|
+
NO_ARG = Object.new.freeze
|
|
10
|
+
|
|
11
|
+
# @param [Object] input
|
|
12
|
+
# @param [:maybe, :result, :validated] mode
|
|
13
|
+
# @return [Dry::Monads::Maybe, Dry::Monads::Result, Dry::Monads::Validated]
|
|
14
|
+
def call(input, mode:)
|
|
15
|
+
case mode
|
|
16
|
+
in :maybe
|
|
17
|
+
coerce_to_maybe(input)
|
|
18
|
+
in :result
|
|
19
|
+
coerce_to_result(input)
|
|
20
|
+
in :validated
|
|
21
|
+
coerce_to_validated(input)
|
|
22
|
+
else
|
|
23
|
+
# :nocov:
|
|
24
|
+
raise ArgumentError, "invalid mode #{mode.inspect}"
|
|
25
|
+
# :nocov:
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @param [Object] input
|
|
30
|
+
# @yieldreturn [Object] the value to be converted to a monad (if necessary)
|
|
31
|
+
# @return [Dry::Monads::Maybe, Dry::Monads::Result, Dry::Monads::Validated]
|
|
32
|
+
def enforce(input = NO_ARG, mode:)
|
|
33
|
+
if input.equal?(NO_ARG)
|
|
34
|
+
# :nocov:
|
|
35
|
+
raise ArgumentError, "no argument given and no block provided" unless block_given?
|
|
36
|
+
# :nocov:
|
|
37
|
+
|
|
38
|
+
return call(yield, mode:)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
call(input, mode:)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def to_maybe(input = NO_ARG, &) = enforce(input, mode: :maybe, &)
|
|
45
|
+
|
|
46
|
+
def to_result(input = NO_ARG, &) = enforce(input, mode: :result, &)
|
|
47
|
+
|
|
48
|
+
def to_validated(input = NO_ARG, &) = enforce(input, mode: :validated, &)
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
# @param [Object] input
|
|
53
|
+
# @return [Dry::Monads::Maybe]
|
|
54
|
+
def coerce_to_maybe(input)
|
|
55
|
+
return input if input.kind_of?(Dry::Monads::Maybe)
|
|
56
|
+
|
|
57
|
+
input.to_monad.then do |monad|
|
|
58
|
+
return monad if monad.kind_of?(Dry::Monads::Maybe)
|
|
59
|
+
|
|
60
|
+
return coerce_to_maybe(monad) if monad != input
|
|
61
|
+
end if input.respond_to?(:to_monad)
|
|
62
|
+
|
|
63
|
+
input.to_maybe.then do |maybe|
|
|
64
|
+
return maybe if maybe.kind_of?(Dry::Monads::Maybe)
|
|
65
|
+
end if input.respond_to?(:to_maybe)
|
|
66
|
+
|
|
67
|
+
Maybe(input)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @param [Object] input
|
|
71
|
+
# @return [Dry::Monads::Result]
|
|
72
|
+
def coerce_to_result(input)
|
|
73
|
+
return input if input.kind_of?(Dry::Monads::Result)
|
|
74
|
+
|
|
75
|
+
input.to_monad.then do |monad|
|
|
76
|
+
return monad if monad.kind_of?(Dry::Monads::Result)
|
|
77
|
+
return coerce_to_result(monad) if monad != input
|
|
78
|
+
end if input.respond_to?(:to_monad)
|
|
79
|
+
|
|
80
|
+
input.to_result.then do |result|
|
|
81
|
+
return result if result.kind_of?(Dry::Monads::Result)
|
|
82
|
+
end if input.respond_to?(:to_result)
|
|
83
|
+
|
|
84
|
+
Success input
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @param [Object] input
|
|
88
|
+
# @return [Dry::Monads::Validated]
|
|
89
|
+
def coerce_to_validated(input)
|
|
90
|
+
return input if input.kind_of?(Dry::Monads::Validated)
|
|
91
|
+
|
|
92
|
+
# Treat Nones as Invalids when coercing to Validated, since Validated doesn't have a concept of None.
|
|
93
|
+
return Invalid(:none) if input.kind_of?(Dry::Monads::Maybe) && input.none?
|
|
94
|
+
|
|
95
|
+
# Treat Somes as Valids when coercing to Validated, since Validated doesn't have a concept of Some.
|
|
96
|
+
return Valid(input.value!) if input.kind_of?(Dry::Monads::Maybe) && input.some?
|
|
97
|
+
|
|
98
|
+
input.to_monad.then do |monad|
|
|
99
|
+
return monad if monad.kind_of?(Dry::Monads::Validated)
|
|
100
|
+
return coerce_to_validated(monad) if monad != input
|
|
101
|
+
end if input.respond_to?(:to_monad)
|
|
102
|
+
|
|
103
|
+
input.to_validated.then do |validated|
|
|
104
|
+
return validated if validated.kind_of?(Dry::Monads::Validated)
|
|
105
|
+
end if input.respond_to?(:to_validated)
|
|
106
|
+
|
|
107
|
+
Valid(input)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hotstepper
|
|
4
|
+
module Monadic
|
|
5
|
+
# A concern that should be `extend`ed on classes to define monadic operations
|
|
6
|
+
# on them. It is used to DRY up the definition of `!` methods for monadic
|
|
7
|
+
# operations, matchers, and validatators.
|
|
8
|
+
module Decorators
|
|
9
|
+
ON_VALID_RESULT = proc {}
|
|
10
|
+
|
|
11
|
+
private_constant :ON_VALID_RESULT
|
|
12
|
+
|
|
13
|
+
# @param [Symbol] method_name
|
|
14
|
+
# @param [Class] with a Dry::Matcher class
|
|
15
|
+
# @return [void]
|
|
16
|
+
def monadic_matcher!(method_name, with: Dry::Matcher::ResultMatcher)
|
|
17
|
+
include Dry::Matcher.for(method_name, with:)
|
|
18
|
+
|
|
19
|
+
monadic_operation!(method_name)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @param [Symbol] method_name
|
|
23
|
+
# @return [void]
|
|
24
|
+
def monadic_operation!(method_name)
|
|
25
|
+
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
26
|
+
def #{method_name}!(...)
|
|
27
|
+
#{method_name}(...).value!.then do |result|
|
|
28
|
+
Dry::Monads::Unit == result ? nil : result
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
RUBY
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param [Symbol] method_name
|
|
35
|
+
# @return [void]
|
|
36
|
+
def monadic_validator!(method_name)
|
|
37
|
+
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
38
|
+
def #{method_name}!(...)
|
|
39
|
+
enumerate_reasons = ->(reasons) do
|
|
40
|
+
reasons.to_a.each do |reason|
|
|
41
|
+
yield reason if block_given?
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
#{method_name}(...).either(
|
|
46
|
+
ON_VALID_RESULT,
|
|
47
|
+
enumerate_reasons
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
RUBY
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class << self
|
|
54
|
+
# @return [void]
|
|
55
|
+
def included(base)
|
|
56
|
+
raise "This module is meant to be extended, not included"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hotstepper
|
|
4
|
+
module Monadic
|
|
5
|
+
# A module that gives you a `do_for!` decorator
|
|
6
|
+
module DoFor
|
|
7
|
+
# @param [Symbol] name
|
|
8
|
+
# @return [Symbol]
|
|
9
|
+
def do_for!(name)
|
|
10
|
+
include Dry::Monads::Do.for name
|
|
11
|
+
|
|
12
|
+
# If this is being called as a decorator, the method is already defined and dry-monad's
|
|
13
|
+
# method watcher won't fire. We trigger it manually.
|
|
14
|
+
if method_defined?(name) || protected_method_defined?(name) || private_method_defined?(name)
|
|
15
|
+
__send__(:method_added, name)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
name
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hotstepper
|
|
4
|
+
# @abstract
|
|
5
|
+
#
|
|
6
|
+
# A base class for defining an operation that takes its args
|
|
7
|
+
# and provides them to a specific service.
|
|
8
|
+
#
|
|
9
|
+
# The operation is automatically set up to support dry-matcher result matching.
|
|
10
|
+
#
|
|
11
|
+
# It allows services to be dependency-injected with dry-rails / dry-system.
|
|
12
|
+
class Operation
|
|
13
|
+
extend Dry::Core::ClassAttributes
|
|
14
|
+
|
|
15
|
+
defines :service_klass, type: Hotstepper::Support::Types::ServiceClass
|
|
16
|
+
|
|
17
|
+
service_klass Hotstepper::Support::NullService
|
|
18
|
+
|
|
19
|
+
# @return [Object] whatever the service class' call method returns,
|
|
20
|
+
# which is often a `Dry::Monads::Result` of some kind.
|
|
21
|
+
def call(...) = self.class.service_klass.new(...).call
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
# @api private
|
|
25
|
+
# @return [void]
|
|
26
|
+
def set_up_matcher!
|
|
27
|
+
include Dry::Matcher.for(:call, with: Dry::Matcher::ResultMatcher)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @param [Class] klass
|
|
31
|
+
# @param [Boolean] with_matcher whether to set up `Dry::Matcher::ResultMatcher`
|
|
32
|
+
# for fluent handling of the service's result.
|
|
33
|
+
# @return [void]
|
|
34
|
+
def wraps_service!(klass, with_matcher: false)
|
|
35
|
+
service_klass klass
|
|
36
|
+
|
|
37
|
+
set_up_matcher! if with_matcher
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hotstepper
|
|
4
|
+
module Patterns
|
|
5
|
+
# @return [Regexp]
|
|
6
|
+
EMAIL = URI::MailTo::EMAIL_REGEXP
|
|
7
|
+
|
|
8
|
+
# @return [Regexp]
|
|
9
|
+
GlobalID = URI::DEFAULT_PARSER.make_regexp("gid")
|
|
10
|
+
|
|
11
|
+
# @return [Regexp]
|
|
12
|
+
HTTP = URI::DEFAULT_PARSER.make_regexp(/https?/)
|
|
13
|
+
|
|
14
|
+
# @return [Regexp]
|
|
15
|
+
HTTPS = URI::DEFAULT_PARSER.make_regexp("https")
|
|
16
|
+
|
|
17
|
+
# @return [Regexp]
|
|
18
|
+
STEP_NAME = /\A[a-zA-Z_]\w+[a-z0-9]*\z/
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hotstepper
|
|
4
|
+
# Methods for working directly with SQL queries in operations and services.
|
|
5
|
+
#
|
|
6
|
+
# To use this module, include it in your ruby class and then call the `sql_*` methods
|
|
7
|
+
# to execute SQL queries.
|
|
8
|
+
# * The `compile_*` methods can be used to build SQL query strings
|
|
9
|
+
# from template strings
|
|
10
|
+
# * The `with_*` methods can be used to conditionally format SQL templates based
|
|
11
|
+
# on the presence of their inputs
|
|
12
|
+
module QueryBuilding
|
|
13
|
+
extend ActiveSupport::Concern
|
|
14
|
+
|
|
15
|
+
included do
|
|
16
|
+
include Dry::Monads[:result]
|
|
17
|
+
|
|
18
|
+
delegate :quote_column_name, to: :connection
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @param [<String>] parts
|
|
22
|
+
# @return [void]
|
|
23
|
+
def sql_select!(*parts)
|
|
24
|
+
ApplicationRecord.uncached do
|
|
25
|
+
connection.exec_query compile_query(*parts)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
alias sql_exec! sql_select!
|
|
30
|
+
|
|
31
|
+
# @param [<String>] parts
|
|
32
|
+
# @return [void]
|
|
33
|
+
def sql_insert!(*parts)
|
|
34
|
+
# We use exec_query instead of exec_insert,
|
|
35
|
+
# because we want control over RETURNING
|
|
36
|
+
connection.exec_query compile_query(*parts)
|
|
37
|
+
ensure
|
|
38
|
+
clear_query_cache!
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @param [<String>] parts
|
|
42
|
+
# @return [Integer]
|
|
43
|
+
def sql_update!(*parts)
|
|
44
|
+
connection.exec_update compile_query(*parts)
|
|
45
|
+
ensure
|
|
46
|
+
clear_query_cache!
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @param [<String>] parts
|
|
50
|
+
# @return [Integer]
|
|
51
|
+
def sql_delete!(*parts)
|
|
52
|
+
connection.exec_delete compile_query(*parts)
|
|
53
|
+
ensure
|
|
54
|
+
clear_query_cache!
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @param [Symbol] table_name
|
|
58
|
+
# @return [Arel::Table]
|
|
59
|
+
def arel_table_for(table_name) = Arel::Table.new(table_name)
|
|
60
|
+
|
|
61
|
+
# @api private
|
|
62
|
+
# @return [ActiveRecord::ConnectionAdapters::PostgreSQLAdapter]
|
|
63
|
+
def connection = Hotstepper.config.db.connection_class.connection
|
|
64
|
+
|
|
65
|
+
# Join components of a query with the separator value in `join_with`.
|
|
66
|
+
#
|
|
67
|
+
# @param [<String>] parts
|
|
68
|
+
# @param [String] join_with
|
|
69
|
+
# @param [Boolean] prefix
|
|
70
|
+
# @return [String]
|
|
71
|
+
def compile_query(*parts, join_with: " ", prefix_with: join_with, prefix: false)
|
|
72
|
+
compiled = parts.flatten.compact_blank.join(join_with).strip
|
|
73
|
+
|
|
74
|
+
return compiled unless prefix && compiled.present?
|
|
75
|
+
|
|
76
|
+
"#{prefix_with.presence || join_with}#{compiled}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Join components of a query with ` AND `.
|
|
80
|
+
#
|
|
81
|
+
# @param [<String>] parts
|
|
82
|
+
# @param [Boolean] prefix
|
|
83
|
+
# @return [String]
|
|
84
|
+
def compile_and(*parts, prefix: false, **options) = compile_query(*parts, **options, join_with: " AND ", prefix:)
|
|
85
|
+
|
|
86
|
+
# Join components of a query with ` OR `.
|
|
87
|
+
#
|
|
88
|
+
# @param [<String>] parts
|
|
89
|
+
# @param [Boolean] prefix
|
|
90
|
+
# @return [String]
|
|
91
|
+
def compile_or(*parts, prefix: false, **options) = compile_query(*parts, **options, join_with: " OR ", prefix:)
|
|
92
|
+
|
|
93
|
+
# Provide the quoted model id as a formatting argument
|
|
94
|
+
# to the `template`.
|
|
95
|
+
#
|
|
96
|
+
# @param [ApplicationRecord] model
|
|
97
|
+
# @param [String] template
|
|
98
|
+
# @return [String]
|
|
99
|
+
def with_quoted_id_for(model, template)
|
|
100
|
+
return "" if model.blank? || model.new_record? || model.destroyed?
|
|
101
|
+
|
|
102
|
+
with_sql_template template, model.quoted_id
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# @param [#to_s] prefix
|
|
106
|
+
# @param [<String>, String] id_or_ids
|
|
107
|
+
# @param [#to_s] column
|
|
108
|
+
# @return [String]
|
|
109
|
+
def with_quoted_id_or_ids_on(prefix, id_or_ids, column: :id)
|
|
110
|
+
case id_or_ids
|
|
111
|
+
when ::Hotstepper::Support::Types::UUIDList
|
|
112
|
+
arel_table_for(prefix)[column].in(id_or_ids).to_sql
|
|
113
|
+
when ::Hotstepper::Support::Types::UUID
|
|
114
|
+
arel_table_for(prefix)[column].eq(id_or_ids).to_sql
|
|
115
|
+
else
|
|
116
|
+
""
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# @param [String] template
|
|
121
|
+
# @param [<Object>] values format values for `String#%`
|
|
122
|
+
# @return [String]
|
|
123
|
+
def with_sql_template(template, *values)
|
|
124
|
+
formatted = template % values
|
|
125
|
+
|
|
126
|
+
cleanup_query formatted
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
# @param [String] sql
|
|
132
|
+
# @return [String]
|
|
133
|
+
def cleanup_query(sql)
|
|
134
|
+
sql.strip_heredoc.squish
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# @return [void]
|
|
138
|
+
def clear_query_cache!
|
|
139
|
+
connection.clear_query_cache
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hotstepper
|
|
4
|
+
# @abstract
|
|
5
|
+
class Service
|
|
6
|
+
extend ActiveModel::Callbacks
|
|
7
|
+
extend Dry::Core::ClassAttributes
|
|
8
|
+
extend Hotstepper::Monadic::DoFor
|
|
9
|
+
|
|
10
|
+
include Dry::Core::Constants
|
|
11
|
+
include Dry::Monads[:result, :maybe, :try]
|
|
12
|
+
|
|
13
|
+
include Hotstepper::Common::CallsOperation
|
|
14
|
+
|
|
15
|
+
defines :step_mapping, type: Hotstepper::Steps::Step::Map
|
|
16
|
+
|
|
17
|
+
step_mapping EMPTY_HASH
|
|
18
|
+
|
|
19
|
+
do_for! :call
|
|
20
|
+
|
|
21
|
+
define_model_callbacks :execute
|
|
22
|
+
|
|
23
|
+
# @return [Symbol]
|
|
24
|
+
attr_reader :current_step
|
|
25
|
+
|
|
26
|
+
def call
|
|
27
|
+
# :nocov:
|
|
28
|
+
raise NotImplementedError, "Subclasses of Hotstepper::Service must implement #call"
|
|
29
|
+
# :nocov:
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @api private
|
|
33
|
+
def inspect
|
|
34
|
+
# :nocov:
|
|
35
|
+
"#<#{self.class}>"
|
|
36
|
+
# :nocov:
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @api private
|
|
40
|
+
# @return [void]
|
|
41
|
+
def __run_step__(step_name)
|
|
42
|
+
@current_step = step_name
|
|
43
|
+
|
|
44
|
+
run_callbacks step_name do
|
|
45
|
+
Hotstepper::Monadic.to_result do
|
|
46
|
+
__send__(step_name)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
ensure
|
|
50
|
+
@current_step = nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class << self
|
|
54
|
+
# @param [Class] subclass
|
|
55
|
+
# @return [void]
|
|
56
|
+
def inherited(subclass)
|
|
57
|
+
super
|
|
58
|
+
|
|
59
|
+
subclass.include Dry::Monads::Do.for(:call)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Defines a step on this service.
|
|
63
|
+
# @param [Symbol] name
|
|
64
|
+
# @return [Symbol]
|
|
65
|
+
# @!macro [attach] step!
|
|
66
|
+
# @!method $1
|
|
67
|
+
# The actual business logic of the step method.
|
|
68
|
+
# @return [Dry::Monads::Result]
|
|
69
|
+
# @!method $1!
|
|
70
|
+
# A wrapper around {$1} that ensures its retval is monadic, runs callbacks,
|
|
71
|
+
# and sets the current step.
|
|
72
|
+
# @return [Dry::Monads::Result]
|
|
73
|
+
def step!(name)
|
|
74
|
+
step = Hotstepper::Steps::Step.new(name:)
|
|
75
|
+
|
|
76
|
+
new_mapping = step_mapping.merge(step.name => step).freeze
|
|
77
|
+
|
|
78
|
+
step_mapping new_mapping
|
|
79
|
+
|
|
80
|
+
include step.implementation
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hotstepper
|
|
4
|
+
module Steps
|
|
5
|
+
# @api private
|
|
6
|
+
# @abstract
|
|
7
|
+
class AbstractModule < Module
|
|
8
|
+
include Dry::Initializer[undefined: false].define -> do
|
|
9
|
+
param :step, Hotstepper::Steps::Step::Type
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
delegate :name, to: :step, prefix: true
|
|
13
|
+
|
|
14
|
+
def inspect
|
|
15
|
+
# :nocov:
|
|
16
|
+
"#{self.class}[#{step_name.inspect}]"
|
|
17
|
+
# :nocov:
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hotstepper
|
|
4
|
+
module Steps
|
|
5
|
+
# @api private
|
|
6
|
+
class Implementation < AbstractModule
|
|
7
|
+
def initialize(...)
|
|
8
|
+
super
|
|
9
|
+
|
|
10
|
+
@wrapper = Hotstepper::Steps::Wrapper.new(step)
|
|
11
|
+
@watcher = Hotstepper::Steps::Watcher.new(step)
|
|
12
|
+
|
|
13
|
+
define_methods!
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def included(base)
|
|
17
|
+
super
|
|
18
|
+
|
|
19
|
+
base.include Dry::Monads::Do.for(step_name)
|
|
20
|
+
base.define_model_callbacks step_name
|
|
21
|
+
base.extend @watcher
|
|
22
|
+
base.prepend @wrapper
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# @return [void]
|
|
28
|
+
def define_methods!
|
|
29
|
+
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
30
|
+
def #{step_name} = Dry::Monads.Success()
|
|
31
|
+
|
|
32
|
+
def #{step_name}!
|
|
33
|
+
__run_step__(#{step_name.inspect})
|
|
34
|
+
end
|
|
35
|
+
RUBY
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hotstepper
|
|
4
|
+
module Steps
|
|
5
|
+
# A step is a single unit of work in a service. It is defined as a method on the service.
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
8
|
+
class Step < Hotstepper::Struct
|
|
9
|
+
include Dry::Core::Equalizer.new(:name)
|
|
10
|
+
include Hotstepper::Typing
|
|
11
|
+
|
|
12
|
+
map_type!(key: Hotstepper::Support::Types::Symbol)
|
|
13
|
+
|
|
14
|
+
attribute :name, Hotstepper::Support::Types::StepName
|
|
15
|
+
|
|
16
|
+
# @return [Hotstepper::Steps::Implementation]
|
|
17
|
+
attr_reader :implementation
|
|
18
|
+
|
|
19
|
+
def initialize(...)
|
|
20
|
+
super
|
|
21
|
+
|
|
22
|
+
@implementation = Hotstepper::Steps::Implementation.new(self)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hotstepper
|
|
4
|
+
module Steps
|
|
5
|
+
# A module that is included into the step class to apply the `Dry::Monads::Do`
|
|
6
|
+
# mixin onto it whenever the class is subclassed.
|
|
7
|
+
#
|
|
8
|
+
# @api private
|
|
9
|
+
class Watcher < Hotstepper::Steps::AbstractModule
|
|
10
|
+
def initialize(...)
|
|
11
|
+
super
|
|
12
|
+
|
|
13
|
+
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
14
|
+
def inherited(subclass)
|
|
15
|
+
super
|
|
16
|
+
|
|
17
|
+
subclass.do_for! #{step_name.inspect}
|
|
18
|
+
end
|
|
19
|
+
RUBY
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hotstepper
|
|
4
|
+
module Steps
|
|
5
|
+
# A module that is prepended into the step class to watch for overrides
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
8
|
+
class Wrapper < Hotstepper::Steps::AbstractModule
|
|
9
|
+
def prepended(base)
|
|
10
|
+
super
|
|
11
|
+
|
|
12
|
+
Dry::Monads::Do.wrap_method(self, step_name, :public)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hotstepper
|
|
4
|
+
# An opinionated dry-struct implementation that accepts stringified keys,
|
|
5
|
+
# and will apply defaults when explicitly passed `nil`
|
|
6
|
+
# on keys where applicable.
|
|
7
|
+
#
|
|
8
|
+
# @abstract
|
|
9
|
+
# @see https://dry-rb.org/gems/dry-struct/main/
|
|
10
|
+
class Struct < Dry::Struct
|
|
11
|
+
transform_keys(&:to_sym)
|
|
12
|
+
|
|
13
|
+
transform_types do |type|
|
|
14
|
+
if type.default?
|
|
15
|
+
type.constructor do |value|
|
|
16
|
+
value.nil? ? Dry::Types::Undefined : value
|
|
17
|
+
end
|
|
18
|
+
else
|
|
19
|
+
type
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @param [Array<Symbol, String>] keys
|
|
24
|
+
def slice(*keys)
|
|
25
|
+
keys.flatten.index_with { public_send _1 }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
alias deconstruct_keys slice
|
|
29
|
+
end
|
|
30
|
+
end
|