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.
Files changed (32) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +237 -0
  4. data/hati-rails-api.gemspec +39 -0
  5. data/lib/generators/hati_rails_api/context_generator.rb +270 -0
  6. data/lib/hati_rails_api/context/configuration/configuration.rb +50 -0
  7. data/lib/hati_rails_api/context/configuration/domain_configuration.rb +55 -0
  8. data/lib/hati_rails_api/context/core/error_handler.rb +76 -0
  9. data/lib/hati_rails_api/context/core/loader.rb +54 -0
  10. data/lib/hati_rails_api/context/core/migration.rb +68 -0
  11. data/lib/hati_rails_api/context/core/public_api.rb +114 -0
  12. data/lib/hati_rails_api/context/core.rb +18 -0
  13. data/lib/hati_rails_api/context/extensions/string_extensions.rb +11 -0
  14. data/lib/hati_rails_api/context/generators/base_generator.rb +33 -0
  15. data/lib/hati_rails_api/context/generators/domain_generator.rb +219 -0
  16. data/lib/hati_rails_api/context/generators/file_generation.rb +72 -0
  17. data/lib/hati_rails_api/context/generators/generator.rb +212 -0
  18. data/lib/hati_rails_api/context/generators/layer_component_generator.rb +88 -0
  19. data/lib/hati_rails_api/context/generators/model_endpoint_generator.rb +97 -0
  20. data/lib/hati_rails_api/context/generators/operation_generator.rb +182 -0
  21. data/lib/hati_rails_api/context/layers/operation_layer.rb +45 -0
  22. data/lib/hati_rails_api/context/layers/standard_layer.rb +44 -0
  23. data/lib/hati_rails_api/context/managers/rollback_manager.rb +123 -0
  24. data/lib/hati_rails_api/context/shared/content_generators.rb +125 -0
  25. data/lib/hati_rails_api/context/shared/layer_factory.rb +37 -0
  26. data/lib/hati_rails_api/context.rb +30 -0
  27. data/lib/hati_rails_api/errors/unsupported_operation_error.rb +11 -0
  28. data/lib/hati_rails_api/macro/serializer_macro.rb +13 -0
  29. data/lib/hati_rails_api/response_handler.rb +71 -0
  30. data/lib/hati_rails_api/version.rb +5 -0
  31. data/lib/hati_rails_api.rb +15 -0
  32. metadata +121 -0
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HatiRailsApi
4
+ module Context
5
+ module Core
6
+ # Comprehensive error handling module for the context system
7
+ # Provides consistent error handling and user-friendly messages
8
+ module ErrorHandler
9
+ # Custom errors for the context system
10
+ class ContextError < StandardError; end
11
+ class ConfigurationError < ContextError; end
12
+ class GenerationError < ContextError; end
13
+ class RollbackError < ContextError; end
14
+
15
+ private
16
+
17
+ # Wraps operations with comprehensive error handling
18
+ #
19
+ # @param operation_name [String] Name of the operation for logging
20
+ # @yield The block to execute with error handling
21
+ # @return [Object] The result of the block
22
+ # @raise [ContextError] Various context-specific errors
23
+ def with_error_handling(operation_name = caller_locations(1, 1)[0].label)
24
+ yield
25
+ rescue ArgumentError => e
26
+ handle_argument_error(e, operation_name)
27
+ rescue StandardError => e
28
+ handle_standard_error(e, operation_name)
29
+ end
30
+
31
+ # Handle argument errors with user-friendly messages
32
+ def handle_argument_error(error, operation_name)
33
+ puts "Invalid arguments for #{operation_name}: #{error.message}"
34
+ puts 'Please check the documentation for correct usage'
35
+ raise ConfigurationError, error.message
36
+ end
37
+
38
+ # Handle standard errors with context information
39
+ def handle_standard_error(error, operation_name)
40
+ puts "Error during #{operation_name}: #{error.message}"
41
+ puts "Location: #{error.backtrace&.first}"
42
+
43
+ case operation_name
44
+ when /configure/
45
+ puts 'Check your configuration syntax and required parameters'
46
+ raise ConfigurationError, error.message
47
+ when /generate/
48
+ puts 'Verify your generation block and file permissions'
49
+ raise GenerationError, error.message
50
+ when /rollback/
51
+ puts 'Check if the timestamp exists and files are accessible'
52
+ raise RollbackError, error.message
53
+ else
54
+ raise ContextError, error.message
55
+ end
56
+ end
57
+
58
+ # Log successful operations
59
+ def log_success(operation, details = nil)
60
+ puts "#{operation} completed successfully"
61
+ puts "#{details}" if details
62
+ end
63
+
64
+ # Log warnings
65
+ def log_warning(message)
66
+ puts "Warning: #{message}"
67
+ end
68
+
69
+ # Log information
70
+ def log_info(message)
71
+ puts "#{message}"
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HatiRailsApi
4
+ module Context
5
+ module Core
6
+ # Handles loading of all context components in the correct dependency order
7
+ # Ensures proper initialization and prevents circular dependencies
8
+ module Loader
9
+ # Load all components in the correct order
10
+ def self.load_all_components
11
+ load_extensions
12
+ load_configurations
13
+ load_shared_components
14
+ load_layers
15
+ load_generators
16
+ load_managers
17
+ end
18
+
19
+ private_class_method def self.load_extensions
20
+ require_relative '../extensions/string_extensions'
21
+ end
22
+
23
+ private_class_method def self.load_configurations
24
+ require_relative '../configuration/configuration'
25
+ require_relative '../configuration/domain_configuration'
26
+ end
27
+
28
+ private_class_method def self.load_shared_components
29
+ require_relative '../shared/layer_factory'
30
+ require_relative '../shared/content_generators'
31
+ end
32
+
33
+ private_class_method def self.load_layers
34
+ require_relative '../layers/standard_layer'
35
+ require_relative '../layers/operation_layer'
36
+ end
37
+
38
+ private_class_method def self.load_generators
39
+ require_relative '../generators/base_generator'
40
+ require_relative '../generators/file_generation'
41
+ require_relative '../generators/model_endpoint_generator'
42
+ require_relative '../generators/layer_component_generator'
43
+ require_relative '../generators/operation_generator'
44
+ require_relative '../generators/domain_generator'
45
+ require_relative '../generators/generator'
46
+ end
47
+
48
+ private_class_method def self.load_managers
49
+ require_relative '../managers/rollback_manager'
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HatiRailsApi
4
+ module Context
5
+ # Base class for context migrations
6
+ # Provides Rails-like migration interface for context generation
7
+ class Migration
8
+ def initialize
9
+ @generator = nil
10
+ end
11
+
12
+ # Domain definition method
13
+ #
14
+ # @param name [Symbol] Domain name
15
+ # @param block [Proc] Domain configuration block
16
+ def domain(name, &block)
17
+ ensure_generator
18
+ @generator.domain(name, &block)
19
+ end
20
+
21
+ # Model generation method
22
+ #
23
+ # @param names [Array, Symbol] Model names to generate
24
+ def model(names)
25
+ ensure_generator
26
+ Array(names).each { |name| @generator.model(name) }
27
+ end
28
+
29
+ # Endpoint generation method
30
+ #
31
+ # @param names [Array, Symbol] Endpoint names to generate
32
+ def endpoint(names)
33
+ ensure_generator
34
+ Array(names).each { |name| @generator.endpoint(name) }
35
+ end
36
+
37
+ # Execute the migration - called automatically after change
38
+ def execute
39
+ return unless @generator
40
+
41
+ @generator.execute
42
+ end
43
+
44
+ # Method to be overridden by migration classes
45
+ def change
46
+ raise NotImplementedError, 'Migration classes must implement the change method'
47
+ end
48
+
49
+ # Run the migration (calls change then execute)
50
+ def run
51
+ change
52
+ execute
53
+ end
54
+
55
+ private
56
+
57
+ def ensure_generator
58
+ return if @generator
59
+
60
+ # Get the global configuration
61
+ config = HatiRailsApi::Context.instance_variable_get(:@global_config)
62
+
63
+ # Create a new generator instance with force option
64
+ @generator = HatiRailsApi::Context::Generator.new(config, force: true)
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'error_handler'
4
+
5
+ module HatiRailsApi
6
+ module Context
7
+ module Core
8
+ # Public API module providing all external-facing methods
9
+ # Includes comprehensive error handling and validation
10
+ module PublicAPI
11
+ include ErrorHandler
12
+
13
+ # Global configuration storage
14
+ attr_accessor :global_config
15
+
16
+ # Configure the context system with global settings
17
+ #
18
+ # @param block [Proc] Configuration block
19
+ # @return [Configuration] The configuration object
20
+ # @raise [ArgumentError] If no block is provided
21
+ #
22
+ # @example
23
+ # Context.configure do |config|
24
+ # config.base_path 'app/contexts'
25
+ # config.model path: 'app/models', base: 'ApplicationRecord'
26
+ # end
27
+ def configure(&block)
28
+ raise ArgumentError, 'Configuration block is required' unless block_given?
29
+
30
+ with_error_handling do
31
+ @global_config = Configuration.new
32
+ @global_config.instance_eval(&block)
33
+ @global_config
34
+ end
35
+ end
36
+
37
+ # Generate files based on configuration
38
+ #
39
+ # @param force [Boolean] Force overwrite existing files
40
+ # @param block [Proc] Generation block
41
+ # @return [Boolean] True if generation was successful
42
+ # @raise [ArgumentError] If no block is provided
43
+ #
44
+ # @example
45
+ # Context.generate do |ctx|
46
+ # ctx.domain :user do |domain|
47
+ # domain.operation { |op| op.component [:create, :update] }
48
+ # domain.endpoint enabled: true
49
+ # end
50
+ # end
51
+ def generate(force: false, &block)
52
+ raise ArgumentError, 'Generation block is required' unless block_given?
53
+
54
+ with_error_handling do
55
+ generator = Generator.new(@global_config, force: force)
56
+ generator.instance_eval(&block)
57
+ generator.execute
58
+ true
59
+ end
60
+ end
61
+
62
+ # Rollback generated files by timestamp or last generation
63
+ #
64
+ # @param timestamp [String, nil] Specific timestamp to rollback, or nil for last
65
+ # @return [Boolean] True if rollback was successful
66
+ #
67
+ # @example
68
+ # Context.rollback('20240101120000') # specific timestamp
69
+ # Context.rollback # last generation
70
+ def rollback(timestamp = nil)
71
+ with_error_handling do
72
+ rollback_manager = RollbackManager.new
73
+
74
+ if timestamp
75
+ rollback_manager.rollback_by_timestamp?(timestamp)
76
+ else
77
+ rollback_manager.rollback_last?
78
+ end
79
+ end
80
+ end
81
+
82
+ # List all tracked generations with detailed information
83
+ #
84
+ # @return [Boolean] True if listing was successful
85
+ #
86
+ # @example
87
+ # Context.list_generations
88
+ def list_generations
89
+ with_error_handling do
90
+ RollbackManager.new.list_generations
91
+ true
92
+ end
93
+ end
94
+
95
+ # Reset the global configuration
96
+ #
97
+ # @return [Boolean] True if reset was successful
98
+ def reset_configuration
99
+ with_error_handling do
100
+ @global_config = nil
101
+ true
102
+ end
103
+ end
104
+
105
+ # Check if the system is configured
106
+ #
107
+ # @return [Boolean] True if configured
108
+ def configured?
109
+ !@global_config.nil?
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Core components
4
+ require_relative 'core/loader'
5
+ require_relative 'core/public_api'
6
+ require_relative 'core/error_handler'
7
+ require_relative 'core/migration'
8
+
9
+ module HatiRailsApi
10
+ module Context
11
+ # Core module containing all essential components
12
+ # Provides a clean separation of concerns and proper error handling
13
+ module Core
14
+ # Initialize the core system
15
+ Loader.load_all_components
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Simple string extension for camelize if ActiveSupport is not available
4
+ class String
5
+ def camelize
6
+ return self if empty?
7
+
8
+ # Handle underscored strings - capitalize all words
9
+ split('_').map(&:capitalize).join
10
+ end
11
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'file_generation'
4
+
5
+ module HatiRailsApi
6
+ module Context
7
+ # Base generator class providing common functionality
8
+ # Follows DRY principle - shared functionality for all generators
9
+ class BaseGenerator
10
+ include FileGeneration
11
+
12
+ TIMESTAMP_PATTERN = '%Y%m%d%H%M%S'
13
+
14
+ attr_reader :config, :generated_files, :timestamp, :force
15
+
16
+ def initialize(config = nil, force: false)
17
+ @config = config
18
+ @generated_files = []
19
+ @timestamp = Time.now.strftime(TIMESTAMP_PATTERN)
20
+ @force = force
21
+ @override_all = nil
22
+ end
23
+
24
+ private
25
+
26
+ def override_all_state(value = nil)
27
+ return @override_all if value.nil?
28
+
29
+ @override_all = value
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_generator'
4
+ require_relative '../shared/layer_factory'
5
+ require_relative 'file_generation'
6
+ require_relative 'model_endpoint_generator'
7
+ require_relative 'layer_component_generator'
8
+ require_relative 'operation_generator'
9
+
10
+ module HatiRailsApi
11
+ module Context
12
+ # Domain generator class for handling domain-specific generation
13
+ # Follows Single Responsibility Principle - handles domain context generation
14
+ class DomainGenerator
15
+ include LayerFactory
16
+ include FileGeneration
17
+ include ModelEndpointGenerator
18
+ include LayerComponentGenerator
19
+ include OperationGenerator
20
+
21
+ DEFAULT_DOMAIN_STATE = { enabled: false }.freeze
22
+ CONTEXT_PATH = 'app/contexts'
23
+ HATI_OPERATION_BASE = 'hati_operation/base'
24
+ DEFAULT_OPTIONS = {}.freeze
25
+ DEFAULT_FILES = [].freeze
26
+
27
+ attr_reader :name, :config, :layers, :options
28
+
29
+ def initialize(
30
+ name:,
31
+ config:,
32
+ options: DEFAULT_OPTIONS,
33
+ generated_files: DEFAULT_FILES,
34
+ force: false,
35
+ override_all: nil
36
+ )
37
+ @name = name
38
+ @config = config
39
+ @options = options
40
+ @generated_files = generated_files
41
+ @force = force
42
+ @override_all = override_all
43
+
44
+ @domain_model = DEFAULT_DOMAIN_STATE.dup
45
+ @domain_endpoint = DEFAULT_DOMAIN_STATE.dup
46
+
47
+ @layers = {}
48
+ @ctx_reference = nil
49
+
50
+ setup_layers
51
+ end
52
+
53
+ def operation(&block)
54
+ @layers[:operation] = create_operation_layer(&block)
55
+ end
56
+
57
+ def layer(name, &block)
58
+ @layers[name.to_sym] = create_standard_layer(name, &block)
59
+ end
60
+
61
+ def model(enabled_or_options = true, **options)
62
+ @domain_model =
63
+ case enabled_or_options
64
+ when false
65
+ DEFAULT_DOMAIN_STATE
66
+ when Hash
67
+ { enabled: true, options: enabled_or_options }
68
+ else
69
+ { enabled: true, options: options }
70
+ end
71
+ end
72
+
73
+ def endpoint(enabled_or_components = true, components: [], **options)
74
+ # Handle different argument patterns:
75
+ # endpoint true
76
+ # endpoint enabled: true
77
+ # endpoint [:comp1, :comp2]
78
+ # endpoint enabled: true, components: [:comp1]
79
+
80
+ enabled =
81
+ case enabled_or_components
82
+ when Array
83
+ true
84
+ when Hash
85
+ enabled_or_components.fetch(:enabled, true)
86
+ else
87
+ enabled_or_components
88
+ end
89
+
90
+ components_to_use =
91
+ case enabled_or_components
92
+ when Array
93
+ enabled_or_components
94
+ else
95
+ components
96
+ end
97
+
98
+ @domain_endpoint = create_endpoint_state(enabled, components_to_use, options)
99
+ end
100
+
101
+ def generate
102
+ generate_domain_model if @domain_model[:enabled]
103
+ generate_domain_endpoint if @domain_endpoint[:enabled]
104
+ generate_layers
105
+ end
106
+
107
+ def context_reference=(ctx)
108
+ @ctx_reference = ctx
109
+ end
110
+
111
+ private
112
+
113
+ def generate_layers
114
+ base_path = File.join(@config&.base_path || CONTEXT_PATH, @name.to_s)
115
+
116
+ # Safety check: remove file if contexts exists as file instead of directory
117
+ contexts_root = @config&.base_path || CONTEXT_PATH
118
+ File.delete(contexts_root) if File.exist?(contexts_root) && !File.directory?(contexts_root)
119
+
120
+ FileUtils.mkdir_p(base_path)
121
+
122
+ @layers.each do |layer_name, layer_config|
123
+ generate_layer(
124
+ base_path: base_path,
125
+ layer_name: layer_name,
126
+ layer_config: layer_config
127
+ )
128
+ end
129
+ end
130
+
131
+ def setup_layers
132
+ if options[:layers]
133
+ copy_default_layers
134
+ setup_specific_layers
135
+ else
136
+ copy_default_layers
137
+ end
138
+ end
139
+
140
+ def setup_specific_layers
141
+ requested_layers = options[:layers]
142
+ requested_components = options[:components]
143
+
144
+ setup_requested_layers(requested_layers, requested_components)
145
+ cleanup_unrequested_layers(requested_layers)
146
+ end
147
+
148
+ def setup_requested_layers(layers, components)
149
+ layers.each do |layer_name|
150
+ if @layers[layer_name]
151
+ update_existing_layer(layer_name, components)
152
+ else
153
+ create_new_layer(layer_name, components)
154
+ end
155
+ end
156
+ end
157
+
158
+ def update_existing_layer(name, components)
159
+ layer = @layers[name]
160
+ layer.component(components) if components
161
+ end
162
+
163
+ def create_new_layer(name, components)
164
+ @layers[name] =
165
+ if name == :operation
166
+ create_operation_layer_with_components(components)
167
+ else
168
+ create_standard_layer_with_components(name, components)
169
+ end
170
+ end
171
+
172
+ def create_operation_layer_with_components(components)
173
+ OperationLayer.new.tap do |layer|
174
+ layer.base(HATI_OPERATION_BASE)
175
+ layer.component(components) if components
176
+ end
177
+ end
178
+
179
+ def create_standard_layer_with_components(name, components)
180
+ StandardLayer.new(name).tap do |layer|
181
+ layer.base("application_#{name}")
182
+ layer.component(components) if components
183
+ end
184
+ end
185
+
186
+ def cleanup_unrequested_layers(requested_layers)
187
+ @layers.select! { |name, _| requested_layers.include?(name) }
188
+ end
189
+
190
+ def copy_default_layers
191
+ return unless @config&.domain_config&.layers
192
+
193
+ @config.domain_config.layers.each do |name, layer|
194
+ @layers[name] = layer.dup
195
+ end
196
+ end
197
+
198
+ def method_missing(method_name, ...)
199
+ result = create_dynamic_layer(method_name, ...)
200
+ return super unless result
201
+
202
+ @layers[method_name.to_sym] = result
203
+ end
204
+
205
+ def respond_to_missing?(_method_name, _include_private = false)
206
+ true
207
+ end
208
+
209
+ def create_endpoint_state(enabled, components, options)
210
+ case enabled
211
+ when false then DEFAULT_DOMAIN_STATE
212
+ when true then { enabled: true, options: options, explicit_components: components.empty? ? nil : components }
213
+ when Array then { enabled: true, options: options, explicit_components: enabled }
214
+ when Hash then { enabled: true, options: enabled, explicit_components: nil }
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module HatiRailsApi
6
+ module Context
7
+ # Shared concern for file generation operations
8
+ # Eliminates duplicate file handling code across generators
9
+ module FileGeneration
10
+ ALL_RGX = [/^a/, /all/, /^a=all$/].freeze
11
+ SKIP_RGX = [/^s/, /skip/, /^s=skip$/].freeze
12
+ YES_RGX = [/^y/, /yes$/].freeze
13
+ NO_RGX = [/^n/, /no$/].freeze
14
+
15
+ def generate_file?(file_path, content)
16
+ FileUtils.mkdir_p(File.dirname(file_path))
17
+
18
+ if create_file?(file_path)
19
+ File.write(file_path, content)
20
+ @generated_files << file_path
21
+ puts "Created: #{file_path}"
22
+ true
23
+ else
24
+ puts "Skipped: #{file_path} (already exists or user declined)"
25
+ false
26
+ end
27
+ end
28
+
29
+ def create_file?(file_path)
30
+ return true unless File.exist?(file_path)
31
+ return true if @force
32
+ return override_all? unless override_all?.nil?
33
+
34
+ prompt_for_override(file_path)
35
+ end
36
+
37
+ def override_all?
38
+ return @override_all_ref.call(:get) if shared_override_state?
39
+
40
+ @override_all
41
+ end
42
+
43
+ def shared_override_state?
44
+ respond_to?(:use_shared_override_state?) && use_shared_override_state?
45
+ end
46
+
47
+ def prompt_for_override(file_path)
48
+ print "File '#{file_path}' already exists. Override? (y/N/a=all/s=skip all): "
49
+ handle_override_response($stdin.gets&.chomp&.downcase || 'n')
50
+ end
51
+
52
+ def handle_override_response(response)
53
+ case response
54
+ when *ALL_RGX then self.override_all_state = true
55
+ when *SKIP_RGX then self.override_all_state = false
56
+ when *YES_RGX then true
57
+ else false
58
+ end
59
+ end
60
+
61
+ def override_all_state=(value)
62
+ use_shared = respond_to?(:use_shared_override_state?) && use_shared_override_state?
63
+
64
+ use_shared ? @override_all_ref&.call(value) : @override_all = value
65
+ end
66
+
67
+ def use_shared_override_state?
68
+ @override_all_ref&.respond_to?(:call)
69
+ end
70
+ end
71
+ end
72
+ end