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
@@ -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,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hotstepper
4
+ module Support
5
+ module Generators
6
+ module ServiceOperations
7
+ extend ActiveSupport::Concern
8
+
9
+ include Common
10
+ include Namespaced
11
+ end
12
+ end
13
+ end
14
+ 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hotstepper
4
+ VERSION = "0.1.0"
5
+ end