yes-core 1.2.0 → 1.3.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 +4 -4
- data/CHANGELOG.md +4 -0
- data/lib/yes/core/aggregate/dsl/class_name_convention.rb +8 -0
- data/lib/yes/core/aggregate/dsl/class_resolvers/command_group/base.rb +34 -0
- data/lib/yes/core/aggregate/dsl/class_resolvers/command_group/command.rb +43 -0
- data/lib/yes/core/aggregate/dsl/class_resolvers/command_group/guard_evaluator.rb +35 -0
- data/lib/yes/core/aggregate/dsl/command_group_data.rb +45 -0
- data/lib/yes/core/aggregate/dsl/command_group_definer.rb +100 -0
- data/lib/yes/core/aggregate/dsl/method_definers/command_group/base.rb +29 -0
- data/lib/yes/core/aggregate/dsl/method_definers/command_group/can_command_group.rb +41 -0
- data/lib/yes/core/aggregate/dsl/method_definers/command_group/command_group.rb +40 -0
- data/lib/yes/core/aggregate.rb +50 -0
- data/lib/yes/core/command_handling/command_group_executor.rb +236 -0
- data/lib/yes/core/command_handling/command_group_handler.rb +89 -0
- data/lib/yes/core/commands/command_group.rb +147 -0
- data/lib/yes/core/commands/command_group_response.rb +66 -0
- data/lib/yes/core/commands/group.rb +7 -12
- data/lib/yes/core/commands/group_payload_normalizer.rb +45 -0
- data/lib/yes/core/configuration.rb +22 -0
- data/lib/yes/core/test_support/aggregate/command_test_dsl.rb +77 -2
- data/lib/yes/core/test_support/aggregate/shared_examples.rb +45 -0
- data/lib/yes/core/utils/command_utils.rb +21 -0
- data/lib/yes/core/version.rb +1 -1
- metadata +14 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0a7213daeb56ae07a8921c83b912e83b7f9cd81f6d93d90a4394cbe07b7c9299
|
|
4
|
+
data.tar.gz: 58379a04830d2ba08c0ca4431d4e372ee8cc67f8445621d7143c61f1dc27ab9a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f5ce8a14fb78e7c9bc1b11264e16a17f0cbdc0ef3dcbbd7175114a1c772097b67cb1226f359429a6be83900f26d0e911dd18df838aa9c9ad5a76cab5d999b889
|
|
7
|
+
data.tar.gz: 01de285d8c90f81625bdd1ff721877b87d23edad1949b1eb860ebd4fc0274c459fbbeb3080e6ecb678fa1cd0174b010b4eb4f146ff00d6e4bea4be935685bb08
|
data/CHANGELOG.md
CHANGED
|
@@ -43,6 +43,14 @@ module Yes
|
|
|
43
43
|
"#{context}::#{aggregate}::Commands::#{name.to_s.camelize}::GuardEvaluator"
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
+
def command_group_class_name(name)
|
|
47
|
+
"#{context}::#{aggregate}::CommandGroups::#{name.to_s.camelize}::Command"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def command_group_guard_evaluator_class_name(name)
|
|
51
|
+
"#{context}::#{aggregate}::CommandGroups::#{name.to_s.camelize}::GuardEvaluator"
|
|
52
|
+
end
|
|
53
|
+
|
|
46
54
|
def state_updater_class_name(name)
|
|
47
55
|
"#{context}::#{aggregate}::Commands::#{name.to_s.camelize}::StateUpdater"
|
|
48
56
|
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
class Aggregate
|
|
6
|
+
module Dsl
|
|
7
|
+
module ClassResolvers
|
|
8
|
+
module CommandGroup
|
|
9
|
+
# Base class for command_group-related class resolvers.
|
|
10
|
+
#
|
|
11
|
+
# Mirrors {ClassResolvers::Command::Base} but binds to
|
|
12
|
+
# {CommandGroupData} instead of {CommandData}.
|
|
13
|
+
#
|
|
14
|
+
# @abstract Subclass and implement {ClassResolvers::Base#class_type},
|
|
15
|
+
# {ClassResolvers::Base#class_name}, and
|
|
16
|
+
# {ClassResolvers::Base#generate_class}.
|
|
17
|
+
class Base < ClassResolvers::Base
|
|
18
|
+
# @param command_group_data [Yes::Core::Aggregate::Dsl::CommandGroupData]
|
|
19
|
+
def initialize(command_group_data)
|
|
20
|
+
@command_group_data = command_group_data
|
|
21
|
+
|
|
22
|
+
super(command_group_data.context_name, command_group_data.aggregate_name)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
attr_reader :command_group_data
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
class Aggregate
|
|
6
|
+
module Dsl
|
|
7
|
+
module ClassResolvers
|
|
8
|
+
module CommandGroup
|
|
9
|
+
# Resolves or generates the Command class for an aggregate-DSL
|
|
10
|
+
# command group. The generated class is a {Yes::Core::Commands::CommandGroup}
|
|
11
|
+
# subclass carrying the group's identity (context, aggregate,
|
|
12
|
+
# group_name) and the ordered list of sub-command names.
|
|
13
|
+
class Command < Base
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def class_type
|
|
17
|
+
:command_group
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def class_name
|
|
21
|
+
command_group_data.name
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def generate_class
|
|
25
|
+
group_name = command_group_data.name
|
|
26
|
+
ctx = command_group_data.context_name
|
|
27
|
+
agg = command_group_data.aggregate_name
|
|
28
|
+
sub_commands = command_group_data.sub_command_names.dup
|
|
29
|
+
|
|
30
|
+
Class.new(Yes::Core::Commands::CommandGroup).tap do |klass|
|
|
31
|
+
klass.context = ctx
|
|
32
|
+
klass.aggregate = agg
|
|
33
|
+
klass.group_name = group_name
|
|
34
|
+
klass.sub_command_names = sub_commands
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
class Aggregate
|
|
6
|
+
module Dsl
|
|
7
|
+
module ClassResolvers
|
|
8
|
+
module CommandGroup
|
|
9
|
+
# Resolves or generates the GuardEvaluator class for a command_group.
|
|
10
|
+
#
|
|
11
|
+
# Unlike the per-command resolver, no `:no_change` guard is
|
|
12
|
+
# auto-injected — command groups are intended to be lighter on
|
|
13
|
+
# guard checks and rely on whatever set of guards the user
|
|
14
|
+
# declares explicitly in the DSL block.
|
|
15
|
+
class GuardEvaluator < Base
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def class_type
|
|
19
|
+
:command_group_guard_evaluator
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def class_name
|
|
23
|
+
command_group_data.name
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def generate_class
|
|
27
|
+
Class.new(Yes::Core::CommandHandling::GuardEvaluator)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
class Aggregate
|
|
6
|
+
module Dsl
|
|
7
|
+
# Data object that holds information about a command_group definition
|
|
8
|
+
# in an aggregate.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# CommandGroupData.new(:create_apprenticeship, MyAggregate, context: 'Companies', aggregate: 'Apprenticeship')
|
|
12
|
+
class CommandGroupData
|
|
13
|
+
attr_reader :name, :context_name, :aggregate_name, :aggregate_class
|
|
14
|
+
attr_accessor :sub_command_names, :guard_names
|
|
15
|
+
|
|
16
|
+
# @param name [Symbol] the name of the command group
|
|
17
|
+
# @param aggregate_class [Class] the aggregate class the group belongs to
|
|
18
|
+
# @param options [Hash] additional options
|
|
19
|
+
# @option options [String] :context the context name
|
|
20
|
+
# @option options [String] :aggregate the aggregate name
|
|
21
|
+
def initialize(name, aggregate_class, options = {})
|
|
22
|
+
@name = name
|
|
23
|
+
@aggregate_class = aggregate_class
|
|
24
|
+
@context_name = options[:context]
|
|
25
|
+
@aggregate_name = options[:aggregate]
|
|
26
|
+
@sub_command_names = []
|
|
27
|
+
@guard_names = []
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @param name [Symbol] sub-command name to append (preserves order)
|
|
31
|
+
# @return [void]
|
|
32
|
+
def add_sub_command(name)
|
|
33
|
+
@sub_command_names << name
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @param name [Symbol] guard name to record on this group
|
|
37
|
+
# @return [void]
|
|
38
|
+
def add_guard(name)
|
|
39
|
+
@guard_names << name
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
class Aggregate
|
|
6
|
+
module Dsl
|
|
7
|
+
# Factory class that creates and defines command_groups on aggregates.
|
|
8
|
+
#
|
|
9
|
+
# Mirrors {CommandDefiner}. The DSL evaluator inside accepts two
|
|
10
|
+
# methods: `command :sub_command_name` to push a sub-command symbol,
|
|
11
|
+
# and `guard(name, error_extra: …) { … }` to register a group-level
|
|
12
|
+
# guard on the generated GuardEvaluator class.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# group_data = CommandGroupData.new(:create_apprenticeship, MyAggregate,
|
|
16
|
+
# context: 'Companies', aggregate: 'Apprenticeship')
|
|
17
|
+
# CommandGroupDefiner.new(group_data).call do
|
|
18
|
+
# command :assign_company
|
|
19
|
+
# command :change_name
|
|
20
|
+
#
|
|
21
|
+
# guard(:company_assigned) { company_id.present? }
|
|
22
|
+
# end
|
|
23
|
+
class CommandGroupDefiner
|
|
24
|
+
# Raised when an unknown sub-command name is referenced.
|
|
25
|
+
class UnknownSubCommandError < Yes::Core::Error; end
|
|
26
|
+
|
|
27
|
+
attr_reader :command_group_data
|
|
28
|
+
private :command_group_data
|
|
29
|
+
|
|
30
|
+
# @param command_group_data [CommandGroupData]
|
|
31
|
+
def initialize(command_group_data)
|
|
32
|
+
@command_group_data = command_group_data
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Generates and registers all classes/methods for the command group.
|
|
36
|
+
#
|
|
37
|
+
# @yield Block for declaring sub-commands and guards
|
|
38
|
+
# @return [void]
|
|
39
|
+
def call(&block)
|
|
40
|
+
create_and_register_guard_evaluator
|
|
41
|
+
evaluate_dsl_block(&block) if block
|
|
42
|
+
create_and_register_command
|
|
43
|
+
define_aggregate_methods
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def create_and_register_guard_evaluator
|
|
49
|
+
@guard_evaluator_class = ClassResolvers::CommandGroup::GuardEvaluator.new(command_group_data).call
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def create_and_register_command
|
|
53
|
+
ClassResolvers::CommandGroup::Command.new(command_group_data).call
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def define_aggregate_methods
|
|
57
|
+
MethodDefiners::CommandGroup::CommandGroup.new(command_group_data).call
|
|
58
|
+
MethodDefiners::CommandGroup::CanCommandGroup.new(command_group_data).call
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def evaluate_dsl_block(&)
|
|
62
|
+
DslEvaluator.new(command_group_data, @guard_evaluator_class).instance_eval(&)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# DSL evaluator that backs the `command_group :name do … end` block.
|
|
66
|
+
class DslEvaluator
|
|
67
|
+
attr_reader :command_group_data, :guard_evaluator_class
|
|
68
|
+
|
|
69
|
+
def initialize(command_group_data, guard_evaluator_class)
|
|
70
|
+
@command_group_data = command_group_data
|
|
71
|
+
@guard_evaluator_class = guard_evaluator_class
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Declare a sub-command. Order is preserved as execution order.
|
|
75
|
+
#
|
|
76
|
+
# @param name [Symbol] the sub-command name (must match a command
|
|
77
|
+
# declared on the same aggregate)
|
|
78
|
+
# @return [void]
|
|
79
|
+
def command(name)
|
|
80
|
+
command_group_data.add_sub_command(name.to_sym)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Register a group-level guard. Semantics match the per-command
|
|
84
|
+
# `guard` DSL — `:no_change` is recognized as the magic name that
|
|
85
|
+
# raises {NoChangeTransition} on failure.
|
|
86
|
+
#
|
|
87
|
+
# @param name [Symbol] the guard name
|
|
88
|
+
# @param error_extra [Hash, Proc] extra error context
|
|
89
|
+
# @yield Block returning true if the guard passes
|
|
90
|
+
# @return [void]
|
|
91
|
+
def guard(name, error_extra: {}, &)
|
|
92
|
+
command_group_data.add_guard(name)
|
|
93
|
+
guard_evaluator_class.guard(name, error_extra:, &)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
class Aggregate
|
|
6
|
+
module Dsl
|
|
7
|
+
module MethodDefiners
|
|
8
|
+
module CommandGroup
|
|
9
|
+
# Base class for command_group method definers.
|
|
10
|
+
class Base
|
|
11
|
+
def initialize(command_group_data)
|
|
12
|
+
@name = command_group_data.name
|
|
13
|
+
@aggregate_class = command_group_data.aggregate_class
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call
|
|
17
|
+
raise NotImplementedError, "#{self.class} must implement #call"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
attr_reader :name, :aggregate_class
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
class Aggregate
|
|
6
|
+
module Dsl
|
|
7
|
+
module MethodDefiners
|
|
8
|
+
module CommandGroup
|
|
9
|
+
# Defines `aggregate.can_<group_name>?(payload = {})` and the
|
|
10
|
+
# `<group_name>_error` accessor on the aggregate class.
|
|
11
|
+
#
|
|
12
|
+
# Mirrors {MethodDefiners::Command::CanCommand} but resolves the
|
|
13
|
+
# group's GuardEvaluator class instead of a command's.
|
|
14
|
+
class CanCommandGroup < Base
|
|
15
|
+
def call
|
|
16
|
+
can_method = :"can_#{@name}?"
|
|
17
|
+
error_method = :"#{@name}_error"
|
|
18
|
+
|
|
19
|
+
aggregate_class.attr_accessor error_method
|
|
20
|
+
group_name = @name
|
|
21
|
+
|
|
22
|
+
aggregate_class.define_method(can_method) do |payload = {}|
|
|
23
|
+
cmd = command_utilities.build_group_command(group_name, payload)
|
|
24
|
+
guard_evaluator_class = command_utilities.fetch_guard_evaluator_class_for_group(group_name)
|
|
25
|
+
|
|
26
|
+
Yes::Core::CommandHandling::GuardRunner.new(self).call(
|
|
27
|
+
cmd, group_name, guard_evaluator_class, skip_guards: false
|
|
28
|
+
).present?
|
|
29
|
+
rescue Yes::Core::CommandHandling::GuardEvaluator::InvalidTransition,
|
|
30
|
+
Yes::Core::CommandHandling::GuardEvaluator::NoChangeTransition,
|
|
31
|
+
Yes::Core::Command::Invalid
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yes
|
|
4
|
+
module Core
|
|
5
|
+
class Aggregate
|
|
6
|
+
module Dsl
|
|
7
|
+
module MethodDefiners
|
|
8
|
+
module CommandGroup
|
|
9
|
+
# Defines `aggregate.<group_name>(payload = nil, **options)` on the
|
|
10
|
+
# aggregate class. Mirrors {MethodDefiners::Command::Command} but
|
|
11
|
+
# delegates to {Yes::Core::CommandHandling::CommandGroupHandler}.
|
|
12
|
+
class CommandGroup < Base
|
|
13
|
+
def call
|
|
14
|
+
group_name = @name
|
|
15
|
+
|
|
16
|
+
aggregate_class.define_method(group_name) do |payload = nil, **options|
|
|
17
|
+
payload = payload.clone if payload.is_a?(Hash)
|
|
18
|
+
|
|
19
|
+
guards = options.delete(:guards)
|
|
20
|
+
guards = true if guards.nil?
|
|
21
|
+
metadata = options.delete(:metadata)
|
|
22
|
+
|
|
23
|
+
if payload.nil? && !options.empty?
|
|
24
|
+
payload = options
|
|
25
|
+
elsif payload.nil?
|
|
26
|
+
payload = {}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
Yes::Core::CommandHandling::CommandGroupHandler.new(self).call(
|
|
30
|
+
group_name, payload, guards:, metadata:
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/yes/core/aggregate.rb
CHANGED
|
@@ -66,6 +66,7 @@ module Yes
|
|
|
66
66
|
if tp.self == subclass
|
|
67
67
|
subclass.setup_read_model_classes if subclass.read_model_enabled?
|
|
68
68
|
subclass.setup_authorizer_classes
|
|
69
|
+
subclass.validate_command_groups!
|
|
69
70
|
tp.disable
|
|
70
71
|
end
|
|
71
72
|
end.enable
|
|
@@ -337,6 +338,55 @@ module Yes
|
|
|
337
338
|
@commands ||= {}
|
|
338
339
|
end
|
|
339
340
|
|
|
341
|
+
# Defines a command_group on the aggregate.
|
|
342
|
+
#
|
|
343
|
+
# A command_group declares a compound action that runs multiple
|
|
344
|
+
# existing aggregate commands in declaration order, atomically, with
|
|
345
|
+
# the sub-commands' guards bypassed. The group itself has its own,
|
|
346
|
+
# leaner guard set declared inside the block via `guard :name`.
|
|
347
|
+
#
|
|
348
|
+
# @param name [Symbol] the group name (also the aggregate method name)
|
|
349
|
+
# @yield Block accepting `command :sub_name` and `guard :name`
|
|
350
|
+
# @return [void]
|
|
351
|
+
#
|
|
352
|
+
# @example
|
|
353
|
+
# command_group :create_apprenticeship do
|
|
354
|
+
# command :assign_company
|
|
355
|
+
# command :assign_user
|
|
356
|
+
# command :change_name
|
|
357
|
+
# command :publish
|
|
358
|
+
#
|
|
359
|
+
# guard(:company_assigned) { payload.company_id.present? }
|
|
360
|
+
# end
|
|
361
|
+
def command_group(name, &)
|
|
362
|
+
@command_groups ||= {}
|
|
363
|
+
group_data = Dsl::CommandGroupData.new(name, self, context:, aggregate:)
|
|
364
|
+
@command_groups[name] = group_data
|
|
365
|
+
Dsl::CommandGroupDefiner.new(group_data).call(&)
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# @return [Hash] The command groups defined on this aggregate
|
|
369
|
+
def command_groups
|
|
370
|
+
@command_groups ||= {}
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Validates that each command_group references commands actually
|
|
374
|
+
# defined on this aggregate. Called from the end-of-class
|
|
375
|
+
# {TracePoint} hook set up in {.inherited}.
|
|
376
|
+
#
|
|
377
|
+
# @raise [Dsl::CommandGroupDefiner::UnknownSubCommandError]
|
|
378
|
+
# @return [void]
|
|
379
|
+
def validate_command_groups!
|
|
380
|
+
command_groups.each do |group_name, data|
|
|
381
|
+
unknown = data.sub_command_names - commands.keys
|
|
382
|
+
next if unknown.empty?
|
|
383
|
+
|
|
384
|
+
raise Dsl::CommandGroupDefiner::UnknownSubCommandError,
|
|
385
|
+
"command_group :#{group_name} on #{name} references unknown commands: " \
|
|
386
|
+
"#{unknown.join(', ')}. Define them with `command :<name>` before using them."
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
340
390
|
private
|
|
341
391
|
|
|
342
392
|
#
|