light-services 2.2.1 → 3.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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/.github/config/rubocop_linter_action.yml +4 -4
  3. data/.github/workflows/ci.yml +12 -12
  4. data/.gitignore +1 -0
  5. data/.rubocop.yml +83 -7
  6. data/CHANGELOG.md +38 -0
  7. data/CLAUDE.md +139 -0
  8. data/Gemfile +16 -11
  9. data/Gemfile.lock +53 -27
  10. data/README.md +84 -21
  11. data/docs/arguments.md +290 -0
  12. data/docs/best-practices.md +153 -0
  13. data/docs/callbacks.md +476 -0
  14. data/docs/concepts.md +80 -0
  15. data/docs/configuration.md +204 -0
  16. data/docs/context.md +128 -0
  17. data/docs/crud.md +525 -0
  18. data/docs/errors.md +280 -0
  19. data/docs/generators.md +250 -0
  20. data/docs/outputs.md +158 -0
  21. data/docs/pundit-authorization.md +320 -0
  22. data/docs/quickstart.md +134 -0
  23. data/docs/readme.md +101 -0
  24. data/docs/recipes.md +14 -0
  25. data/docs/rubocop.md +285 -0
  26. data/docs/ruby-lsp.md +133 -0
  27. data/docs/service-rendering.md +222 -0
  28. data/docs/steps.md +391 -0
  29. data/docs/summary.md +21 -0
  30. data/docs/testing.md +549 -0
  31. data/lib/generators/light_services/install/USAGE +15 -0
  32. data/lib/generators/light_services/install/install_generator.rb +41 -0
  33. data/lib/generators/light_services/install/templates/application_service.rb.tt +8 -0
  34. data/lib/generators/light_services/install/templates/application_service_spec.rb.tt +7 -0
  35. data/lib/generators/light_services/install/templates/initializer.rb.tt +30 -0
  36. data/lib/generators/light_services/service/USAGE +21 -0
  37. data/lib/generators/light_services/service/service_generator.rb +68 -0
  38. data/lib/generators/light_services/service/templates/service.rb.tt +48 -0
  39. data/lib/generators/light_services/service/templates/service_spec.rb.tt +40 -0
  40. data/lib/light/services/base.rb +134 -122
  41. data/lib/light/services/base_with_context.rb +23 -1
  42. data/lib/light/services/callbacks.rb +157 -0
  43. data/lib/light/services/collection.rb +145 -0
  44. data/lib/light/services/concerns/execution.rb +79 -0
  45. data/lib/light/services/concerns/parent_service.rb +34 -0
  46. data/lib/light/services/concerns/state_management.rb +30 -0
  47. data/lib/light/services/config.rb +82 -16
  48. data/lib/light/services/constants.rb +100 -0
  49. data/lib/light/services/dsl/arguments_dsl.rb +85 -0
  50. data/lib/light/services/dsl/outputs_dsl.rb +81 -0
  51. data/lib/light/services/dsl/steps_dsl.rb +205 -0
  52. data/lib/light/services/dsl/validation.rb +162 -0
  53. data/lib/light/services/exceptions.rb +25 -2
  54. data/lib/light/services/message.rb +28 -3
  55. data/lib/light/services/messages.rb +92 -32
  56. data/lib/light/services/rspec/matchers/define_argument.rb +174 -0
  57. data/lib/light/services/rspec/matchers/define_output.rb +147 -0
  58. data/lib/light/services/rspec/matchers/define_step.rb +225 -0
  59. data/lib/light/services/rspec/matchers/execute_step.rb +230 -0
  60. data/lib/light/services/rspec/matchers/have_error_on.rb +148 -0
  61. data/lib/light/services/rspec/matchers/have_warning_on.rb +148 -0
  62. data/lib/light/services/rspec/matchers/trigger_callback.rb +138 -0
  63. data/lib/light/services/rspec.rb +15 -0
  64. data/lib/light/services/rubocop/cop/light_services/argument_type_required.rb +52 -0
  65. data/lib/light/services/rubocop/cop/light_services/condition_method_exists.rb +173 -0
  66. data/lib/light/services/rubocop/cop/light_services/deprecated_methods.rb +113 -0
  67. data/lib/light/services/rubocop/cop/light_services/dsl_order.rb +176 -0
  68. data/lib/light/services/rubocop/cop/light_services/missing_private_keyword.rb +102 -0
  69. data/lib/light/services/rubocop/cop/light_services/no_direct_instantiation.rb +66 -0
  70. data/lib/light/services/rubocop/cop/light_services/output_type_required.rb +52 -0
  71. data/lib/light/services/rubocop/cop/light_services/step_method_exists.rb +109 -0
  72. data/lib/light/services/rubocop.rb +12 -0
  73. data/lib/light/services/settings/field.rb +114 -0
  74. data/lib/light/services/settings/step.rb +53 -20
  75. data/lib/light/services/utils.rb +38 -0
  76. data/lib/light/services/version.rb +1 -1
  77. data/lib/light/services.rb +2 -0
  78. data/lib/ruby_lsp/light_services/addon.rb +36 -0
  79. data/lib/ruby_lsp/light_services/definition.rb +132 -0
  80. data/lib/ruby_lsp/light_services/indexing_enhancement.rb +263 -0
  81. data/light-services.gemspec +6 -8
  82. metadata +68 -26
  83. data/lib/light/services/class_based_collection/base.rb +0 -86
  84. data/lib/light/services/class_based_collection/mount.rb +0 -33
  85. data/lib/light/services/collection/arguments.rb +0 -34
  86. data/lib/light/services/collection/base.rb +0 -59
  87. data/lib/light/services/collection/outputs.rb +0 -16
  88. data/lib/light/services/settings/argument.rb +0 -68
  89. data/lib/light/services/settings/output.rb +0 -34
@@ -0,0 +1,263 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLsp
4
+ module LightServices
5
+ class IndexingEnhancement < RubyIndexer::Enhancement # rubocop:disable Metrics/ClassLength
6
+ # DSL methods that generate getter, predicate, and setter methods
7
+ FIELD_DSL_METHODS = [:arg, :output].freeze
8
+
9
+ # Default type mappings for common dry-types to their underlying Ruby types
10
+ # These can be extended via Light::Services.config.ruby_lsp_type_mappings
11
+ DEFAULT_TYPE_MAPPINGS = {
12
+ "Types::String" => "String",
13
+ "Types::Strict::String" => "String",
14
+ "Types::Coercible::String" => "String",
15
+ "Types::Integer" => "Integer",
16
+ "Types::Strict::Integer" => "Integer",
17
+ "Types::Coercible::Integer" => "Integer",
18
+ "Types::Float" => "Float",
19
+ "Types::Strict::Float" => "Float",
20
+ "Types::Coercible::Float" => "Float",
21
+ "Types::Decimal" => "BigDecimal",
22
+ "Types::Strict::Decimal" => "BigDecimal",
23
+ "Types::Coercible::Decimal" => "BigDecimal",
24
+ "Types::Bool" => "TrueClass | FalseClass",
25
+ "Types::Strict::Bool" => "TrueClass | FalseClass",
26
+ "Types::True" => "TrueClass",
27
+ "Types::Strict::True" => "TrueClass",
28
+ "Types::False" => "FalseClass",
29
+ "Types::Strict::False" => "FalseClass",
30
+ "Types::Array" => "Array",
31
+ "Types::Strict::Array" => "Array",
32
+ "Types::Hash" => "Hash",
33
+ "Types::Strict::Hash" => "Hash",
34
+ "Types::Symbol" => "Symbol",
35
+ "Types::Strict::Symbol" => "Symbol",
36
+ "Types::Coercible::Symbol" => "Symbol",
37
+ "Types::Date" => "Date",
38
+ "Types::Strict::Date" => "Date",
39
+ "Types::DateTime" => "DateTime",
40
+ "Types::Strict::DateTime" => "DateTime",
41
+ "Types::Time" => "Time",
42
+ "Types::Strict::Time" => "Time",
43
+ "Types::Nil" => "NilClass",
44
+ "Types::Strict::Nil" => "NilClass",
45
+ "Types::Any" => "Object",
46
+ }.freeze
47
+
48
+ # ─────────────────────────────────────────────────────────────────────────
49
+ # Public API - Called by Ruby LSP indexer
50
+ # ─────────────────────────────────────────────────────────────────────────
51
+
52
+ # Called when the indexer encounters a method call node
53
+ # Detects `arg` and `output` DSL calls and registers the generated methods
54
+ def on_call_node_enter(node)
55
+ return unless @listener.current_owner
56
+ return unless FIELD_DSL_METHODS.include?(node.name)
57
+
58
+ field_name = extract_field_name(node)
59
+ return unless field_name
60
+
61
+ ruby_type = extract_ruby_type(node)
62
+ register_field_methods(field_name, node.location, ruby_type)
63
+ end
64
+
65
+ def on_call_node_leave(node); end
66
+
67
+ private
68
+
69
+ # ─────────────────────────────────────────────────────────────────────────
70
+ # Field Extraction
71
+ # ─────────────────────────────────────────────────────────────────────────
72
+
73
+ # Extract the field name from the first argument (symbol)
74
+ # Example: `arg :user` → "user"
75
+ def extract_field_name(node)
76
+ arguments = node.arguments&.arguments
77
+ return unless arguments&.any?
78
+
79
+ first_arg = arguments.first
80
+ return unless first_arg.is_a?(Prism::SymbolNode)
81
+
82
+ first_arg.value
83
+ end
84
+
85
+ # ─────────────────────────────────────────────────────────────────────────
86
+ # Type Resolution
87
+ # ─────────────────────────────────────────────────────────────────────────
88
+
89
+ # Extract and resolve the Ruby type from the `type:` keyword argument
90
+ # Returns the mapped Ruby type string or the original type if no mapping exists
91
+ def extract_ruby_type(node)
92
+ type_node = find_type_value_node(node)
93
+ return unless type_node
94
+
95
+ resolve_to_ruby_type(type_node)
96
+ end
97
+
98
+ # Find the value node for the `type:` keyword argument
99
+ def find_type_value_node(node)
100
+ arguments = node.arguments&.arguments
101
+ return unless arguments
102
+
103
+ keyword_hash = arguments.find { |arg| arg.is_a?(Prism::KeywordHashNode) }
104
+ return unless keyword_hash
105
+
106
+ # NOTE: Prism's SymbolNode#value returns a String, not a Symbol
107
+ type_element = keyword_hash.elements.find do |element|
108
+ element.is_a?(Prism::AssocNode) &&
109
+ element.key.is_a?(Prism::SymbolNode) &&
110
+ element.key.value == "type"
111
+ end
112
+
113
+ type_element&.value
114
+ end
115
+
116
+ # Resolve a Prism type node to a Ruby type string
117
+ # Handles constants, constant paths, and method chains
118
+ def resolve_to_ruby_type(node)
119
+ type_string = node_to_constant_string(node)
120
+ return unless type_string
121
+
122
+ map_to_ruby_type(type_string) || type_string
123
+ end
124
+
125
+ # Convert a Prism node to its constant string representation
126
+ def node_to_constant_string(node)
127
+ case node
128
+ when Prism::ConstantReadNode
129
+ node.name.to_s
130
+ when Prism::ConstantPathNode
131
+ build_constant_path(node)
132
+ when Prism::CallNode
133
+ # Handle method chains like Types::String.constrained(...) or Types::Array.of(...)
134
+ extract_receiver_constant(node)
135
+ end
136
+ end
137
+
138
+ # Build a full constant path string from nested ConstantPathNodes
139
+ # Example: Types::Strict::String
140
+ def build_constant_path(node)
141
+ parts = []
142
+ current = node
143
+
144
+ while current.is_a?(Prism::ConstantPathNode)
145
+ parts.unshift(current.name.to_s)
146
+ current = current.parent
147
+ end
148
+
149
+ parts.unshift(current.name.to_s) if current.is_a?(Prism::ConstantReadNode)
150
+ parts.join("::")
151
+ end
152
+
153
+ # Extract the receiver constant from a method call chain
154
+ # Example: Types::String.constrained(format: /@/) → "Types::String"
155
+ def extract_receiver_constant(node)
156
+ receiver = node.receiver
157
+ return unless receiver
158
+
159
+ case receiver
160
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
161
+ node_to_constant_string(receiver)
162
+ when Prism::CallNode
163
+ extract_receiver_constant(receiver)
164
+ end
165
+ end
166
+
167
+ # ─────────────────────────────────────────────────────────────────────────
168
+ # Type Mapping
169
+ # ─────────────────────────────────────────────────────────────────────────
170
+
171
+ # Map a type string to its corresponding Ruby type
172
+ # Uses custom mappings from config (if available) merged with defaults
173
+ def map_to_ruby_type(type_string)
174
+ mappings = effective_type_mappings
175
+
176
+ # Direct mapping lookup (custom mappings take precedence)
177
+ return mappings[type_string] if mappings.key?(type_string)
178
+
179
+ # Handle parameterized types: Types::Array.of(...) → Types::Array
180
+ base_type = type_string.split(".").first
181
+ mappings[base_type]
182
+ end
183
+
184
+ # Returns the effective type mappings (defaults + custom from config)
185
+ # Custom mappings take precedence over defaults
186
+ def effective_type_mappings
187
+ return DEFAULT_TYPE_MAPPINGS unless defined?(Light::Services)
188
+ return DEFAULT_TYPE_MAPPINGS unless Light::Services.respond_to?(:config)
189
+
190
+ custom_mappings = Light::Services.config&.ruby_lsp_type_mappings
191
+ return DEFAULT_TYPE_MAPPINGS if custom_mappings.nil? || custom_mappings.empty?
192
+
193
+ DEFAULT_TYPE_MAPPINGS.merge(custom_mappings)
194
+ rescue NoMethodError
195
+ DEFAULT_TYPE_MAPPINGS
196
+ end
197
+
198
+ # ─────────────────────────────────────────────────────────────────────────
199
+ # Method Registration
200
+ # ─────────────────────────────────────────────────────────────────────────
201
+
202
+ # Register all three generated methods for a field (getter, predicate, setter)
203
+ def register_field_methods(field_name, location, ruby_type)
204
+ register_getter(field_name, location, ruby_type)
205
+ register_predicate(field_name, location)
206
+ register_setter(field_name, location, ruby_type)
207
+ end
208
+
209
+ def register_getter(field_name, location, ruby_type)
210
+ @listener.add_method(
211
+ field_name.to_s,
212
+ location,
213
+ no_params_signature,
214
+ comments: return_type_comment(ruby_type),
215
+ )
216
+ end
217
+
218
+ def register_predicate(field_name, location)
219
+ @listener.add_method(
220
+ "#{field_name}?",
221
+ location,
222
+ no_params_signature,
223
+ comments: "@return [Boolean]",
224
+ )
225
+ end
226
+
227
+ def register_setter(field_name, location, ruby_type)
228
+ @listener.add_method(
229
+ "#{field_name}=",
230
+ location,
231
+ value_param_signature,
232
+ comments: setter_comment(ruby_type),
233
+ )
234
+ end
235
+
236
+ # ─────────────────────────────────────────────────────────────────────────
237
+ # Signatures & Comments
238
+ # ─────────────────────────────────────────────────────────────────────────
239
+
240
+ def no_params_signature
241
+ [RubyIndexer::Entry::Signature.new([])]
242
+ end
243
+
244
+ def value_param_signature
245
+ [RubyIndexer::Entry::Signature.new([
246
+ RubyIndexer::Entry::RequiredParameter.new(name: :value),
247
+ ])]
248
+ end
249
+
250
+ def return_type_comment(ruby_type)
251
+ return nil unless ruby_type
252
+
253
+ "@return [#{ruby_type}]"
254
+ end
255
+
256
+ def setter_comment(ruby_type)
257
+ return "@param value the value to set" unless ruby_type
258
+
259
+ "@param value [#{ruby_type}] the value to set\n@return [#{ruby_type}]"
260
+ end
261
+ end
262
+ end
263
+ end
@@ -8,11 +8,11 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ["Andrew Kodkod"]
9
9
  spec.email = ["andrew@kodkod.me"]
10
10
 
11
- spec.summary = "Robust service architecture for Ruby frameworks"
12
- spec.description = "Robust service architecture for Ruby frameworks"
11
+ spec.summary = "Robust service architecture for Ruby/Rails applications"
12
+ spec.description = "Light Services is a simple yet powerful way to organize business logic in Ruby applications. Build services that are easy to test, maintain, and understand." # rubocop:disable Layout/LineLength
13
13
  spec.homepage = "https://light-services-docs.vercel.app/"
14
14
  spec.license = "MIT"
15
- spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
15
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0")
16
16
 
17
17
  spec.metadata["allowed_push_host"] = "https://rubygems.org"
18
18
 
@@ -26,10 +26,8 @@ Gem::Specification.new do |spec|
26
26
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
27
27
  end
28
28
 
29
- spec.bindir = "exe"
30
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
31
- spec.require_paths = ["lib"]
29
+ spec.bindir = "exe"
30
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
31
+ spec.require_paths = ["lib"]
32
32
  spec.metadata["rubygems_mfa_required"] = "true"
33
-
34
- spec.add_dependency "benchmark", ">= 0.5"
35
33
  end
metadata CHANGED
@@ -1,29 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: light-services
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.1
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kodkod
8
8
  bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies:
12
- - !ruby/object:Gem::Dependency
13
- name: benchmark
14
- requirement: !ruby/object:Gem::Requirement
15
- requirements:
16
- - - ">="
17
- - !ruby/object:Gem::Version
18
- version: '0.5'
19
- type: :runtime
20
- prerelease: false
21
- version_requirements: !ruby/object:Gem::Requirement
22
- requirements:
23
- - - ">="
24
- - !ruby/object:Gem::Version
25
- version: '0.5'
26
- description: Robust service architecture for Ruby frameworks
11
+ dependencies: []
12
+ description: Light Services is a simple yet powerful way to organize business logic
13
+ in Ruby applications. Build services that are easy to test, maintain, and understand.
27
14
  email:
28
15
  - andrew@kodkod.me
29
16
  executables: []
@@ -38,6 +25,7 @@ files:
38
25
  - ".rubocop.yml"
39
26
  - ".ruby-version"
40
27
  - CHANGELOG.md
28
+ - CLAUDE.md
41
29
  - CODE_OF_CONDUCT.md
42
30
  - Gemfile
43
31
  - Gemfile.lock
@@ -46,22 +34,76 @@ files:
46
34
  - Rakefile
47
35
  - bin/console
48
36
  - bin/setup
37
+ - docs/arguments.md
38
+ - docs/best-practices.md
39
+ - docs/callbacks.md
40
+ - docs/concepts.md
41
+ - docs/configuration.md
42
+ - docs/context.md
43
+ - docs/crud.md
44
+ - docs/errors.md
45
+ - docs/generators.md
46
+ - docs/outputs.md
47
+ - docs/pundit-authorization.md
48
+ - docs/quickstart.md
49
+ - docs/readme.md
50
+ - docs/recipes.md
51
+ - docs/rubocop.md
52
+ - docs/ruby-lsp.md
53
+ - docs/service-rendering.md
54
+ - docs/steps.md
55
+ - docs/summary.md
56
+ - docs/testing.md
57
+ - lib/generators/light_services/install/USAGE
58
+ - lib/generators/light_services/install/install_generator.rb
59
+ - lib/generators/light_services/install/templates/application_service.rb.tt
60
+ - lib/generators/light_services/install/templates/application_service_spec.rb.tt
61
+ - lib/generators/light_services/install/templates/initializer.rb.tt
62
+ - lib/generators/light_services/service/USAGE
63
+ - lib/generators/light_services/service/service_generator.rb
64
+ - lib/generators/light_services/service/templates/service.rb.tt
65
+ - lib/generators/light_services/service/templates/service_spec.rb.tt
49
66
  - lib/light/services.rb
50
67
  - lib/light/services/base.rb
51
68
  - lib/light/services/base_with_context.rb
52
- - lib/light/services/class_based_collection/base.rb
53
- - lib/light/services/class_based_collection/mount.rb
54
- - lib/light/services/collection/arguments.rb
55
- - lib/light/services/collection/base.rb
56
- - lib/light/services/collection/outputs.rb
69
+ - lib/light/services/callbacks.rb
70
+ - lib/light/services/collection.rb
71
+ - lib/light/services/concerns/execution.rb
72
+ - lib/light/services/concerns/parent_service.rb
73
+ - lib/light/services/concerns/state_management.rb
57
74
  - lib/light/services/config.rb
75
+ - lib/light/services/constants.rb
76
+ - lib/light/services/dsl/arguments_dsl.rb
77
+ - lib/light/services/dsl/outputs_dsl.rb
78
+ - lib/light/services/dsl/steps_dsl.rb
79
+ - lib/light/services/dsl/validation.rb
58
80
  - lib/light/services/exceptions.rb
59
81
  - lib/light/services/message.rb
60
82
  - lib/light/services/messages.rb
61
- - lib/light/services/settings/argument.rb
62
- - lib/light/services/settings/output.rb
83
+ - lib/light/services/rspec.rb
84
+ - lib/light/services/rspec/matchers/define_argument.rb
85
+ - lib/light/services/rspec/matchers/define_output.rb
86
+ - lib/light/services/rspec/matchers/define_step.rb
87
+ - lib/light/services/rspec/matchers/execute_step.rb
88
+ - lib/light/services/rspec/matchers/have_error_on.rb
89
+ - lib/light/services/rspec/matchers/have_warning_on.rb
90
+ - lib/light/services/rspec/matchers/trigger_callback.rb
91
+ - lib/light/services/rubocop.rb
92
+ - lib/light/services/rubocop/cop/light_services/argument_type_required.rb
93
+ - lib/light/services/rubocop/cop/light_services/condition_method_exists.rb
94
+ - lib/light/services/rubocop/cop/light_services/deprecated_methods.rb
95
+ - lib/light/services/rubocop/cop/light_services/dsl_order.rb
96
+ - lib/light/services/rubocop/cop/light_services/missing_private_keyword.rb
97
+ - lib/light/services/rubocop/cop/light_services/no_direct_instantiation.rb
98
+ - lib/light/services/rubocop/cop/light_services/output_type_required.rb
99
+ - lib/light/services/rubocop/cop/light_services/step_method_exists.rb
100
+ - lib/light/services/settings/field.rb
63
101
  - lib/light/services/settings/step.rb
102
+ - lib/light/services/utils.rb
64
103
  - lib/light/services/version.rb
104
+ - lib/ruby_lsp/light_services/addon.rb
105
+ - lib/ruby_lsp/light_services/definition.rb
106
+ - lib/ruby_lsp/light_services/indexing_enhancement.rb
65
107
  - light-services.gemspec
66
108
  homepage: https://light-services-docs.vercel.app/
67
109
  licenses:
@@ -79,7 +121,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
79
121
  requirements:
80
122
  - - ">="
81
123
  - !ruby/object:Gem::Version
82
- version: 2.7.0
124
+ version: 3.0.0
83
125
  required_rubygems_version: !ruby/object:Gem::Requirement
84
126
  requirements:
85
127
  - - ">="
@@ -88,5 +130,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
88
130
  requirements: []
89
131
  rubygems_version: 3.6.9
90
132
  specification_version: 4
91
- summary: Robust service architecture for Ruby frameworks
133
+ summary: Robust service architecture for Ruby/Rails applications
92
134
  test_files: []
@@ -1,86 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Create class based collections for storing arguments settings, steps settings and outputs settings
4
- #
5
- # General functionality:
6
- # 1. Collection automatically loads data from parent classes
7
- # 2. It's possible to redefine items if needed (e.g. arguments)
8
- # 3. We can add items into collection after or before another items
9
- #
10
- module Light
11
- module Services
12
- module ClassBasedCollection
13
- class Base
14
- # TODO: Add `prepend: true`
15
- def initialize(item_class, allow_redefine)
16
- @item_class = item_class
17
- @allow_redefine = allow_redefine
18
- @collection = {}
19
- end
20
-
21
- def add(klass, name, opts = {})
22
- @collection[klass] ||= all_from_superclass(klass)
23
-
24
- validate_name!(klass, name)
25
- validate_opts!(klass, name, opts)
26
-
27
- item = @item_class.new(name, klass, opts)
28
-
29
- if opts[:before] || opts[:after]
30
- insert_item(klass, name, opts, item)
31
- else
32
- @collection[klass][name] = item
33
- end
34
- end
35
-
36
- def find_index(klass, name)
37
- index = @collection[klass].keys.index(name)
38
-
39
- return index if index
40
-
41
- # TODO: Update `NoStepError` because it maybe not only step
42
- raise Light::Services::NoStepError, "Cannot find #{@item_class} `#{name}` in service #{klass}"
43
- end
44
-
45
- def remove(klass, name)
46
- @collection[klass] ||= all_from_superclass(klass)
47
- @collection[klass].delete(name)
48
- end
49
-
50
- def all(klass)
51
- @collection[klass] || all_from_superclass(klass)
52
- end
53
-
54
- private
55
-
56
- def all_from_superclass(klass)
57
- if klass.superclass <= Light::Services::Base
58
- all(klass.superclass).dup
59
- else
60
- {}
61
- end
62
- end
63
-
64
- def validate_name!(klass, name)
65
- if !@allow_redefine && all(klass).key?(name)
66
- raise Light::Services::Error, "#{@item_class} with name `#{name}` already exists in service #{klass}"
67
- end
68
- end
69
-
70
- def validate_opts!(klass, name, opts)
71
- if opts[:before] && opts[:after]
72
- raise Light::Services::Error, "You cannot specify `before` and `after` " \
73
- "for #{@item_class} `#{name}` in service #{klass} at the same time"
74
- end
75
- end
76
-
77
- def insert_item(klass, name, opts, item)
78
- index = find_index(klass, opts[:before] || opts[:after])
79
- index += 1 unless opts[:before]
80
-
81
- @collection[klass] = @collection[klass].to_a.insert(index, [name, item]).to_h
82
- end
83
- end
84
- end
85
- end
86
- end
@@ -1,33 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # This class allows to mount class based collections to service objects
4
- #
5
- # Usage:
6
- #
7
- # mount_class_based_collection :steps, klass: Settings::Step, shortcut: :step
8
- # mount_class_based_collection :outputs, klass: Settings::Output, shortcut: :output
9
- # mount_class_based_collection :arguments, klass: Settings::Argument, shortcut: :arg, allow_redefine: true
10
- #
11
- module Light
12
- module Services
13
- module ClassBasedCollection
14
- module Mount
15
- def mount_class_based_collection(collection_name, item_class:, shortcut:, allow_redefine: false)
16
- class_variable_set(:"@@#{collection_name}", ClassBasedCollection::Base.new(item_class, allow_redefine))
17
-
18
- define_singleton_method shortcut do |item_name, opts = {}|
19
- class_variable_get(:"@@#{collection_name}").add(self, item_name, opts)
20
- end
21
-
22
- define_singleton_method :"remove_#{shortcut}" do |item_name|
23
- class_variable_get(:"@@#{collection_name}").remove(self, item_name)
24
- end
25
-
26
- define_singleton_method collection_name do
27
- class_variable_get(:"@@#{collection_name}").all(self)
28
- end
29
- end
30
- end
31
- end
32
- end
33
- end
@@ -1,34 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Collection to store, merge and validate arguments
4
- module Light
5
- module Services
6
- module Collection
7
- class Arguments < Base
8
- def extend_with_context(args)
9
- settings_collection.each do |name, settings|
10
- next if !settings.context || args.key?(name) || !key?(name)
11
-
12
- args[settings.name] = get(name)
13
- end
14
-
15
- args
16
- end
17
-
18
- def validate!
19
- settings_collection.each do |name, settings|
20
- next if settings.optional && (!key?(name) || get(name).nil?)
21
-
22
- settings.validate_type!(get(name))
23
- end
24
- end
25
-
26
- private
27
-
28
- def settings_collection
29
- @instance.class.arguments
30
- end
31
- end
32
- end
33
- end
34
- end
@@ -1,59 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Collection to store arguments and outputs values
4
- module Light
5
- module Services
6
- module Collection
7
- class Base
8
- # Includes
9
- extend Forwardable
10
-
11
- # Settings
12
- def_delegators :@storage, :key?, :to_h
13
-
14
- def initialize(instance, storage = {})
15
- @instance = instance
16
- @storage = storage
17
-
18
- return if storage.is_a?(Hash)
19
-
20
- raise Light::Services::ArgTypeError, "#{instance.class} - arguments must be a Hash"
21
- end
22
-
23
- def set(key, value)
24
- @storage[key] = value
25
- end
26
-
27
- def get(key)
28
- @storage[key]
29
- end
30
-
31
- def [](key)
32
- get(key)
33
- end
34
-
35
- def []=(key, value)
36
- set(key, value)
37
- end
38
-
39
- def load_defaults
40
- settings_collection.each do |name, settings|
41
- next if !settings.default_exists || key?(name)
42
-
43
- if settings.default.is_a?(Proc)
44
- set(name, @instance.instance_exec(&settings.default))
45
- else
46
- set(name, deep_dup(settings.default))
47
- end
48
- end
49
- end
50
-
51
- private
52
-
53
- def deep_dup(object)
54
- Marshal.load(Marshal.dump(object))
55
- end
56
- end
57
- end
58
- end
59
- end
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Collection to store outputs values
4
- module Light
5
- module Services
6
- module Collection
7
- class Outputs < Base
8
- private
9
-
10
- def settings_collection
11
- @instance.class.outputs
12
- end
13
- end
14
- end
15
- end
16
- end