hati-rails-api 0.1.0.beta1
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/LICENSE +21 -0
- data/README.md +237 -0
- data/hati-rails-api.gemspec +39 -0
- data/lib/generators/hati_rails_api/context_generator.rb +270 -0
- data/lib/hati_rails_api/context/configuration/configuration.rb +50 -0
- data/lib/hati_rails_api/context/configuration/domain_configuration.rb +55 -0
- data/lib/hati_rails_api/context/core/error_handler.rb +76 -0
- data/lib/hati_rails_api/context/core/loader.rb +54 -0
- data/lib/hati_rails_api/context/core/migration.rb +68 -0
- data/lib/hati_rails_api/context/core/public_api.rb +114 -0
- data/lib/hati_rails_api/context/core.rb +18 -0
- data/lib/hati_rails_api/context/extensions/string_extensions.rb +11 -0
- data/lib/hati_rails_api/context/generators/base_generator.rb +33 -0
- data/lib/hati_rails_api/context/generators/domain_generator.rb +219 -0
- data/lib/hati_rails_api/context/generators/file_generation.rb +72 -0
- data/lib/hati_rails_api/context/generators/generator.rb +212 -0
- data/lib/hati_rails_api/context/generators/layer_component_generator.rb +88 -0
- data/lib/hati_rails_api/context/generators/model_endpoint_generator.rb +97 -0
- data/lib/hati_rails_api/context/generators/operation_generator.rb +182 -0
- data/lib/hati_rails_api/context/layers/operation_layer.rb +45 -0
- data/lib/hati_rails_api/context/layers/standard_layer.rb +44 -0
- data/lib/hati_rails_api/context/managers/rollback_manager.rb +123 -0
- data/lib/hati_rails_api/context/shared/content_generators.rb +125 -0
- data/lib/hati_rails_api/context/shared/layer_factory.rb +37 -0
- data/lib/hati_rails_api/context.rb +30 -0
- data/lib/hati_rails_api/errors/unsupported_operation_error.rb +11 -0
- data/lib/hati_rails_api/macro/serializer_macro.rb +13 -0
- data/lib/hati_rails_api/response_handler.rb +71 -0
- data/lib/hati_rails_api/version.rb +5 -0
- data/lib/hati_rails_api.rb +15 -0
- metadata +121 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_generator'
|
|
4
|
+
require_relative 'domain_generator'
|
|
5
|
+
require_relative '../managers/rollback_manager'
|
|
6
|
+
require_relative '../shared/content_generators'
|
|
7
|
+
|
|
8
|
+
module HatiRailsApi
|
|
9
|
+
module Context
|
|
10
|
+
# Main orchestrator for the generation process
|
|
11
|
+
# Coordinates all generation activities with clean separation of concerns
|
|
12
|
+
class Generator < BaseGenerator
|
|
13
|
+
include ContentGenerators
|
|
14
|
+
|
|
15
|
+
# Default configurations for models and endpoints
|
|
16
|
+
DEFAULT_MODEL = { path: 'app/models', base: 'ApplicationRecord' }.freeze
|
|
17
|
+
DEFAULT_ENDPOINT = { path: 'app/controllers/api', base: 'ApplicationController' }.freeze
|
|
18
|
+
|
|
19
|
+
attr_reader :domains, :models, :endpoints
|
|
20
|
+
|
|
21
|
+
def initialize(config = nil, force: false)
|
|
22
|
+
super
|
|
23
|
+
@domains = {}
|
|
24
|
+
@models = []
|
|
25
|
+
@endpoints = []
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Register global models for generation
|
|
29
|
+
#
|
|
30
|
+
# @param names_or_options [String, Array, Hash] Model names or options
|
|
31
|
+
# @param options [Hash] Additional options
|
|
32
|
+
def model(names_or_options = nil, **options)
|
|
33
|
+
return unless names_or_options
|
|
34
|
+
|
|
35
|
+
names, opts = extract_names_and_options(names_or_options, options)
|
|
36
|
+
names.each { |name| @models << { name: name, options: opts } }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Register global endpoints for generation
|
|
40
|
+
#
|
|
41
|
+
# @param names_or_options [String, Array, Hash] Endpoint names or options
|
|
42
|
+
# @param options [Hash] Additional options
|
|
43
|
+
def endpoint(names_or_options = nil, **options)
|
|
44
|
+
return unless names_or_options
|
|
45
|
+
|
|
46
|
+
names, opts = extract_names_and_options(names_or_options, options)
|
|
47
|
+
names.each { |name| @endpoints << { name: name, options: opts } }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Define a domain with its configuration
|
|
51
|
+
#
|
|
52
|
+
# @param name [Symbol, String] Domain name
|
|
53
|
+
# @param options [Hash] Domain-specific options
|
|
54
|
+
# @param block [Proc] Domain configuration block
|
|
55
|
+
def domain(name, **options, &block)
|
|
56
|
+
domain_generator = create_domain_generator(name, options)
|
|
57
|
+
domain_generator.instance_eval(&block) if block_given?
|
|
58
|
+
@domains[name] = domain_generator
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Execute the complete generation process
|
|
62
|
+
#
|
|
63
|
+
# @return [Boolean] True if generation was successful
|
|
64
|
+
def execute
|
|
65
|
+
return false if nothing_to_generate?
|
|
66
|
+
|
|
67
|
+
generate_all_components
|
|
68
|
+
track_generation_results
|
|
69
|
+
true
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
# Extract names and options from flexible parameter format
|
|
75
|
+
def extract_names_and_options(names_or_options, options)
|
|
76
|
+
if names_or_options.is_a?(Hash)
|
|
77
|
+
[[], names_or_options]
|
|
78
|
+
else
|
|
79
|
+
names = Array(names_or_options)
|
|
80
|
+
[names, options]
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Create a properly configured domain generator
|
|
85
|
+
def create_domain_generator(name, options)
|
|
86
|
+
DomainGenerator.new(
|
|
87
|
+
name: name,
|
|
88
|
+
config: @config,
|
|
89
|
+
options: options,
|
|
90
|
+
generated_files: @generated_files,
|
|
91
|
+
force: @force,
|
|
92
|
+
override_all: method(:override_all_state)
|
|
93
|
+
).tap do |generator|
|
|
94
|
+
generator.context_reference = self
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Check if there's anything to generate
|
|
99
|
+
def nothing_to_generate?
|
|
100
|
+
@models.empty? && @endpoints.empty? && @domains.empty?
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Generate all components in the correct order
|
|
104
|
+
def generate_all_components
|
|
105
|
+
generate_global_models
|
|
106
|
+
generate_global_endpoints
|
|
107
|
+
generate_domains
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Generate all registered global models
|
|
111
|
+
def generate_global_models
|
|
112
|
+
@models.each { |model_config| generate_single_model(model_config) }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Generate all registered global endpoints
|
|
116
|
+
def generate_global_endpoints
|
|
117
|
+
@endpoints.each { |endpoint_config| generate_single_endpoint(endpoint_config) }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Generate all defined domains
|
|
121
|
+
def generate_domains
|
|
122
|
+
@domains.each_value(&:generate)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Generate a single model with error handling
|
|
126
|
+
def generate_single_model(model_config)
|
|
127
|
+
file_path = model_file_path(model_config)
|
|
128
|
+
content = model_content(model_config)
|
|
129
|
+
|
|
130
|
+
generate_file?(file_path, content)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Generate a single endpoint with error handling
|
|
134
|
+
def generate_single_endpoint(endpoint_config)
|
|
135
|
+
file_path = endpoint_file_path(endpoint_config)
|
|
136
|
+
content = endpoint_content(endpoint_config)
|
|
137
|
+
|
|
138
|
+
generate_file?(file_path, content)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Build model file path
|
|
142
|
+
def model_file_path(model_config)
|
|
143
|
+
path = resolve_model_path(model_config[:options])
|
|
144
|
+
File.join(path, "#{model_config[:name]}.rb")
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Build endpoint file path
|
|
148
|
+
def endpoint_file_path(endpoint_config)
|
|
149
|
+
path = resolve_endpoint_path(endpoint_config[:options])
|
|
150
|
+
File.join(path, "#{endpoint_config[:name]}_controller.rb")
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Generate model content
|
|
154
|
+
def model_content(model_config)
|
|
155
|
+
generate_model_content(
|
|
156
|
+
name: model_config[:name],
|
|
157
|
+
base_class: resolve_model_base_class,
|
|
158
|
+
timestamp: @timestamp
|
|
159
|
+
)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Generate endpoint content
|
|
163
|
+
def endpoint_content(endpoint_config)
|
|
164
|
+
generate_controller_content(
|
|
165
|
+
name: endpoint_config[:name],
|
|
166
|
+
base_class: resolve_endpoint_base_class,
|
|
167
|
+
timestamp: @timestamp
|
|
168
|
+
)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Resolve model path with fallbacks
|
|
172
|
+
def resolve_model_path(options)
|
|
173
|
+
options[:path] ||
|
|
174
|
+
@config&.model_config&.dig(:path) ||
|
|
175
|
+
DEFAULT_MODEL[:path]
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Resolve model base class with fallbacks
|
|
179
|
+
def resolve_model_base_class
|
|
180
|
+
@config&.model_config&.dig(:base) || DEFAULT_MODEL[:base]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Resolve endpoint path with fallbacks
|
|
184
|
+
def resolve_endpoint_path(options)
|
|
185
|
+
options[:path] ||
|
|
186
|
+
@config&.endpoint_config&.dig(:path) ||
|
|
187
|
+
DEFAULT_ENDPOINT[:path]
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Resolve endpoint base class with fallbacks
|
|
191
|
+
def resolve_endpoint_base_class
|
|
192
|
+
@config&.endpoint_config&.dig(:base) || DEFAULT_ENDPOINT[:base]
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Track generation results for rollback functionality
|
|
196
|
+
def track_generation_results
|
|
197
|
+
return if @generated_files.empty?
|
|
198
|
+
|
|
199
|
+
RollbackManager.new.track_generation(@timestamp, @generated_files)
|
|
200
|
+
log_generation_summary
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Log a summary of the generation results
|
|
204
|
+
def log_generation_summary
|
|
205
|
+
puts "\nGeneration completed successfully!"
|
|
206
|
+
puts " Timestamp: #{@timestamp}"
|
|
207
|
+
puts " Files generated: #{@generated_files.length}"
|
|
208
|
+
puts " To rollback: rails generate hati_rails_api:context rollback --timestamp=#{@timestamp}"
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HatiRailsApi
|
|
4
|
+
module Context
|
|
5
|
+
# Concern for layer and component generation
|
|
6
|
+
# Follows Single Responsibility Principle - handles only layer/component generation
|
|
7
|
+
module LayerComponentGenerator
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def generate_layer(base_path:, layer_name:, layer_config:)
|
|
11
|
+
layer_path = File.join(base_path, layer_name.to_s)
|
|
12
|
+
FileUtils.mkdir_p(layer_path)
|
|
13
|
+
|
|
14
|
+
layer_config.components.each do |component|
|
|
15
|
+
generate_component(
|
|
16
|
+
layer_path: layer_path,
|
|
17
|
+
component: component,
|
|
18
|
+
layer_config: layer_config
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def generate_component(layer_path:, component:, layer_config:)
|
|
24
|
+
component_name = resolve_component_name(component: component, layer_config: layer_config)
|
|
25
|
+
file_path = File.join(layer_path, "#{component_name}.rb")
|
|
26
|
+
content = generate_component_content(component: component, layer_config: layer_config)
|
|
27
|
+
|
|
28
|
+
generate_file?(file_path, content)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def resolve_component_name(component:, layer_config:)
|
|
32
|
+
if component.is_a?(Hash)
|
|
33
|
+
name = component.keys.first.to_s
|
|
34
|
+
suffix = component.values.first[:suffix]
|
|
35
|
+
else
|
|
36
|
+
name = component == :domain ? @name.to_s : component.to_s
|
|
37
|
+
suffix = layer_config.default_suffix
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
suffix && suffix != false ? "#{name}_#{layer_config.layer_name}" : name
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def generate_component_content(component:, layer_config:)
|
|
44
|
+
class_name = generate_class_name(component: component, layer_config: layer_config)
|
|
45
|
+
|
|
46
|
+
if layer_config.is_a?(OperationLayer)
|
|
47
|
+
generate_component_operation_content(
|
|
48
|
+
component: component,
|
|
49
|
+
class_name: class_name,
|
|
50
|
+
layer_config: layer_config
|
|
51
|
+
)
|
|
52
|
+
else
|
|
53
|
+
generate_standard_content(
|
|
54
|
+
component: component,
|
|
55
|
+
class_name: class_name,
|
|
56
|
+
layer_config: layer_config
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def generate_class_name(component:, layer_config:)
|
|
62
|
+
if component.is_a?(Hash)
|
|
63
|
+
base_name = component.keys.first.to_s.camelize
|
|
64
|
+
suffix = component.values.first[:suffix]
|
|
65
|
+
else
|
|
66
|
+
name = component == :domain ? @name : component
|
|
67
|
+
base_name = name.to_s.camelize
|
|
68
|
+
suffix = layer_config.default_suffix
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
suffix && suffix != false ? "#{base_name}#{layer_config.layer_name.to_s.camelize}" : base_name
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def generate_standard_content(component:, class_name:, layer_config:)
|
|
75
|
+
base_requirement = layer_config.base_class ? "require_relative '#{layer_config.base_class}'\n\n" : ''
|
|
76
|
+
base_class_inheritance = (" < #{layer_config.base_class.split('/').last.camelize}" if layer_config.base_class)
|
|
77
|
+
|
|
78
|
+
<<~RUBY
|
|
79
|
+
# frozen_string_literal: true
|
|
80
|
+
# Generated at #{Time.now.strftime('%Y%m%d%H%M%S')}
|
|
81
|
+
#{base_requirement}class #{class_name}#{base_class_inheritance}
|
|
82
|
+
# TODO: Implement #{class_name} logic
|
|
83
|
+
end
|
|
84
|
+
RUBY
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../shared/content_generators'
|
|
4
|
+
|
|
5
|
+
module HatiRailsApi
|
|
6
|
+
module Context
|
|
7
|
+
# Concern for model and endpoint generation
|
|
8
|
+
# Follows Single Responsibility Principle - handles only model/endpoint generation
|
|
9
|
+
module ModelEndpointGenerator
|
|
10
|
+
include ContentGenerators
|
|
11
|
+
|
|
12
|
+
DEFAULT_MODEL = {
|
|
13
|
+
path: 'app/models',
|
|
14
|
+
base: 'ApplicationRecord'
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
DEFAULT_ENDPOINT = {
|
|
18
|
+
path: 'app/controllers/api',
|
|
19
|
+
base: 'ApplicationController'
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
def generate_domain_model
|
|
23
|
+
options = @domain_model[:options] || {}
|
|
24
|
+
|
|
25
|
+
path = @config&.model_config&.dig(:path)
|
|
26
|
+
base_path = options[:path] || path || DEFAULT_MODEL[:path]
|
|
27
|
+
|
|
28
|
+
base = @config&.model_config&.dig(:base)
|
|
29
|
+
base_class = base || DEFAULT_MODEL[:base]
|
|
30
|
+
|
|
31
|
+
file_path = File.join(base_path, "#{@name}.rb")
|
|
32
|
+
content = generate_model_content(name: @name, base_class: base_class)
|
|
33
|
+
|
|
34
|
+
generate_file?(file_path, content)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def generate_domain_endpoint
|
|
38
|
+
options = @domain_endpoint[:options] || {}
|
|
39
|
+
|
|
40
|
+
path = @config&.endpoint_config&.dig(:path)
|
|
41
|
+
base_path = options[:path] || path || DEFAULT_ENDPOINT[:path]
|
|
42
|
+
|
|
43
|
+
base = @config&.endpoint_config&.dig(:base)
|
|
44
|
+
base_class = base || DEFAULT_ENDPOINT[:base]
|
|
45
|
+
|
|
46
|
+
file_path = File.join(base_path, "#{@name}_controller.rb")
|
|
47
|
+
content = generate_endpoint_content(base_class)
|
|
48
|
+
|
|
49
|
+
generate_file?(file_path, content)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def generate_endpoint_content(base_class)
|
|
53
|
+
# Get operation components to generate actions
|
|
54
|
+
operation_components = get_operation_components
|
|
55
|
+
actions_code = generate_controller_actions(operation_components)
|
|
56
|
+
|
|
57
|
+
generate_controller_content(
|
|
58
|
+
name: @name,
|
|
59
|
+
base_class: base_class,
|
|
60
|
+
actions_code: actions_code
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def get_operation_components
|
|
65
|
+
# If explicit components are provided in endpoint declaration, use those
|
|
66
|
+
return @domain_endpoint[:explicit_components] if @domain_endpoint&.dig(:explicit_components)
|
|
67
|
+
|
|
68
|
+
# Otherwise, use operation layer components
|
|
69
|
+
operation_layer = @layers[:operation]
|
|
70
|
+
return [] unless operation_layer
|
|
71
|
+
|
|
72
|
+
operation_layer.components.reject { |comp| comp == :domain }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def generate_controller_actions(components)
|
|
76
|
+
return ' # TODO: Add controller actions here' if components.empty?
|
|
77
|
+
|
|
78
|
+
actions = components.map do |component|
|
|
79
|
+
action_name = component.to_s
|
|
80
|
+
operation_class = "#{action_name.camelize}Operation"
|
|
81
|
+
|
|
82
|
+
generate_custom_action(action_name, operation_class)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
actions.join("\n\n")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def generate_custom_action(action_name, operation_class)
|
|
89
|
+
<<~RUBY.chomp
|
|
90
|
+
def #{action_name}
|
|
91
|
+
run_and_render #{operation_class}
|
|
92
|
+
end
|
|
93
|
+
RUBY
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../shared/content_generators'
|
|
4
|
+
|
|
5
|
+
module HatiRailsApi
|
|
6
|
+
module Context
|
|
7
|
+
# Concern for operation generation
|
|
8
|
+
# Follows Single Responsibility Principle - handles only operation generation
|
|
9
|
+
module OperationGenerator
|
|
10
|
+
include ContentGenerators
|
|
11
|
+
|
|
12
|
+
TIMESTAMP_FORMAT = '%Y%m%d%H%M%S'
|
|
13
|
+
|
|
14
|
+
def generate_component_operation_content(component:, class_name:, layer_config:)
|
|
15
|
+
base_class = resolve_base_class(layer_config)
|
|
16
|
+
action = extract_action_from_component(component)
|
|
17
|
+
timestamp = Time.now.strftime(TIMESTAMP_FORMAT)
|
|
18
|
+
|
|
19
|
+
# Check if we should generate step-based operations
|
|
20
|
+
if should_use_steps?(layer_config)
|
|
21
|
+
generate_step_based_operation_content(
|
|
22
|
+
class_name: class_name,
|
|
23
|
+
base_class: base_class,
|
|
24
|
+
action: action,
|
|
25
|
+
layer_config: layer_config,
|
|
26
|
+
timestamp: timestamp
|
|
27
|
+
)
|
|
28
|
+
else
|
|
29
|
+
generate_simple_operation_content(
|
|
30
|
+
class_name: class_name,
|
|
31
|
+
base_class: base_class,
|
|
32
|
+
action: action,
|
|
33
|
+
timestamp: timestamp
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def generate_step_based_operation(class_name:, base_class:, action:, steps:)
|
|
39
|
+
step_declarations = generate_step_declarations(action, steps)
|
|
40
|
+
step_calls = generate_step_calls(steps)
|
|
41
|
+
|
|
42
|
+
generate_step_based_operation_content(
|
|
43
|
+
class_name: class_name,
|
|
44
|
+
base_class: base_class,
|
|
45
|
+
step_declarations: step_declarations,
|
|
46
|
+
step_calls: step_calls
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def generate_standard_operation(class_name:, base_class:, action:, base_requirement:)
|
|
51
|
+
steps_content = generate_default_steps_for_action(action)
|
|
52
|
+
timestamp = Time.now.strftime(TIMESTAMP_FORMAT)
|
|
53
|
+
|
|
54
|
+
if base_requirement.blank?
|
|
55
|
+
generate_operation_content(
|
|
56
|
+
class_name: class_name,
|
|
57
|
+
base_class: base_class,
|
|
58
|
+
action: action,
|
|
59
|
+
steps_content: steps_content
|
|
60
|
+
)
|
|
61
|
+
return
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
<<~RUBY
|
|
65
|
+
# frozen_string_literal: true
|
|
66
|
+
# Generated at #{timestamp}
|
|
67
|
+
|
|
68
|
+
#{base_requirement}class #{class_name} < #{base_class}
|
|
69
|
+
# Operation configuration
|
|
70
|
+
#{generate_operation_macros(action)}
|
|
71
|
+
|
|
72
|
+
def call(params:)
|
|
73
|
+
#{steps_content}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
#{generate_private_steps(action)}
|
|
79
|
+
end
|
|
80
|
+
RUBY
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def should_use_steps?(layer_config)
|
|
86
|
+
# If operation layer has explicit steps, use those
|
|
87
|
+
return true if layer_config.respond_to?(:steps) && layer_config.steps.any?
|
|
88
|
+
|
|
89
|
+
# If step options include granular: true, use steps from other layers
|
|
90
|
+
return true if layer_config.respond_to?(:granular_steps?) && layer_config.granular_steps?
|
|
91
|
+
|
|
92
|
+
# If domain has multiple layers besides operation, use steps
|
|
93
|
+
other_layers = @layers.keys.reject { |name| name == :operation }
|
|
94
|
+
other_layers.any?
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def generate_step_based_operation_content(class_name:, base_class:, action:, layer_config:, timestamp:)
|
|
98
|
+
steps = determine_steps_for_operation(layer_config)
|
|
99
|
+
step_declarations = generate_step_declarations(action, steps)
|
|
100
|
+
|
|
101
|
+
<<~RUBY
|
|
102
|
+
# frozen_string_literal: true
|
|
103
|
+
# Generated at #{timestamp}
|
|
104
|
+
|
|
105
|
+
class #{class_name} < #{base_class}
|
|
106
|
+
#{step_declarations}
|
|
107
|
+
|
|
108
|
+
def call(params:)
|
|
109
|
+
Success(params)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
RUBY
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def generate_simple_operation_content(class_name:, base_class:, action:, timestamp:)
|
|
116
|
+
<<~RUBY
|
|
117
|
+
# frozen_string_literal: true
|
|
118
|
+
# Generated at #{timestamp}
|
|
119
|
+
|
|
120
|
+
class #{class_name} < #{base_class}
|
|
121
|
+
def call(params:)
|
|
122
|
+
# TODO: Implement #{action} business logic
|
|
123
|
+
# Add your implementation here
|
|
124
|
+
|
|
125
|
+
Success(params)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
# TODO: Add your private methods here
|
|
131
|
+
# Example:
|
|
132
|
+
# def validate_params(params)
|
|
133
|
+
# # validation logic
|
|
134
|
+
# end
|
|
135
|
+
end
|
|
136
|
+
RUBY
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def determine_steps_for_operation(layer_config)
|
|
140
|
+
# If operation has explicit steps configured, use those
|
|
141
|
+
return layer_config.steps if layer_config.respond_to?(:steps) && layer_config.steps.any?
|
|
142
|
+
|
|
143
|
+
# If granular steps enabled, use other domain layers as steps
|
|
144
|
+
if layer_config.respond_to?(:granular_steps?) && layer_config.granular_steps?
|
|
145
|
+
return @layers.keys.reject { |name| name == :operation }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Otherwise, use domain layers (excluding operation itself)
|
|
149
|
+
@layers.keys.reject { |name| name == :operation }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def generate_step_declarations(action, steps)
|
|
153
|
+
steps_arr = steps.map do |step_name|
|
|
154
|
+
service_class = "#{action.camelize}#{step_name.to_s.camelize}"
|
|
155
|
+
" step #{step_name}: \"#{service_class}\""
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
steps_arr.join("\n")
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def generate_step_calls(steps)
|
|
162
|
+
step_calls = steps.map do |step_name|
|
|
163
|
+
" rez = #{step_name}.call(rez)"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
([' rez = params'] + step_calls).join("\n")
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def resolve_base_class(layer_config)
|
|
170
|
+
if layer_config.base_class
|
|
171
|
+
layer_config.base_class.split('/').last.camelize
|
|
172
|
+
else
|
|
173
|
+
'HatiOperation::Base'
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def extract_action_from_component(component)
|
|
178
|
+
component.is_a?(Hash) ? component.keys.first.to_s : component.to_s
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'standard_layer'
|
|
4
|
+
|
|
5
|
+
module HatiRailsApi
|
|
6
|
+
module Context
|
|
7
|
+
# Operation layer configuration class
|
|
8
|
+
# Extends StandardLayer for operation-specific functionality
|
|
9
|
+
class OperationLayer < StandardLayer
|
|
10
|
+
attr_reader :step_options
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
super(:operation)
|
|
14
|
+
@base_class = 'hati_operation/base'
|
|
15
|
+
@step_options = {}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def step(*step_names, **options)
|
|
19
|
+
if step_names.empty? && options.any?
|
|
20
|
+
# Handle: operation.step true, granular: true
|
|
21
|
+
@step_options.merge!(options)
|
|
22
|
+
elsif step_names.any?
|
|
23
|
+
# Handle: operation.step :validate, :encrypt, :store
|
|
24
|
+
@steps = step_names.flatten
|
|
25
|
+
@step_options.merge!(options) if options.any?
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def steps
|
|
30
|
+
@steps || []
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def granular_steps?
|
|
34
|
+
@step_options[:granular] == true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def dup
|
|
38
|
+
new_layer = super
|
|
39
|
+
new_layer.instance_variable_set(:@steps, @steps&.dup)
|
|
40
|
+
new_layer.instance_variable_set(:@step_options, @step_options.dup)
|
|
41
|
+
new_layer
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HatiRailsApi
|
|
4
|
+
module Context
|
|
5
|
+
# Standard layer configuration class
|
|
6
|
+
# Follows Single Responsibility Principle - handles standard layer configuration
|
|
7
|
+
class StandardLayer
|
|
8
|
+
attr_reader :layer_name, :base_class, :components, :default_suffix
|
|
9
|
+
|
|
10
|
+
def initialize(layer_name)
|
|
11
|
+
@layer_name = layer_name
|
|
12
|
+
@base_class = nil
|
|
13
|
+
@components = []
|
|
14
|
+
@default_suffix = true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def base(base_class)
|
|
18
|
+
@base_class = base_class
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def component(*names, **options)
|
|
22
|
+
if names.empty?
|
|
23
|
+
@components = [:domain]
|
|
24
|
+
else
|
|
25
|
+
names.each do |name|
|
|
26
|
+
if name.is_a?(Array)
|
|
27
|
+
@components.concat(name)
|
|
28
|
+
else
|
|
29
|
+
@components << (options.empty? ? name : { name => options })
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def dup
|
|
36
|
+
new_layer = self.class.new(@layer_name)
|
|
37
|
+
new_layer.instance_variable_set(:@base_class, @base_class)
|
|
38
|
+
new_layer.instance_variable_set(:@components, @components.dup)
|
|
39
|
+
new_layer.instance_variable_set(:@default_suffix, @default_suffix)
|
|
40
|
+
new_layer
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|