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,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hotstepper
|
|
4
|
+
module Support
|
|
5
|
+
# A wrapper around a file that we want to generate, which knows how to generate
|
|
6
|
+
# its own path and name based on the namespace it's in.
|
|
7
|
+
class FileWrapper
|
|
8
|
+
include Dry::Core::Equalizer.new(:namespace, :name)
|
|
9
|
+
|
|
10
|
+
# @return [String]
|
|
11
|
+
TAB = " "
|
|
12
|
+
|
|
13
|
+
private_constant :TAB
|
|
14
|
+
|
|
15
|
+
# @return [String]
|
|
16
|
+
attr_reader :extension
|
|
17
|
+
|
|
18
|
+
# @return [String]
|
|
19
|
+
attr_reader :filename
|
|
20
|
+
|
|
21
|
+
# The full constant name of the class or module, including its namespace.
|
|
22
|
+
# @return [String]
|
|
23
|
+
attr_reader :full_name
|
|
24
|
+
|
|
25
|
+
# The proper base constant name of the class or module,
|
|
26
|
+
# without its namespace.
|
|
27
|
+
# @return [String]
|
|
28
|
+
attr_reader :name
|
|
29
|
+
|
|
30
|
+
# @return [Hotstepper::Support::NamespaceWrapper]
|
|
31
|
+
attr_reader :namespace
|
|
32
|
+
|
|
33
|
+
# The namespaced_file path to the object, suitable for use with `File.join`,
|
|
34
|
+
# absent any of the Rails paths. For example, "Foo::Bar::Baz" would become `["foo", "bar", "baz.rb"]`.
|
|
35
|
+
# @return [<String>]
|
|
36
|
+
attr_reader :path
|
|
37
|
+
|
|
38
|
+
# @param [#to_s] input
|
|
39
|
+
# @param [Hotstepper::Support::NamespaceWrapper] namespace
|
|
40
|
+
def initialize(input, namespace:, extension: "rb")
|
|
41
|
+
@extension = extension
|
|
42
|
+
|
|
43
|
+
@namespace = namespace
|
|
44
|
+
|
|
45
|
+
# Ensure that our input is a properly camelized constant name.
|
|
46
|
+
# It uses ActiveSupport's inflector at runtime, so it should
|
|
47
|
+
# respect whatever custom inflections the user has defined.
|
|
48
|
+
@name = input.to_s.camelize(:upper).demodulize
|
|
49
|
+
|
|
50
|
+
@filename = [name.underscore, extension].compact_blank.join(?.).freeze
|
|
51
|
+
|
|
52
|
+
@full_name = [*namespace.parts, name].join("::")
|
|
53
|
+
|
|
54
|
+
@path = [*namespace.path, filename].freeze
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @return [Pathname]
|
|
58
|
+
def to_operation_path = operations_root.join(*path)
|
|
59
|
+
|
|
60
|
+
# @return [Pathname]
|
|
61
|
+
def to_service_path = services_root.join(*path)
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
# @return [Pathname]
|
|
66
|
+
def app_root = Pathname("app")
|
|
67
|
+
|
|
68
|
+
# @return [Pathname]
|
|
69
|
+
def operations_root = app_root.join("operations")
|
|
70
|
+
|
|
71
|
+
# @return [Pathname]
|
|
72
|
+
def services_root = app_root.join("services")
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hotstepper
|
|
4
|
+
module Support
|
|
5
|
+
module Generators
|
|
6
|
+
module Common
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
# @return [Hotstepper::Support::NamespaceWrapper]
|
|
12
|
+
attr_reader :namespace
|
|
13
|
+
|
|
14
|
+
# @return [Pathname]
|
|
15
|
+
def operations_root = Rails.root.join("app", "operations")
|
|
16
|
+
|
|
17
|
+
# @return [Pathname]
|
|
18
|
+
def services_root = Rails.root.join("app", "services")
|
|
19
|
+
|
|
20
|
+
# @param [#to_s] input
|
|
21
|
+
# @return [void]
|
|
22
|
+
def set_up_wrapped_namespace!(input)
|
|
23
|
+
@namespace = Hotstepper::Support::NamespaceWrapper.new(input)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @param [#to_s] input
|
|
27
|
+
# @return [Hotstepper::Support::FileWrapper]
|
|
28
|
+
def namespaced_file(input) = namespace.file(input)
|
|
29
|
+
|
|
30
|
+
# @return [void]
|
|
31
|
+
def nest_under_namespace(&)
|
|
32
|
+
content = capture(&)
|
|
33
|
+
|
|
34
|
+
concat(namespace.wrap(content))
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hotstepper
|
|
4
|
+
module Support
|
|
5
|
+
module Generators
|
|
6
|
+
module Namespaced
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
include Common
|
|
10
|
+
|
|
11
|
+
included do
|
|
12
|
+
argument :namespace_name, type: :string, required: true,
|
|
13
|
+
desc: "The namespace that the services and operations should live in"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @return [void]
|
|
17
|
+
def set_up_namespace!
|
|
18
|
+
set_up_wrapped_namespace!(namespace_name)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hotstepper
|
|
4
|
+
module Support
|
|
5
|
+
module Generators
|
|
6
|
+
class StepDefinition < Hotstepper::Struct
|
|
7
|
+
include ActiveModel::Validations
|
|
8
|
+
|
|
9
|
+
PARSE_PATTERN = /\A(?<name>.+?)(?<is_abstract>\?|:abstract)?\z/
|
|
10
|
+
|
|
11
|
+
attribute :name, Hotstepper::Support::Types::Coercible::Symbol
|
|
12
|
+
attribute :abstract, Hotstepper::Support::Types::Bool.default(false)
|
|
13
|
+
attribute :index, Hotstepper::Support::Types::Integer.default(0)
|
|
14
|
+
|
|
15
|
+
validates :name, presence: true, format: { with: Hotstepper::Patterns::STEP_NAME }
|
|
16
|
+
|
|
17
|
+
alias abstract? abstract
|
|
18
|
+
|
|
19
|
+
def name! = "#{name}!"
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
# @param [String] input
|
|
23
|
+
# @return [StepDefinition]
|
|
24
|
+
def parse(input, index: 0)
|
|
25
|
+
match = PARSE_PATTERN.match(input)
|
|
26
|
+
|
|
27
|
+
unless match
|
|
28
|
+
raise ArgumentError, "Invalid step definition: #{input.inspect}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
new(
|
|
32
|
+
index:,
|
|
33
|
+
name: match[:name],
|
|
34
|
+
abstract: match[:is_abstract].present?
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @param [<String>] inputs
|
|
39
|
+
# @return [<StepDefinition>]
|
|
40
|
+
def parse_many(inputs) = inputs.map.with_index { |input, index| parse(input, index:) }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hotstepper
|
|
4
|
+
module Support
|
|
5
|
+
class NamespaceWrapper
|
|
6
|
+
include Dry::Core::Equalizer.new(:namespace)
|
|
7
|
+
|
|
8
|
+
TAB = " "
|
|
9
|
+
|
|
10
|
+
# @return [String]
|
|
11
|
+
attr_reader :namespace
|
|
12
|
+
|
|
13
|
+
# @return [<String>]
|
|
14
|
+
attr_reader :parts
|
|
15
|
+
|
|
16
|
+
# @return [<String>]
|
|
17
|
+
attr_reader :path
|
|
18
|
+
|
|
19
|
+
# @return [Integer]
|
|
20
|
+
attr_reader :size
|
|
21
|
+
|
|
22
|
+
# @param [#to_s] input
|
|
23
|
+
def initialize(input)
|
|
24
|
+
# Ensure that our input is a properly camelized constant name.
|
|
25
|
+
# It uses ActiveSupport's inflector at runtime, so it should
|
|
26
|
+
# respect whatever custom inflections the user has defined.
|
|
27
|
+
@namespace = input.to_s.camelize(:upper)
|
|
28
|
+
|
|
29
|
+
@parts = namespace.split("::")
|
|
30
|
+
|
|
31
|
+
@path = parts.map(&:underscore)
|
|
32
|
+
|
|
33
|
+
@size = parts.size
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def empty? = size.zero?
|
|
37
|
+
|
|
38
|
+
# @param [#to_s] input
|
|
39
|
+
# @return [Hotstepper::Support::FileWrapper]
|
|
40
|
+
def file(input)
|
|
41
|
+
Hotstepper::Support::FileWrapper.new(input, namespace: self)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @param [String] content
|
|
45
|
+
# @return [String]
|
|
46
|
+
def wrap(content)
|
|
47
|
+
# :nocov:
|
|
48
|
+
return content if empty?
|
|
49
|
+
# :nocov:
|
|
50
|
+
|
|
51
|
+
sio = StringIO.new
|
|
52
|
+
|
|
53
|
+
parts.each_with_index do |part, index|
|
|
54
|
+
indentation = TAB * index
|
|
55
|
+
|
|
56
|
+
sio.puts "#{indentation}module #{part}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
sio.puts content.strip.indent(size * 2)
|
|
60
|
+
|
|
61
|
+
last_index = size - 1
|
|
62
|
+
|
|
63
|
+
last_index.downto(0).each do |index|
|
|
64
|
+
indentation = TAB * index
|
|
65
|
+
|
|
66
|
+
sio.puts "#{indentation}end"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
sio.string
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hotstepper
|
|
4
|
+
module Support
|
|
5
|
+
# @api private
|
|
6
|
+
# @see Hotstepper::Operation
|
|
7
|
+
class NullService
|
|
8
|
+
include Dry::Monads[:result]
|
|
9
|
+
|
|
10
|
+
# @return [Dry::Monads::Result]
|
|
11
|
+
def call(*args, **kwargs) = Success()
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../patterns"
|
|
4
|
+
|
|
5
|
+
module Hotstepper
|
|
6
|
+
module Support
|
|
7
|
+
# DRY.rb types used within this gem.
|
|
8
|
+
#
|
|
9
|
+
# @api private
|
|
10
|
+
module Types
|
|
11
|
+
extend Hotstepper::Typespace
|
|
12
|
+
|
|
13
|
+
DatabaseConnectionClass = ClassOrSubclass(ActiveRecord::Base)
|
|
14
|
+
|
|
15
|
+
ServiceClass = Class
|
|
16
|
+
|
|
17
|
+
SimpleSortDirection = Coercible::String.default("asc").enum("asc", "desc").fallback("asc")
|
|
18
|
+
|
|
19
|
+
StepName = Coercible::Symbol.constrained(format: Hotstepper::Patterns::STEP_NAME)
|
|
20
|
+
|
|
21
|
+
UUID = String.constrained(any_uuid: true)
|
|
22
|
+
|
|
23
|
+
UUIDList = Types::Array.of(Types::UUID)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hotstepper
|
|
4
|
+
module Testing
|
|
5
|
+
class DoFor
|
|
6
|
+
extend Hotstepper::Monadic::DoFor
|
|
7
|
+
|
|
8
|
+
include Dry::Monads[:result]
|
|
9
|
+
|
|
10
|
+
def do_method
|
|
11
|
+
value = yield try_something!
|
|
12
|
+
|
|
13
|
+
Success value
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
do_for! :do_method
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def try_something! = Success("tried")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hotstepper
|
|
4
|
+
module Testing
|
|
5
|
+
module Operations
|
|
6
|
+
module ExampleHelpers
|
|
7
|
+
extend RSpec::Matchers::DSL
|
|
8
|
+
|
|
9
|
+
# @return [Dry::Monads::Result, Object]
|
|
10
|
+
attr_reader :last_result
|
|
11
|
+
|
|
12
|
+
# @return [Object]
|
|
13
|
+
attr_reader :last_success
|
|
14
|
+
|
|
15
|
+
# @return [Object]
|
|
16
|
+
attr_reader :last_failure
|
|
17
|
+
|
|
18
|
+
def expect_calling
|
|
19
|
+
@last_result = operation.call
|
|
20
|
+
|
|
21
|
+
expect(@last_result)
|
|
22
|
+
ensure
|
|
23
|
+
extract_last_result!
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def expect_calling_with(...)
|
|
27
|
+
@last_result = operation.call(...)
|
|
28
|
+
|
|
29
|
+
expect(@last_result)
|
|
30
|
+
ensure
|
|
31
|
+
extract_last_result!
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @api private
|
|
35
|
+
# @return [void]
|
|
36
|
+
def extract_last_result!
|
|
37
|
+
case @last_result
|
|
38
|
+
in Dry::Monads::Success
|
|
39
|
+
@last_success = @last_result.value!
|
|
40
|
+
@last_failure = nil
|
|
41
|
+
in Dry::Monads::Failure
|
|
42
|
+
@last_success = nil
|
|
43
|
+
@last_failure = @last_result.failure
|
|
44
|
+
else
|
|
45
|
+
@last_success = @last_failure = nil
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
matcher :be_the_result_with do |*args, **kwargs|
|
|
50
|
+
match do |actual|
|
|
51
|
+
@expected = operation.call(*args, **kwargs)
|
|
52
|
+
|
|
53
|
+
if @expected.success?
|
|
54
|
+
values_match? @expected.value!, actual
|
|
55
|
+
else
|
|
56
|
+
false
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
description do
|
|
61
|
+
"be the result of calling #{operation.class.name}(#{inspect_args})"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
define_method :inspect_args do
|
|
65
|
+
[
|
|
66
|
+
*inspect_value(args),
|
|
67
|
+
inspect_value(kwargs)
|
|
68
|
+
].join(", ")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def inspect_value(value)
|
|
72
|
+
case value
|
|
73
|
+
when Array then value.map { |v| inspect_value v }
|
|
74
|
+
when Hash then value.transform_values { |v| inspect_value v }
|
|
75
|
+
when Hotstepper::Support::Types::Any.constrained(model: true)
|
|
76
|
+
"Model(#{value.model_name}##{value.to_param.inspect})"
|
|
77
|
+
when Hotstepper::Support::Types::Any.constrained(model_class: true)
|
|
78
|
+
"ModelClass(#{value.model_name})"
|
|
79
|
+
when String, Symbol then value.inspect
|
|
80
|
+
else
|
|
81
|
+
value
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hotstepper
|
|
4
|
+
module Testing
|
|
5
|
+
module Typing
|
|
6
|
+
module Interface
|
|
7
|
+
extend Hotstepper::Typing
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class Foo
|
|
11
|
+
include Hotstepper::Typing
|
|
12
|
+
include Interface
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class Bar < Foo; end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hotstepper
|
|
4
|
+
# Testing utilities for hotstepper.
|
|
5
|
+
module Testing
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
begin
|
|
10
|
+
require "rspec/core"
|
|
11
|
+
require "rspec/matchers"
|
|
12
|
+
rescue LoadError
|
|
13
|
+
# :nocov:
|
|
14
|
+
warn "RSpec is required for hotstepper testing utilities. Please add it to your Gemfile."
|
|
15
|
+
|
|
16
|
+
return
|
|
17
|
+
# :nocov:
|
|
18
|
+
else
|
|
19
|
+
require_relative "../testing/matchers/00_disable_with"
|
|
20
|
+
require_relative "../testing/matchers/dry_validation"
|
|
21
|
+
require_relative "../testing/matchers/monadic"
|
|
22
|
+
require_relative "../testing/matchers/negated"
|
|
23
|
+
require_relative "../testing/contexts/operations"
|
|
24
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hotstepper
|
|
4
|
+
# Enhance a module to act as a `Dry.Types` namespace with some extra
|
|
5
|
+
# helper methods that build on what `dry-types` provides out of the box.
|
|
6
|
+
#
|
|
7
|
+
# @note Unlike `Dry.Types`, this should be `extended` rather than `included`.
|
|
8
|
+
module Typespace
|
|
9
|
+
include Dry.Types
|
|
10
|
+
|
|
11
|
+
# @return [Module] A memoized version of the `Dry.Types` module, which
|
|
12
|
+
# is stored to avoid needing to compile a new set of types with every
|
|
13
|
+
# new typespace.
|
|
14
|
+
CORE_TYPES = Dry.Types
|
|
15
|
+
|
|
16
|
+
private_constant :CORE_TYPES
|
|
17
|
+
|
|
18
|
+
# @api private
|
|
19
|
+
module CommonTypes
|
|
20
|
+
# @return [Dry::Types::Type(String)]
|
|
21
|
+
Email = CORE_TYPES::String.constrained(email: true)
|
|
22
|
+
|
|
23
|
+
# An interface type that matches anything that responds to `#call`.
|
|
24
|
+
# @return [Dry::Types::Type(#call)]
|
|
25
|
+
Operation = CORE_TYPES::Any.constrained(respond_to: :call)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
include CommonTypes
|
|
29
|
+
|
|
30
|
+
# @return [Dry::Types::Type(Pathname)]
|
|
31
|
+
Pathname = Instance(Pathname).constructor do |input|
|
|
32
|
+
case input
|
|
33
|
+
when ::String then Pathname(input)
|
|
34
|
+
when ::Pathname then input
|
|
35
|
+
else
|
|
36
|
+
input
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class << self
|
|
41
|
+
# @return [void]
|
|
42
|
+
def included(_mod)
|
|
43
|
+
raise TypeError, "use #extend to apply Hotstepper::Types to a module"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @return [void]
|
|
47
|
+
# @!parse [ruby]
|
|
48
|
+
# # An interface type that matches anything that responds to `#call`.
|
|
49
|
+
# Operation = Dry::Types["any"].constrained(respond_to: :call)
|
|
50
|
+
def extended(mod)
|
|
51
|
+
mod.include CORE_TYPES
|
|
52
|
+
mod.include CommonTypes
|
|
53
|
+
mod.include ::Hotstepper::Patterns
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Create a type that matches an exact class or one of its subclasses.
|
|
58
|
+
#
|
|
59
|
+
# @param [Class] kls
|
|
60
|
+
# @return [Dry::Types::Sum::Constrained]
|
|
61
|
+
def ClassOrSubclass(kls)
|
|
62
|
+
unless kls.is_a?(::Module)
|
|
63
|
+
raise ::ArgumentError, "Expected a class or module, got #{kls.inspect}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
is_klass = Class.constrained(eql: kls)
|
|
67
|
+
|
|
68
|
+
is_klass | Implements(kls)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Create a type that matches a class that includes a given module
|
|
72
|
+
# @param [Class, Module] mod
|
|
73
|
+
# @return [Dry::Types::Type]
|
|
74
|
+
def Implements(mod) = Class.constrained(lt: mod)
|
|
75
|
+
|
|
76
|
+
alias Inherits Implements
|
|
77
|
+
|
|
78
|
+
# A type that can match a class, one of its subclasses, or an instance
|
|
79
|
+
# of any of the former.
|
|
80
|
+
#
|
|
81
|
+
# @param [Class, Module] type
|
|
82
|
+
# @return [Dry::Types::Sum::Constrained]
|
|
83
|
+
def InstanceOrClass(type)
|
|
84
|
+
unless type.is_a?(::Module)
|
|
85
|
+
raise ::ArgumentError, "Expected a class or module, got #{type.inspect}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
instance = Instance(type)
|
|
89
|
+
|
|
90
|
+
klass = ClassOrSubclass(type)
|
|
91
|
+
|
|
92
|
+
instance | klass
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# This type builder is helpful for avoiding circular dependencies as opposed
|
|
96
|
+
# to using `Instance` directly, since it doesn't require the class to be defined at the time it's called.
|
|
97
|
+
#
|
|
98
|
+
# @param [String] instance_named
|
|
99
|
+
# @return [Dry::Types::Constrained]
|
|
100
|
+
def InstanceOf(instance_named) = Any.constrained(instance_named:)
|
|
101
|
+
|
|
102
|
+
# @param [String] model_named
|
|
103
|
+
# @return [Dry::Types::Constrained]
|
|
104
|
+
def ModelInstance(model_named) = Instance(ActiveRecord::Base).constrained(model_named:)
|
|
105
|
+
|
|
106
|
+
extend self
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hotstepper
|
|
4
|
+
# Automatically generate `::Type`, `::List`, and `::Subclass` constants for any class (or module) that includes this.
|
|
5
|
+
module Typing
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
hotstepper_typings!
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
module ClassMethods
|
|
13
|
+
# @param [Class] subclass
|
|
14
|
+
# @return [void]
|
|
15
|
+
def inherited(subclass)
|
|
16
|
+
super
|
|
17
|
+
|
|
18
|
+
subclass.hotstepper_typings!
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Generate a configurable "map" type with a certain type of key.
|
|
22
|
+
#
|
|
23
|
+
# @param [Dry::Types::Type] key
|
|
24
|
+
# @param [Symbol] on
|
|
25
|
+
def map_type!(key: Hotstepper::Support::Types::Coercible::String, on: :Map)
|
|
26
|
+
const_set on, Hotstepper::Support::Types::Hash.map(key, const_get(:Type))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
protected
|
|
30
|
+
|
|
31
|
+
# @return [void]
|
|
32
|
+
def hotstepper_typings!
|
|
33
|
+
type = Hotstepper::Support::Types.Instance(self)
|
|
34
|
+
|
|
35
|
+
const_set :Type, type
|
|
36
|
+
|
|
37
|
+
list = Hotstepper::Support::Types::Array.of(type)
|
|
38
|
+
|
|
39
|
+
const_set :List, list
|
|
40
|
+
|
|
41
|
+
subclass = Hotstepper::Support::Types.Implements(self)
|
|
42
|
+
|
|
43
|
+
const_set :Subclass, subclass
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class << self
|
|
48
|
+
# Allow our typing module to be applied to modules as well,
|
|
49
|
+
# so we can more succinctly type interfaces.
|
|
50
|
+
#
|
|
51
|
+
# @param [Class, Module] base
|
|
52
|
+
# @return [void]
|
|
53
|
+
def extend_object(mod)
|
|
54
|
+
# :nocov:
|
|
55
|
+
raise TypeError, "use #include for classes" if mod.kind_of?(Class)
|
|
56
|
+
|
|
57
|
+
# Already extended
|
|
58
|
+
return false if mod.singleton_class < self
|
|
59
|
+
|
|
60
|
+
# :nocov:
|
|
61
|
+
|
|
62
|
+
super
|
|
63
|
+
|
|
64
|
+
mod.extend const_get(:ClassMethods)
|
|
65
|
+
mod.class_eval(&@_included_block)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|