gaskit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Gaskit
6
+ # Represents the result of an operation, encapsulating success/failure, values, errors, and execution duration.
7
+ #
8
+ # @example Using OperationResult to handle success and failure
9
+ # result = Gaskit::BaseResult.new(true, "data", nil, 1.23)
10
+ # if result.success?
11
+ # puts "Operation succeeded with value: #{result.value}"
12
+ # else
13
+ # puts "Operation failed with reason: #{result.to_h[:error]}"
14
+ # end
15
+ class OperationResult
16
+ # @return [Boolean] Whether the operation was successful.
17
+ attr_reader :success
18
+
19
+ # @return [Object, nil] The result value of the operation, if any.
20
+ attr_reader :value
21
+
22
+ # @return [Exception, nil] The error that occurred during the operation, if any.
23
+ attr_reader :error
24
+
25
+ # @return [Float] The duration of the operation in seconds.
26
+ attr_reader :duration
27
+
28
+ # @return [Hash] The context used during the operation.
29
+ attr_reader :context
30
+
31
+ # Initializes a new instance of OperationResult.
32
+ #
33
+ # @param [Boolean] success Whether the operation was successful.
34
+ # @param [Object, nil] value The value obtained as a result of the operation.
35
+ # @param [Exception, nil] error The error encountered during the operation.
36
+ # @param [Float, String] duration The time taken to complete the operation in seconds.
37
+ # @param [Hash] context Optional context metadata for this operation.
38
+ def initialize(success, value, error, duration:, context: {})
39
+ @success = success
40
+ @value = value
41
+ @error = error
42
+ @duration = format_duration(duration)
43
+ @context = context
44
+ end
45
+
46
+ # Provides a human-readable string representation of the result.
47
+ #
48
+ # @return [String] The formatted inspection string.
49
+ def inspect
50
+ "#<#{self.class.name} success=#{success?} value=#{value.inspect} duration=#{duration}>"
51
+ end
52
+
53
+ # Indicates whether the operation was successful.
54
+ #
55
+ # @return [Boolean] `true` if the operation was successful, `false` otherwise.
56
+ def success?
57
+ @success
58
+ end
59
+
60
+ # Indicates whether the operation failed.
61
+ #
62
+ # @return [Boolean] `true` if the operation failed, `false` otherwise.
63
+ def failure?
64
+ !@success
65
+ end
66
+
67
+ # Indicates whether the operation exited early using `exit(:key)`.
68
+ #
69
+ # @return [Boolean] `true` if the operation exited early, `false` otherwise.
70
+ def early_exit?
71
+ !@success && error.is_a?(Gaskit::OperationExit)
72
+ end
73
+
74
+ # Returns the status of the operation result.
75
+ #
76
+ # @return [Symbol] :success, :failure, or :early_exit
77
+ def status
78
+ return :early_exit if early_exit?
79
+
80
+ success? ? :success : :failure
81
+ end
82
+
83
+ # Converts the operation result to a structured hash.
84
+ #
85
+ # - Includes `:value` on success
86
+ # - Includes `:exit` if early_exit?
87
+ # - Includes `:error` if a failure occurred
88
+ # - Always includes `:meta` with duration and context
89
+ #
90
+ # @return [Hash] A nested representation of the result.
91
+ def to_h
92
+ hash = {
93
+ success: success?,
94
+ status: status,
95
+ value: value
96
+ }.compact
97
+
98
+ hash = failure_to_hash(hash) if failure?
99
+
100
+ hash[:meta] = {
101
+ duration: duration,
102
+ context: context
103
+ }.compact
104
+
105
+ hash.freeze
106
+ end
107
+
108
+ # Serializes the result to a JSON string.
109
+ #
110
+ # @param options [Hash] Optional hash passed to `JSON.generate`
111
+ # @return [String] JSON representation of the operation result
112
+ def to_json(options = {})
113
+ to_h.to_json(options)
114
+ end
115
+
116
+ private
117
+
118
+ # Formats duration as a 6-digit string.
119
+ #
120
+ # @param duration [Float, String]
121
+ # @return [String]
122
+ def format_duration(duration)
123
+ duration = duration.to_f
124
+ format("%.6f", duration)
125
+ end
126
+
127
+ # Builds the exit section of the result hash.
128
+ #
129
+ # @return [Hash] Details about the early exit, including key and message.
130
+ def exit_to_hash
131
+ {
132
+ key: error.key,
133
+ message: error.message,
134
+ code: error.respond_to?(:code) ? error.code : nil
135
+ }.compact
136
+ end
137
+
138
+ # Builds the error section of the result hash.
139
+ #
140
+ # @return [Hash] Details about the raised exception (if any).
141
+ def error_to_hash
142
+ {
143
+ type: error.class.name,
144
+ message: error.message,
145
+ class: error.class.name,
146
+ backtrace: error.backtrace
147
+ }.compact
148
+ end
149
+
150
+ def failure_to_hash(hash)
151
+ hash[:exit] = exit_to_hash if early_exit?
152
+ hash[:error] = error_to_hash if failure? && !early_exit? && error
153
+
154
+ hash
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/railtie"
4
+
5
+ module Gaskit
6
+ class Railtie < Rails::Railtie
7
+ config.app_generators do |g|
8
+ g.templates.unshift File.expand_path("../../generators", __dir__)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gaskit
4
+ class Repository
5
+ COMMON_AR_METHODS = %i[
6
+ find find_by find_by! find_each
7
+ where order limit offset group having
8
+ pluck select exists? create create!
9
+ update update_all destroy destroy_all
10
+ count any? none? all?
11
+ ].freeze
12
+
13
+ class << self
14
+ # Prevents instantiation of repository classes.
15
+ #
16
+ # This ensures that subclasses of `Gaskit::Repository` cannot be instantiated
17
+ # directly. If an attempt is made, a `TypeError` will be raised with the class name.
18
+ #
19
+ # @param subclass [Class] the inheriting class
20
+ # @return [void]
21
+ # @raise [TypeError] if the subclass attempts to be instantiated
22
+ def inherited(subclass)
23
+ subclass.define_singleton_method(:new) do
24
+ raise TypeError, "Repositories cannot be instantiated: #{subclass.name}"
25
+ end
26
+ super
27
+ end
28
+
29
+ # Defines or retrieves the base model for this repository.
30
+ #
31
+ # When a model is defined, common ActiveRecord-like methods are delegated
32
+ # to the model automatically. The model can only be set once per repository class.
33
+ #
34
+ # @param klass [Class, nil] The model class (e.g., an ActiveRecord model)
35
+ # @return [Class, nil] Returns the currently defined model
36
+ # @raise [StandardError] If a model is set more than once
37
+ #
38
+ # @example Define a model
39
+ # UserRepository.model(User)
40
+ #
41
+ # @example Retrieve the model
42
+ # UserRepository.model
43
+ def model(klass = nil)
44
+ return get_model_class! unless klass
45
+
46
+ raise "#{name} already has a model set" if instance_variable_defined?(:@model_class)
47
+
48
+ instance_variable_set(:@model_class, klass)
49
+ delegate_common_model_methods
50
+ end
51
+
52
+ # Returns a logger instance for this repository.
53
+ #
54
+ # The logger is specifically scoped to the repository class, allowing for
55
+ # structured and context-based logging.
56
+ #
57
+ # @return [Gaskit::Logger] The logger instance associated with the class
58
+ #
59
+ # @example Log a message
60
+ # UserRepository.logger.debug("This is a debug message")
61
+ def logger
62
+ @logger ||= Gaskit::Logger.new(self)
63
+ end
64
+
65
+ # Logs the execution time of a block of code.
66
+ #
67
+ # This method measures the duration of the given block. It conditionally logs
68
+ # the timing information based on the log level and configuration settings.
69
+ #
70
+ # If the block raises an exception, the duration is still logged, and the exception
71
+ # is re-raised after logging the error.
72
+ #
73
+ # @param context [Hash] Optional additional context to include in the log
74
+ # @param log_level [Symbol] The log level (default: `:debug`)
75
+ # @yield The block of code to be measured
76
+ # @return [Object] The result of the block execution
77
+ #
78
+ # @raise [StandardError] If the block raises an error
79
+ #
80
+ # @example Log execution time
81
+ # UserRepository.log_execution_time(log_level: :info) do
82
+ # perform_work
83
+ # end
84
+ def log_execution_time(context: {}, log_level: :debug, &block)
85
+ return yield unless should_log_execution_time?(log_level)
86
+
87
+ method_name = caller_locations(1, 1)&.first&.label
88
+ duration, result = Gaskit::Helpers.time_execution(&block)
89
+
90
+ logger.log(log_level, "#{method_name} completed", context: context.merge(duration: duration))
91
+
92
+ result
93
+ rescue StandardError => e
94
+ duration ||= "0.000000"
95
+ logger.error("#{method_name} failed", context: context.merge(duration: duration, error: e.message))
96
+
97
+ raise
98
+ end
99
+
100
+ private
101
+
102
+ # Retrieves the base model for the repository.
103
+ #
104
+ # @return [Class] The model class associated with the repository
105
+ # @raise [StandardError] If no model is defined
106
+ def get_model_class!
107
+ instance_variable_get(:@model_class) || raise("#{name} must declare a model using `model YourModel`")
108
+ end
109
+
110
+ # Delegates common ActiveRecord-like methods to the base model.
111
+ #
112
+ # This method iterates through a predefined list of methods (`COMMON_AR_METHODS`)
113
+ # and dynamically defines singleton methods for the repository. These methods
114
+ # call the corresponding methods on the associated model.
115
+ #
116
+ # @return [void]
117
+ #
118
+ # @note This method is called automatically when a model is defined using {#model}.
119
+ def delegate_common_model_methods
120
+ COMMON_AR_METHODS.each do |method_name|
121
+ define_singleton_method(method_name) do |*args, **kwargs, &block|
122
+ model.public_send(method_name, *args, **kwargs, &block)
123
+ end
124
+ end
125
+ end
126
+
127
+ # Determines whether execution time logging should occur.
128
+ #
129
+ # This checks various conditions (e.g., debug mode or logger level)
130
+ # to decide if execution timing should be logged.
131
+ #
132
+ # @param log_level [Symbol] The desired log level
133
+ # @return [Boolean] `true` if execution time logging is enabled, otherwise `false`
134
+ def should_log_execution_time?(log_level)
135
+ return true if Gaskit.configuration.debug
136
+ return true if log_level == :debug
137
+ return true if logger.respond_to?(:level) && logger.level <= Logger::DEBUG
138
+
139
+ false
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gaskit
4
+ VERSION = "0.1.0"
5
+ end
data/lib/gaskit.rb ADDED
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "gaskit/version"
4
+ require_relative "gaskit/error"
5
+ require_relative "gaskit/operation_result"
6
+ require_relative "gaskit/logger"
7
+ require_relative "gaskit/operation"
8
+ require_relative "gaskit/repository"
9
+ require_relative "gaskit/flow"
10
+ require_relative "gaskit/core"
11
+
12
+ require_relative "gaskit/boot/service"
13
+ require_relative "gaskit/boot/query"
14
+
15
+ require_relative "gaskit/railtie" if defined?(Rails::Railtie)
16
+
17
+ # Gaskit is a lightweight, extensible framework for building structured application operations.
18
+ #
19
+ # It provides a clear architecture for defining, executing, and managing operations,
20
+ # supporting common patterns like services and queries, early exits, context-aware logging,
21
+ # and standardized result wrapping.
22
+ #
23
+ # @example Configuring Gaskit
24
+ # Gaskit.config do |c|
25
+ # c.setup_logger(Logger.new(STDOUT), level: ::Logger::INFO, formatter: Gaskit::Logger.formatter(:json))
26
+ # c.context_provider = -> { { request_id: SecureRandom.uuid } }
27
+ # end
28
+ #
29
+ # @example Registering a contract
30
+ # Gaskit.register_contract(:service, MyResultClass)
31
+ #
32
+ # @example Defining a service
33
+ # class MyService < Gaskit::Service
34
+ # def call
35
+ # # do work
36
+ # "done"
37
+ # end
38
+ # end
39
+ #
40
+ # @see Gaskit::Operation
41
+ # @see Gaskit::Service
42
+ # @see Gaskit::Query
43
+ # @see Gaskit::Configuration
44
+ module Gaskit; end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+
5
+ module Gaskit
6
+ module Generators
7
+ # Generates a new Gaskit::Flow class using the class-based DSL.
8
+ #
9
+ # This generator creates a flow subclass using statically defined steps.
10
+ #
11
+ # @example Generates a class like:
12
+ # class CheckoutFlow < Gaskit::Flow
13
+ # step AddToCart
14
+ # step ApplyDiscount
15
+ # step Finalize
16
+ # end
17
+ #
18
+ # CheckoutFlow.call(user_id: 123)
19
+ #
20
+ # rails generate gaskit:flow Checkout AddToCart ApplyDiscount Finalize
21
+ #
22
+ # @see templates/flow.rb.tt for the ERB template used.
23
+ class FlowGenerator < Rails::Generators::NamedBase
24
+ source_root File.expand_path("templates", __dir__)
25
+
26
+ # List of operation steps for the flow
27
+ argument :steps, type: :array, default: [], banner: "StepOne StepTwo"
28
+
29
+ def create_flow_file
30
+ template "flow.rb.tt", File.join("app/flows", class_path, "#{file_name}_flow.rb")
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+
5
+ module Gaskit
6
+ module Generators
7
+ # A Rails generator for creating new operation classes that inherit from `Gaskit::Operation`,
8
+ # `Gaskit::Service`, or `Gaskit::Query`, depending on the specified `--type` option.
9
+ #
10
+ # This generator supports namespaced operations and places them under `app/operations`.
11
+ #
12
+ # @example Generate a base operation
13
+ # rails generate gaskit:operation CreateUser
14
+ #
15
+ # @example Generate a service operation
16
+ # rails generate gaskit:operation CreateUser --type=service
17
+ #
18
+ # @example Generate a query operation
19
+ # rails generate gaskit:operation FetchUsers --type=query
20
+ #
21
+ # @see templates/operation.rb.tt for the ERB template used.
22
+ class OperationGenerator < Rails::Generators::NamedBase
23
+ SUPPORTED_TYPES = %w[base service query].freeze
24
+
25
+ source_root File.expand_path("templates", __dir__)
26
+
27
+ class_option :type,
28
+ type: :string,
29
+ default: "base",
30
+ desc: "Operation type (base, service, query)"
31
+
32
+ def validate_type!
33
+ return if SUPPORTED_TYPES.include?(options["type"])
34
+
35
+ raise ArgumentError, "Invalid type: #{options["type"]}. Supported types are: #{SUPPORTED_TYPES.join(", ")}"
36
+ end
37
+
38
+ def create_operation_file
39
+ validate_type!
40
+
41
+ subdir = case options["type"]
42
+ when "service" then "services"
43
+ when "query" then "queries"
44
+ else "operations"
45
+ end
46
+
47
+ template "operation.rb.tt", File.join("app", subdir, class_path, "#{file_name}.rb")
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+
5
+ module Gaskit
6
+ module Generators
7
+ # A Rails generator for creating new service classes that inherit from `Gaskit::Query`.
8
+ #
9
+ # This generator supports namespaced queries and places them under `app/queries`.
10
+ #
11
+ # @example Generate a base operation
12
+ # rails generate gaskit:query FetchUserQuery
13
+ #
14
+ # @see templates/operation.rb.tt for the ERB template used.
15
+ class QueryGenerator < OperationGenerator
16
+ def initialize(*args)
17
+ super
18
+ options[:type] = "query"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+
5
+ module Gaskit
6
+ module Generators
7
+ # Generates a repository for a given model name
8
+ #
9
+ # @example
10
+ # rails generate gaskit:repository User
11
+ #
12
+ # # Creates:
13
+ # # app/repositories/user_repository.rb
14
+ #
15
+ # # With contents:
16
+ # # class UserRepository < Gaskit::Repository
17
+ # # model User
18
+ # # end
19
+ class RepositoryGenerator < Rails::Generators::NamedBase
20
+ source_root File.expand_path("templates", __dir__)
21
+
22
+ def create_repository_file
23
+ @model_name = class_name
24
+ @repository_class_name = "#{class_name}Repository"
25
+ @file_path = File.join("app/repositories", class_path, "#{file_name}_repository.rb")
26
+
27
+ template "repository.rb.tt", @file_path
28
+ end
29
+
30
+ private
31
+
32
+ def file_name
33
+ super.underscore
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+
5
+ module Gaskit
6
+ module Generators
7
+ # A Rails generator for creating new service classes that inherit from `Gaskit::Service`.
8
+ #
9
+ # This generator supports namespaced services and places them under `app/services`.
10
+ #
11
+ # @example Generate a base operation
12
+ # rails generate gaskit:service CreateUserService
13
+ #
14
+ # @see templates/operation.rb.tt for the ERB template used.
15
+ class ServiceGenerator < OperationGenerator
16
+ def initialize(*args)
17
+ super
18
+ options[:type] = "service"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= class_name %>Flow < Gaskit::Flow
4
+ <% steps.each do |step| -%>
5
+ step <%= step %>
6
+ <% end -%>
7
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ <% gaskit_type = case options["type"] %>
4
+ <% when "service" then "Gaskit::Service" %>
5
+ <% when "query" then "Gaskit::Query" %>
6
+ <% else "Gaskit::Operation" %>
7
+ <% end %>
8
+
9
+ class <%= class_name %> < <%= gaskit_type %>
10
+ # This is a generated Gaskit operation. Add your logic below.
11
+
12
+ def call(*args, **kwargs)
13
+ # Your operation logic goes here.
14
+ "ok"
15
+ end
16
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= @repository_class_name %> < Gaskit::Repository
4
+ model <%= @model_name %>
5
+
6
+ # Define additional methods here as needed
7
+ end
data/sig/gaskit.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Gaskit
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end