diff --git a/.cursor/prompts/yardoc.md b/.cursor/prompts/yardoc.md index c42d7517a..094ef18c6 100644 --- a/.cursor/prompts/yardoc.md +++ b/.cursor/prompts/yardoc.md @@ -12,3 +12,4 @@ Add yardoc to the active tab using the following guidelines: - Method level docs should include `@example`, `param`, `@options`, `@return`, and any `@raise` - Hash `@params` should expand with possible `@option` - Module and method level docs should NOT include `@since` +- Add RBS inline comments after YARDoc block diff --git a/CHANGELOG.md b/CHANGELOG.md index 1eee86f9c..043cd8e71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## [UNRELEASED] +### Added +- Added RBS inlines type signatures +- Added YARDocs for `attr_reader` and `attr_accessor` methods + ## [1.9.0] - 2025-10-21 ### Added diff --git a/LLM.md b/LLM.md index f9115e1f1..54ed0637c 100644 --- a/LLM.md +++ b/LLM.md @@ -372,6 +372,15 @@ end > [!TIP] > Use **present tense verbs + noun** for task names, eg: `ModerateBlogPost`, `ScheduleAppointment`, `ValidateDocument` +## Type safety + +CMDx includes built-in RBS (Ruby Type Signature) inline annotations throughout the codebase, providing type information for static analysis and editor support. + +- **Type checking** — Catch type errors before runtime using tools like Steep or TypeProf +- **Better IDE support** — Enhanced autocomplete, navigation, and inline documentation +- **Self-documenting code** — Clear method signatures and return types +- **Refactoring confidence** — Type-aware refactoring reduces bugs + --- url: https://github.com/drexed/cmdx/blob/main/docs/basics/setup.md diff --git a/docs/getting_started.md b/docs/getting_started.md index 47bd9880c..aa695c197 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -367,3 +367,12 @@ end !!! tip Use **present tense verbs + noun** for task names, eg: `ModerateBlogPost`, `ScheduleAppointment`, `ValidateDocument` + +## Type safety + +CMDx includes built-in RBS (Ruby Type Signature) inline annotations throughout the codebase, providing type information for static analysis and editor support. + +- **Type checking** — Catch type errors before runtime using tools like Steep or TypeProf +- **Better IDE support** — Enhanced autocomplete, navigation, and inline documentation +- **Self-documenting code** — Clear method signatures and return types +- **Refactoring confidence** — Type-aware refactoring reduces bugs diff --git a/lib/cmdx.rb b/lib/cmdx.rb index 1c34bb2e9..9f81305ce 100644 --- a/lib/cmdx.rb +++ b/lib/cmdx.rb @@ -17,6 +17,14 @@ module CMDx extend self + # Returns the path to the CMDx gem. + # + # @return [Pathname] the path to the CMDx gem + # + # @example + # CMDx.gem_path # => Pathname.new("/path/to/cmdx") + # + # @rbs return: Pathname def gem_path @gem_path ||= Pathname.new(__dir__).parent end diff --git a/lib/cmdx/attribute.rb b/lib/cmdx/attribute.rb index 3a6162d3e..b6c9d57d2 100644 --- a/lib/cmdx/attribute.rb +++ b/lib/cmdx/attribute.rb @@ -6,14 +6,71 @@ module CMDx # They can be nested to create complex hierarchical data structures. class Attribute + # @rbs AFFIX: Proc AFFIX = proc do |value, &block| value == true ? block.call : value end.freeze private_constant :AFFIX + # Returns the task instance associated with this attribute. + # + # @return [CMDx::Task] The task instance + # + # @example + # attribute.task.context[:user_id] # => 42 + # + # @rbs @task: Task attr_accessor :task - attr_reader :name, :options, :children, :parent, :types + # Returns the name of this attribute. + # + # @return [Symbol] The attribute name + # + # @example + # attribute.name # => :user_id + # + # @rbs @name: Symbol + attr_reader :name + + # Returns the configuration options for this attribute. + # + # @return [Hash{Symbol => Object}] Configuration options hash + # + # @example + # attribute.options # => { required: true, default: 0 } + # + # @rbs @options: Hash[Symbol, untyped] + attr_reader :options + + # Returns the child attributes for nested structures. + # + # @return [Array] Array of child attributes + # + # @example + # attribute.children # => [#, #] + # + # @rbs @children: Array[Attribute] + attr_reader :children + + # Returns the parent attribute if this is a nested attribute. + # + # @return [Attribute, nil] The parent attribute, or nil if root-level + # + # @example + # attribute.parent # => # + # + # @rbs @parent: (Attribute | nil) + attr_reader :parent + + # Returns the expected type(s) for this attribute's value. + # + # @return [Array] Array of expected type classes + # + # @example + # attribute.types # => [Integer, String] + # + # @rbs @types: Array[Class] + attr_reader :types # Creates a new attribute with the specified name and configuration. # @@ -35,6 +92,8 @@ class Attribute # required :name, types: String # optional :email, types: String # end + # + # @rbs ((Symbol | String) name, ?Hash[Symbol, untyped] options) ?{ () -> void } -> void def initialize(name, options = {}, &) @parent = options.delete(:parent) @required = options.delete(:required) || false @@ -62,6 +121,8 @@ class << self # # @example # Attribute.build(:first_name, :last_name, required: true, types: String) + # + # @rbs (*untyped names, **untyped options) ?{ () -> void } -> Array[Attribute] def build(*names, **options, &) if names.none? raise ArgumentError, "no attributes given" @@ -83,6 +144,8 @@ def build(*names, **options, &) # # @example # Attribute.optional(:description, :tags, types: String) + # + # @rbs (*untyped names, **untyped options) ?{ () -> void } -> Array[Attribute] def optional(*names, **options, &) build(*names, **options.merge(required: false), &) end @@ -98,6 +161,8 @@ def optional(*names, **options, &) # # @example # Attribute.required(:id, :name, types: [Integer, String]) + # + # @rbs (*untyped names, **untyped options) ?{ () -> void } -> Array[Attribute] def required(*names, **options, &) build(*names, **options.merge(required: true), &) end @@ -110,6 +175,8 @@ def required(*names, **options, &) # # @example # attribute.required? # => true + # + # @rbs () -> bool def required? !!@required end @@ -120,6 +187,8 @@ def required? # # @example # attribute.source # => :context + # + # @rbs () -> untyped def source @source ||= parent&.method_name || begin value = options[:source] @@ -140,6 +209,8 @@ def source # # @example # attribute.method_name # => :user_name + # + # @rbs () -> Symbol def method_name @method_name ||= options[:as] || begin prefix = AFFIX.call(options[:prefix]) { "#{source}_" } @@ -150,6 +221,8 @@ def method_name end # Defines and verifies the entire attribute tree including nested children. + # + # @rbs () -> void def define_and_verify_tree define_and_verify @@ -172,6 +245,8 @@ def define_and_verify_tree # # @example # attributes :street, :city, :zip, types: String + # + # @rbs (*untyped names, **untyped options) ?{ () -> void } -> Array[Attribute] def attributes(*names, **options, &) attrs = self.class.build(*names, **options.merge(parent: self), &) children.concat(attrs) @@ -189,6 +264,8 @@ def attributes(*names, **options, &) # # @example # optional :middle_name, :nickname, types: String + # + # @rbs (*untyped names, **untyped options) ?{ () -> void } -> Array[Attribute] def optional(*names, **options, &) attributes(*names, **options.merge(required: false), &) end @@ -204,6 +281,8 @@ def optional(*names, **options, &) # # @example # required :first_name, :last_name, types: String + # + # @rbs (*untyped names, **untyped options) ?{ () -> void } -> Array[Attribute] def required(*names, **options, &) attributes(*names, **options.merge(required: true), &) end @@ -211,6 +290,8 @@ def required(*names, **options, &) # Defines the attribute method on the task and validates the configuration. # # @raise [RuntimeError] When the method name is already defined on the task + # + # @rbs () -> void def define_and_verify if task.respond_to?(method_name, true) raise <<~MESSAGE diff --git a/lib/cmdx/attribute_registry.rb b/lib/cmdx/attribute_registry.rb index c186b74fc..78459622b 100644 --- a/lib/cmdx/attribute_registry.rb +++ b/lib/cmdx/attribute_registry.rb @@ -6,6 +6,14 @@ module CMDx # in a hierarchical structure, supporting nested attribute definitions. class AttributeRegistry + # Returns the collection of registered attributes. + # + # @return [Array] Array of registered attributes + # + # @example + # registry.registry # => [#, #] + # + # @rbs @registry: Array[Attribute] attr_reader :registry alias to_a registry @@ -18,6 +26,8 @@ class AttributeRegistry # @example # registry = AttributeRegistry.new # registry = AttributeRegistry.new([attr1, attr2]) + # + # @rbs (?Array[Attribute] registry) -> void def initialize(registry = []) @registry = registry end @@ -28,6 +38,8 @@ def initialize(registry = []) # # @example # new_registry = registry.dup + # + # @rbs () -> AttributeRegistry def dup self.class.new(registry.dup) end @@ -41,6 +53,8 @@ def dup # @example # registry.register(attribute) # registry.register([attr1, attr2]) + # + # @rbs (Attribute | Array[Attribute] attributes) -> self def register(attributes) @registry.concat(Array(attributes)) self @@ -56,6 +70,8 @@ def register(attributes) # @example # registry.deregister(:name) # registry.deregister(['name1', 'name2']) + # + # @rbs ((Symbol | String | Array[Symbol | String]) names) -> self def deregister(names) Array(names).each do |name| @registry.reject! { |attribute| matches_attribute_tree?(attribute, name.to_sym) } @@ -69,6 +85,8 @@ def deregister(names) # and validate the attribute hierarchy. # # @param task [Task] The task to associate with all attributes + # + # @rbs (Task task) -> void def define_and_verify(task) registry.each do |attribute| attribute.task = task @@ -84,6 +102,8 @@ def define_and_verify(task) # @param name [Symbol] The name to match against # # @return [Boolean] True if the attribute or any child matches the name + # + # @rbs (Attribute attribute, Symbol name) -> bool def matches_attribute_tree?(attribute, name) return true if attribute.method_name == name diff --git a/lib/cmdx/attribute_value.rb b/lib/cmdx/attribute_value.rb index 8702f341b..140863bbb 100644 --- a/lib/cmdx/attribute_value.rb +++ b/lib/cmdx/attribute_value.rb @@ -8,6 +8,14 @@ class AttributeValue extend Forwardable + # Returns the attribute managed by this value handler. + # + # @return [Attribute] The attribute instance + # + # @example + # attr_value.attribute.name # => :user_id + # + # @rbs @attribute: Attribute attr_reader :attribute def_delegators :attribute, :task, :parent, :name, :options, :types, :source, :method_name, :required? @@ -20,6 +28,8 @@ class AttributeValue # @example # attr = Attribute.new(:user_id, required: true) # attr_value = AttributeValue.new(attr) + # + # @rbs (Attribute attribute) -> void def initialize(attribute) @attribute = attribute end @@ -30,6 +40,8 @@ def initialize(attribute) # # @example # attr_value.value # => "john_doe" + # + # @rbs () -> untyped def value attributes[method_name] end @@ -41,6 +53,8 @@ def value # # @example # attr_value.generate # => 42 + # + # @rbs () -> untyped def generate return value if attributes.key?(method_name) @@ -64,6 +78,8 @@ def generate # @example # attr_value.validate # # Validates value against :presence, :format, etc. + # + # @rbs () -> void def validate registry = task.class.settings[:validators] @@ -86,6 +102,7 @@ def validate # @example # # Sources from task method, proc, or direct value # source_value # => "raw_value" + # @rbs () -> untyped def source_value sourced_value = case source @@ -115,6 +132,8 @@ def source_value # @example # # Default can be symbol, proc, or direct value # -> { rand(100) } # => 23 + # + # @rbs () -> untyped def default_value default = options[:default] @@ -140,6 +159,8 @@ def default_value # @example # # Derives from hash key, method call, or proc execution # context.user_id # => 42 + # + # @rbs (untyped source_value) -> untyped def derive_value(source_value) derived_value = case source_value @@ -163,6 +184,8 @@ def derive_value(source_value) # # @example # :downcase # => "hello" + # + # @rbs (untyped derived_value) -> untyped def transform_value(derived_value) transform = options[:transform] @@ -186,6 +209,8 @@ def transform_value(derived_value) # @example # # Coerces "42" to Integer, "true" to Boolean, etc. # coerce_value("42") # => 42 + # + # @rbs (untyped transformed_value) -> untyped def coerce_value(transformed_value) return transformed_value if types.empty? diff --git a/lib/cmdx/callback_registry.rb b/lib/cmdx/callback_registry.rb index 128557581..eee1365ea 100644 --- a/lib/cmdx/callback_registry.rb +++ b/lib/cmdx/callback_registry.rb @@ -7,6 +7,7 @@ module CMDx # Each callback type represents a specific execution phase or outcome. class CallbackRegistry + # @rbs TYPES: Array[Symbol] TYPES = %i[ before_validation before_execution @@ -20,10 +21,20 @@ class CallbackRegistry on_bad ].freeze + # Returns the internal registry of callbacks organized by type. + # + # @return [Hash{Symbol => Set}] Hash mapping callback types to their registered callables + # + # @example + # registry.registry # => { before_execution: # } + # + # @rbs @registry: Hash[Symbol, Set[Array[untyped]]] attr_reader :registry alias to_h registry # @param registry [Hash] Initial registry hash, defaults to empty + # + # @rbs (?Hash[Symbol, Set[Array[untyped]]] registry) -> void def initialize(registry = {}) @registry = registry end @@ -31,6 +42,8 @@ def initialize(registry = {}) # Creates a deep copy of the registry with duplicated callable sets # # @return [CallbackRegistry] A new instance with duplicated registry contents + # + # @rbs () -> CallbackRegistry def dup self.class.new(registry.transform_values(&:dup)) end @@ -54,6 +67,8 @@ def dup # registry.register(:on_success, if: { status: :completed }) do |task| # task.log("Success callback executed") # end + # + # @rbs (Symbol type, *untyped callables, **untyped options) ?{ (Task) -> void } -> self def register(type, *callables, **options, &block) callables << block if block_given? @@ -73,6 +88,8 @@ def register(type, *callables, **options, &block) # # @example Remove a specific callback # registry.deregister(:before_execution, :validate_inputs) + # + # @rbs (Symbol type, *untyped callables, **untyped options) ?{ (Task) -> void } -> self def deregister(type, *callables, **options, &block) callables << block if block_given? return self unless registry[type] @@ -91,6 +108,8 @@ def deregister(type, *callables, **options, &block) # # @example Invoke all before_execution callbacks # registry.invoke(:before_execution, task) + # + # @rbs (Symbol type, Task task) -> void def invoke(type, task) raise TypeError, "unknown callback type #{type.inspect}" unless TYPES.include?(type) diff --git a/lib/cmdx/chain.rb b/lib/cmdx/chain.rb index fbb4347e8..b0570097c 100644 --- a/lib/cmdx/chain.rb +++ b/lib/cmdx/chain.rb @@ -8,9 +8,28 @@ class Chain extend Forwardable + # @rbs THREAD_KEY: Symbol THREAD_KEY = :cmdx_chain - attr_reader :id, :results + # Returns the unique identifier for this chain. + # + # @return [String] The chain identifier + # + # @example + # chain.id # => "abc123xyz" + # + # @rbs @id: String + attr_reader :id + + # Returns the collection of execution results in this chain. + # + # @return [Array] Array of task results + # + # @example + # chain.results # => [#, #] + # + # @rbs @results: Array[Result] + attr_reader :results def_delegators :results, :index, :first, :last, :size def_delegators :first, :state, :status, :outcome, :runtime @@ -18,6 +37,8 @@ class Chain # Creates a new chain with a unique identifier and empty results collection. # # @return [Chain] A new chain instance + # + # @rbs () -> void def initialize @id = Identifier.generate @results = [] @@ -34,6 +55,8 @@ class << self # if chain # puts "Current chain: #{chain.id}" # end + # + # @rbs () -> Chain? def current Thread.current[THREAD_KEY] end @@ -46,6 +69,8 @@ def current # # @example # Chain.current = my_chain + # + # @rbs (Chain chain) -> Chain def current=(chain) Thread.current[THREAD_KEY] = chain end @@ -56,6 +81,8 @@ def current=(chain) # # @example # Chain.clear + # + # @rbs () -> nil def clear Thread.current[THREAD_KEY] = nil end @@ -73,6 +100,8 @@ def clear # result = task.execute # chain = Chain.build(result) # puts "Chain size: #{chain.size}" + # + # @rbs (Result result) -> Chain def build(result) raise TypeError, "must be a CMDx::Result" unless result.is_a?(Result) @@ -95,6 +124,8 @@ def build(result) # chain_hash = chain.to_h # puts chain_hash[:id] # puts chain_hash[:results].size + # + # @rbs () -> Hash[Symbol, untyped] def to_h { id: id, @@ -108,6 +139,8 @@ def to_h # # @example # puts chain.to_s + # + # @rbs () -> String def to_s Utils::Format.to_str(to_h) end diff --git a/lib/cmdx/coercion_registry.rb b/lib/cmdx/coercion_registry.rb index f11dc285e..d1285bbf9 100644 --- a/lib/cmdx/coercion_registry.rb +++ b/lib/cmdx/coercion_registry.rb @@ -7,6 +7,14 @@ module CMDx # for various data types including arrays, numbers, dates, and other primitives. class CoercionRegistry + # Returns the internal registry mapping coercion types to handler classes. + # + # @return [Hash{Symbol => Class}] Hash of coercion type names to coercion classes + # + # @example + # registry.registry # => { integer: Coercions::Integer, boolean: Coercions::Boolean } + # + # @rbs @registry: Hash[Symbol, Class] attr_reader :registry alias to_h registry @@ -17,6 +25,8 @@ class CoercionRegistry # @example # registry = CoercionRegistry.new # registry = CoercionRegistry.new(custom: CustomCoercion) + # + # @rbs (?Hash[Symbol, Class]? registry) -> void def initialize(registry = nil) @registry = registry || { array: Coercions::Array, @@ -40,6 +50,8 @@ def initialize(registry = nil) # # @example # new_registry = registry.dup + # + # @rbs () -> CoercionRegistry def dup self.class.new(registry.dup) end @@ -54,6 +66,8 @@ def dup # @example # registry.register(:custom_type, CustomCoercion) # registry.register("another_type", AnotherCoercion) + # + # @rbs ((Symbol | String) name, Class coercion) -> self def register(name, coercion) registry[name.to_sym] = coercion self @@ -68,6 +82,8 @@ def register(name, coercion) # @example # registry.deregister(:custom_type) # registry.deregister("another_type") + # + # @rbs ((Symbol | String) name) -> self def deregister(name) registry.delete(name.to_sym) self @@ -87,6 +103,8 @@ def deregister(name) # @example # result = registry.coerce(:integer, task, "42") # result = registry.coerce(:boolean, task, "true", strict: true) + # + # @rbs (Symbol type, untyped task, untyped value, ?Hash[Symbol, untyped] options) -> untyped def coerce(type, task, value, options = {}) raise TypeError, "unknown coercion type #{type.inspect}" unless registry.key?(type) diff --git a/lib/cmdx/coercions/array.rb b/lib/cmdx/coercions/array.rb index f7065f4a0..f4f498209 100644 --- a/lib/cmdx/coercions/array.rb +++ b/lib/cmdx/coercions/array.rb @@ -26,6 +26,8 @@ module Array # Array.call("hello") # => ["hello"] # Array.call(42) # => [42] # Array.call(nil) # => [] + # + # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Array[untyped] def call(value, options = {}) if value.is_a?(::String) && value.start_with?("[") JSON.parse(value) diff --git a/lib/cmdx/coercions/big_decimal.rb b/lib/cmdx/coercions/big_decimal.rb index a5448b177..6d3e51d1f 100644 --- a/lib/cmdx/coercions/big_decimal.rb +++ b/lib/cmdx/coercions/big_decimal.rb @@ -10,6 +10,7 @@ module BigDecimal extend self + # @rbs DEFAULT_PRECISION: Integer DEFAULT_PRECISION = 14 # Converts a value to a BigDecimal @@ -28,6 +29,8 @@ module BigDecimal # @example Convert other numeric types # BigDecimal.call(42) # => # # BigDecimal.call(3.14159) # => # + # + # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> BigDecimal def call(value, options = {}) BigDecimal(value, options[:precision] || DEFAULT_PRECISION) rescue ArgumentError, TypeError diff --git a/lib/cmdx/coercions/boolean.rb b/lib/cmdx/coercions/boolean.rb index 01b4261ee..8e3d71068 100644 --- a/lib/cmdx/coercions/boolean.rb +++ b/lib/cmdx/coercions/boolean.rb @@ -10,7 +10,10 @@ module Boolean extend self + # @rbs FALSEY: Regexp FALSEY = /^(false|f|no|n|0)$/i + + # @rbs TRUTHY: Regexp TRUTHY = /^(true|t|yes|y|1)$/i # Converts a value to a Boolean @@ -34,6 +37,8 @@ module Boolean # @example Handle case-insensitive input # Boolean.call("TRUE") # => true # Boolean.call("False") # => false + # + # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> bool def call(value, options = {}) case value.to_s.downcase when FALSEY then false diff --git a/lib/cmdx/coercions/complex.rb b/lib/cmdx/coercions/complex.rb index 2a278d168..56100e644 100644 --- a/lib/cmdx/coercions/complex.rb +++ b/lib/cmdx/coercions/complex.rb @@ -26,6 +26,8 @@ module Complex # Complex.call(5) # => (5+0i) # Complex.call(3.14) # => (3.14+0i) # Complex.call(Complex(1, 2)) # => (1+2i) + # + # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Complex def call(value, options = {}) Complex(value) rescue ArgumentError, TypeError diff --git a/lib/cmdx/coercions/date.rb b/lib/cmdx/coercions/date.rb index 7db1b52ae..ab7b21851 100644 --- a/lib/cmdx/coercions/date.rb +++ b/lib/cmdx/coercions/date.rb @@ -12,6 +12,8 @@ module Date extend self # Types that are already date-like and don't need conversion + # + # @rbs ANALOG_TYPES: Array[String] ANALOG_TYPES = %w[Date DateTime Time].freeze # Converts a value to a Date object @@ -33,6 +35,8 @@ module Date # @example Return existing Date objects unchanged # Date.call(Date.new(2023, 12, 25)) # => # # Date.call(DateTime.new(2023, 12, 25)) # => # + # + # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Date def call(value, options = {}) return value if ANALOG_TYPES.include?(value.class.name) return ::Date.strptime(value, options[:strptime]) if options[:strptime] diff --git a/lib/cmdx/coercions/date_time.rb b/lib/cmdx/coercions/date_time.rb index b2fbb16de..5f56792ea 100644 --- a/lib/cmdx/coercions/date_time.rb +++ b/lib/cmdx/coercions/date_time.rb @@ -11,6 +11,9 @@ module DateTime extend self + # Types that are already date-time-like and don't need conversion + # + # @rbs ANALOG_TYPES: Array[String] ANALOG_TYPES = %w[Date DateTime Time].freeze # Converts a value to a DateTime @@ -32,6 +35,8 @@ module DateTime # @example Convert existing date objects # DateTime.call(Date.new(2023, 12, 25)) # => # # DateTime.call(Time.new(2023, 12, 25)) # => # + # + # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> DateTime def call(value, options = {}) return value if ANALOG_TYPES.include?(value.class.name) return ::DateTime.strptime(value, options[:strptime]) if options[:strptime] diff --git a/lib/cmdx/coercions/float.rb b/lib/cmdx/coercions/float.rb index 67b56360c..036b010b1 100644 --- a/lib/cmdx/coercions/float.rb +++ b/lib/cmdx/coercions/float.rb @@ -30,6 +30,8 @@ module Float # Float.call(BigDecimal("123.456")) # => 123.456 # Float.call(Rational(3, 4)) # => 0.75 # Float.call(Complex(5.0, 0)) # => 5.0 + # + # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Float def call(value, options = {}) Float(value) rescue ArgumentError, RangeError, TypeError diff --git a/lib/cmdx/coercions/hash.rb b/lib/cmdx/coercions/hash.rb index b741f1ba8..87100a1c3 100644 --- a/lib/cmdx/coercions/hash.rb +++ b/lib/cmdx/coercions/hash.rb @@ -30,6 +30,8 @@ module Hash # Hash.call([:a, 1, :b, 2]) # => {a: 1, b: 2} # @example Coerce from JSON string # Hash.call('{"key": "value"}') # => {"key" => "value"} + # + # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Hash[untyped, untyped] def call(value, options = {}) if value.nil? {} diff --git a/lib/cmdx/coercions/integer.rb b/lib/cmdx/coercions/integer.rb index fac67c725..953945295 100644 --- a/lib/cmdx/coercions/integer.rb +++ b/lib/cmdx/coercions/integer.rb @@ -34,6 +34,8 @@ module Integer # Integer.call(nil) # => 0 # Integer.call(false) # => 0 # Integer.call(true) # => 1 + # + # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Integer def call(value, options = {}) Integer(value) rescue ArgumentError, FloatDomainError, RangeError, TypeError diff --git a/lib/cmdx/coercions/rational.rb b/lib/cmdx/coercions/rational.rb index da911dbd5..51251b9bb 100644 --- a/lib/cmdx/coercions/rational.rb +++ b/lib/cmdx/coercions/rational.rb @@ -33,6 +33,8 @@ module Rational # Rational.call("") # => (0/1) # Rational.call(nil) # => (0/1) # Rational.call(0) # => (0/1) + # + # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Rational def call(value, options = {}) Rational(value) rescue ArgumentError, FloatDomainError, RangeError, TypeError, ZeroDivisionError diff --git a/lib/cmdx/coercions/string.rb b/lib/cmdx/coercions/string.rb index aa359b337..5363e0b69 100644 --- a/lib/cmdx/coercions/string.rb +++ b/lib/cmdx/coercions/string.rb @@ -26,6 +26,8 @@ module String # String.call([1, 2, 3]) # => "[1, 2, 3]" # String.call(nil) # => "" # String.call(true) # => "true" + # + # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> String def call(value, options = {}) String(value) end diff --git a/lib/cmdx/coercions/symbol.rb b/lib/cmdx/coercions/symbol.rb index 556bc6afc..23dc8171a 100644 --- a/lib/cmdx/coercions/symbol.rb +++ b/lib/cmdx/coercions/symbol.rb @@ -25,6 +25,8 @@ module Symbol # Symbol.call("user_id") # => :user_id # Symbol.call("") # => :"" # Symbol.call(:existing) # => :existing + # + # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Symbol def call(value, options = {}) value.to_sym rescue NoMethodError diff --git a/lib/cmdx/coercions/time.rb b/lib/cmdx/coercions/time.rb index d2474a3ea..5573434d6 100644 --- a/lib/cmdx/coercions/time.rb +++ b/lib/cmdx/coercions/time.rb @@ -11,6 +11,9 @@ module Time extend self + # Types that are already time-like and don't need conversion + # + # @rbs ANALOG_TYPES: Array[String] ANALOG_TYPES = %w[DateTime Time].freeze # Converts a value to a Time object @@ -34,6 +37,8 @@ module Time # @example Convert strings with custom format # Time.call("25/12/2023", strptime: "%d/%m/%Y") # => Time object # Time.call("12-25-2023", strptime: "%m-%d-%Y") # => Time object + # + # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Time def call(value, options = {}) return value if ANALOG_TYPES.include?(value.class.name) return value.to_time if value.respond_to?(:to_time) diff --git a/lib/cmdx/configuration.rb b/lib/cmdx/configuration.rb index afd7ab4e0..bcaf6ce42 100644 --- a/lib/cmdx/configuration.rb +++ b/lib/cmdx/configuration.rb @@ -6,11 +6,109 @@ module CMDx # callbacks, coercions, validators, breakpoints, backtraces, and logging. class Configuration + # @rbs DEFAULT_BREAKPOINTS: Array[String] DEFAULT_BREAKPOINTS = %w[failed].freeze - attr_accessor :middlewares, :callbacks, :coercions, :validators, - :task_breakpoints, :workflow_breakpoints, :logger, - :backtrace, :backtrace_cleaner, :exception_handler + # Returns the middleware registry for task execution. + # + # @return [MiddlewareRegistry] The middleware registry + # + # @example + # config.middlewares.register(CustomMiddleware) + # + # @rbs @middlewares: MiddlewareRegistry + attr_accessor :middlewares + + # Returns the callback registry for task lifecycle hooks. + # + # @return [CallbackRegistry] The callback registry + # + # @example + # config.callbacks.register(:before_execution, :log_start) + # + # @rbs @callbacks: CallbackRegistry + attr_accessor :callbacks + + # Returns the coercion registry for type conversions. + # + # @return [CoercionRegistry] The coercion registry + # + # @example + # config.coercions.register(:custom, CustomCoercion) + # + # @rbs @coercions: CoercionRegistry + attr_accessor :coercions + + # Returns the validator registry for attribute validation. + # + # @return [ValidatorRegistry] The validator registry + # + # @example + # config.validators.register(:email, EmailValidator) + # + # @rbs @validators: ValidatorRegistry + attr_accessor :validators + + # Returns the breakpoint statuses for task execution interruption. + # + # @return [Array] Array of status names that trigger breakpoints + # + # @example + # config.task_breakpoints = ["failed", "skipped"] + # + # @rbs @task_breakpoints: Array[String] + attr_accessor :task_breakpoints + + # Returns the breakpoint statuses for workflow execution interruption. + # + # @return [Array] Array of status names that trigger breakpoints + # + # @example + # config.workflow_breakpoints = ["failed", "skipped"] + # + # @rbs @task_breakpoints: Array[String] + # @rbs @workflow_breakpoints: Array[String] + attr_accessor :workflow_breakpoints + + # Returns the logger instance for CMDx operations. + # + # @return [Logger] The logger instance + # + # @example + # config.logger.level = Logger::DEBUG + # + # @rbs @logger: Logger + attr_accessor :logger + + # Returns whether to log backtraces for failed tasks. + # + # @return [Boolean] true if backtraces should be logged + # + # @example + # config.backtrace = true + # + # @rbs @backtrace: bool + attr_accessor :backtrace + + # Returns the proc used to clean backtraces before logging. + # + # @return [Proc, nil] The backtrace cleaner proc, or nil if not set + # + # @example + # config.backtrace_cleaner = ->(bt) { bt.first(5) } + # + # @rbs @backtrace_cleaner: (Proc | nil) + attr_accessor :backtrace_cleaner + + # Returns the proc called when exceptions occur during execution. + # + # @return [Proc, nil] The exception handler proc, or nil if not set + # + # @example + # config.exception_handler = ->(task, error) { Sentry.capture_exception(error) } + # + # @rbs @exception_handler: (Proc | nil) + attr_accessor :exception_handler # Initializes a new Configuration instance with default values. # @@ -23,6 +121,8 @@ class Configuration # config = Configuration.new # config.middlewares.class # => MiddlewareRegistry # config.task_breakpoints # => ["failed"] + # + # @rbs () -> void def initialize @middlewares = MiddlewareRegistry.new @callbacks = CallbackRegistry.new @@ -52,6 +152,8 @@ def initialize # config = Configuration.new # config.to_h # # => { middlewares: #, callbacks: #, ... } + # + # @rbs () -> Hash[Symbol, untyped] def to_h { middlewares: @middlewares, @@ -78,6 +180,8 @@ def to_h # @example # config = CMDx.configuration # config.middlewares # => # + # + # @rbs () -> Configuration def configuration return @configuration if @configuration @@ -99,6 +203,8 @@ def configuration # config.task_breakpoints = ["failed", "skipped"] # config.logger.level = Logger::DEBUG # end + # + # @rbs () { (Configuration) -> void } -> Configuration def configure raise ArgumentError, "block required" unless block_given? @@ -114,6 +220,8 @@ def configure # @example # CMDx.reset_configuration! # # Configuration is now reset to defaults + # + # @rbs () -> Configuration def reset_configuration! @configuration = Configuration.new end diff --git a/lib/cmdx/context.rb b/lib/cmdx/context.rb index 62e4b275c..140500e20 100644 --- a/lib/cmdx/context.rb +++ b/lib/cmdx/context.rb @@ -11,6 +11,14 @@ class Context extend Forwardable + # Returns the internal hash storing context data. + # + # @return [Hash{Symbol => Object}] The internal hash table + # + # @example + # context.table # => { name: "John", age: 30 } + # + # @rbs @table: Hash[Symbol, untyped] attr_reader :table alias to_h table @@ -28,6 +36,8 @@ class Context # @example # context = Context.new(name: "John", age: 30) # context[:name] # => "John" + # + # @rbs (untyped args) -> void def initialize(args = {}) @table = if args.respond_to?(:to_hash) @@ -50,6 +60,8 @@ def initialize(args = {}) # existing = Context.new(name: "John") # built = Context.build(existing) # reuses existing context # built.object_id == existing.object_id # => true + # + # @rbs (untyped context) -> Context def self.build(context = {}) if context.is_a?(self) && !context.frozen? context @@ -70,6 +82,8 @@ def self.build(context = {}) # context = Context.new(name: "John") # context[:name] # => "John" # context["name"] # => "John" (automatically converted to symbol) + # + # @rbs ((String | Symbol) key) -> untyped def [](key) table[key.to_sym] end @@ -85,6 +99,8 @@ def [](key) # context = Context.new # context.store(:name, "John") # context[:name] # => "John" + # + # @rbs ((String | Symbol) key, untyped value) -> untyped def store(key, value) table[key.to_sym] = value end @@ -104,6 +120,8 @@ def store(key, value) # context.fetch(:name) # => "John" # context.fetch(:age, 25) # => 25 # context.fetch(:city) { |key| "Unknown #{key}" } # => "Unknown city" + # + # @rbs ((String | Symbol) key, *untyped) ?{ ((String | Symbol)) -> untyped } -> untyped def fetch(key, ...) table.fetch(key.to_sym, ...) end @@ -122,6 +140,8 @@ def fetch(key, ...) # context.fetch_or_store(:name, "Default") # => "John" (existing value) # context.fetch_or_store(:age, 25) # => 25 (stored and returned) # context.fetch_or_store(:city) { |key| "Unknown #{key}" } # => "Unknown city" (stored and returned) + # + # @rbs ((String | Symbol) key, ?untyped value) ?{ () -> untyped } -> untyped def fetch_or_store(key, value = nil) table.fetch(key.to_sym) do table[key.to_sym] = block_given? ? yield : value @@ -139,6 +159,8 @@ def fetch_or_store(key, value = nil) # context = Context.new(name: "John") # context.merge!(age: 30, city: "NYC") # context.to_h # => {name: "John", age: 30, city: "NYC"} + # + # @rbs (?untyped args) -> self def merge!(args = {}) args.to_h.each { |key, value| self[key.to_sym] = value } self @@ -156,6 +178,8 @@ def merge!(args = {}) # context = Context.new(name: "John", age: 30) # context.delete!(:age) # => 30 # context.delete!(:city) { |key| "Key #{key} not found" } # => "Key city not found" + # + # @rbs ((String | Symbol) key) ?{ ((String | Symbol)) -> untyped } -> untyped def delete!(key, &) table.delete(key.to_sym, &) end @@ -170,6 +194,8 @@ def delete!(key, &) # context1 = Context.new(name: "John") # context2 = Context.new(name: "John") # context1 == context2 # => true + # + # @rbs (untyped other) -> bool def eql?(other) other.is_a?(self.class) && (to_h == other.to_h) end @@ -185,6 +211,8 @@ def eql?(other) # context = Context.new(name: "John") # context.key?(:name) # => true # context.key?(:age) # => false + # + # @rbs ((String | Symbol) key) -> bool def key?(key) table.key?(key.to_sym) end @@ -200,6 +228,8 @@ def key?(key) # context = Context.new(user: {profile: {name: "John"}}) # context.dig(:user, :profile, :name) # => "John" # context.dig(:user, :profile, :age) # => nil + # + # @rbs ((String | Symbol) key, *(String | Symbol) keys) -> untyped def dig(key, *keys) table.dig(key.to_sym, *keys) end @@ -211,6 +241,8 @@ def dig(key, *keys) # @example # context = Context.new(name: "John", age: 30) # context.to_s # => "name: John, age: 30" + # + # @rbs () -> String def to_s Utils::Format.to_str(to_h) end @@ -227,6 +259,8 @@ def to_s # @yield [Object] optional block # # @return [Object] the result of the method call + # + # @rbs (Symbol method_name, *untyped args, **untyped _kwargs) ?{ () -> untyped } -> untyped def method_missing(method_name, *args, **_kwargs, &) fetch(method_name) do store(method_name[0..-2], args.first) if method_name.end_with?("=") @@ -244,6 +278,8 @@ def method_missing(method_name, *args, **_kwargs, &) # context = Context.new(name: "John") # context.respond_to?(:name) # => true # context.respond_to?(:age) # => false + # + # @rbs (Symbol method_name, ?bool include_private) -> bool def respond_to_missing?(method_name, include_private = false) key?(method_name) || super end diff --git a/lib/cmdx/deprecator.rb b/lib/cmdx/deprecator.rb index c5a26f36e..85619fec7 100644 --- a/lib/cmdx/deprecator.rb +++ b/lib/cmdx/deprecator.rb @@ -10,6 +10,7 @@ module Deprecator extend self + # @rbs EVAL: Proc EVAL = proc do |target, callable| case callable when /raise|log|warn/ then callable @@ -45,6 +46,8 @@ module Deprecator # end # # MyTask.new # => [MyTask] DEPRECATED: migrate to a replacement or discontinue use + # + # @rbs (Task task) -> void def restrict(task) type = EVAL.call(task, task.class.settings[:deprecate]) diff --git a/lib/cmdx/errors.rb b/lib/cmdx/errors.rb index e716b5541..213e5b36f 100644 --- a/lib/cmdx/errors.rb +++ b/lib/cmdx/errors.rb @@ -8,11 +8,21 @@ class Errors extend Forwardable + # Returns the internal hash of error messages by attribute. + # + # @return [Hash{Symbol => Set}] Hash mapping attribute names to error message sets + # + # @example + # errors.messages # => { email: # } + # + # @rbs @messages: Hash[Symbol, Set[String]] attr_reader :messages def_delegators :messages, :empty? # Initialize a new error collection. + # + # @rbs () -> void def initialize @messages = {} end @@ -26,6 +36,8 @@ def initialize # errors = CMDx::Errors.new # errors.add(:email, "must be valid format") # errors.add(:email, "cannot be blank") + # + # @rbs (Symbol attribute, String message) -> void def add(attribute, message) return if message.empty? @@ -42,6 +54,8 @@ def add(attribute, message) # @example # errors.for?(:email) # => true # errors.for?(:name) # => false + # + # @rbs (Symbol attribute) -> bool def for?(attribute) return false unless messages.key?(attribute) @@ -54,6 +68,8 @@ def for?(attribute) # # @example # errors.full_messages # => { email: ["email must be valid format", "email cannot be blank"] } + # + # @rbs () -> Hash[Symbol, Array[String]] def full_messages messages.each_with_object({}) do |(attribute, messages), hash| hash[attribute] = messages.map { |message| "#{attribute} #{message}" } @@ -66,6 +82,8 @@ def full_messages # # @example # errors.to_h # => { email: ["must be valid format", "cannot be blank"] } + # + # @rbs () -> Hash[Symbol, Array[String]] def to_h messages.transform_values(&:to_a) end @@ -78,6 +96,8 @@ def to_h # @example # errors.to_hash # => { email: ["must be valid format", "cannot be blank"] } # errors.to_hash(true) # => { email: ["email must be valid format", "email cannot be blank"] } + # + # @rbs (?bool full) -> Hash[Symbol, Array[String]] def to_hash(full = false) full ? full_messages : to_h end @@ -88,6 +108,8 @@ def to_hash(full = false) # # @example # errors.to_s # => "email must be valid format. email cannot be blank" + # + # @rbs () -> String def to_s full_messages.values.flatten.join(". ") end diff --git a/lib/cmdx/executor.rb b/lib/cmdx/executor.rb index 1d9c867cb..bf90aaee4 100644 --- a/lib/cmdx/executor.rb +++ b/lib/cmdx/executor.rb @@ -8,6 +8,14 @@ module CMDx # and proper error handling for different types of failures. class Executor + # Returns the task being executed. + # + # @return [Task] The task instance + # + # @example + # executor.task.id # => "abc123" + # + # @rbs @task: Task attr_reader :task # @param task [CMDx::Task] The task to execute @@ -16,6 +24,8 @@ class Executor # # @example # executor = CMDx::Executor.new(my_task) + # + # @rbs (Task task) -> void def initialize(task) @task = task end @@ -32,6 +42,8 @@ def initialize(task) # @example # CMDx::Executor.execute(my_task) # CMDx::Executor.execute(my_task, raise: true) + # + # @rbs (Task task, raise: bool) -> Result def self.execute(task, raise: false) instance = new(task) raise ? instance.execute! : instance.execute @@ -44,6 +56,8 @@ def self.execute(task, raise: false) # @example # executor = CMDx::Executor.new(my_task) # result = executor.execute + # + # @rbs () -> Result def execute task.class.settings[:middlewares].call!(task) do pre_execution! unless @pre_execution @@ -73,6 +87,8 @@ def execute # @example # executor = CMDx::Executor.new(my_task) # result = executor.execute! + # + # @rbs () -> Result def execute! task.class.settings[:middlewares].call!(task) do pre_execution! unless @pre_execution @@ -104,6 +120,8 @@ def execute! # # @example # halt_execution?(fault_exception) + # + # @rbs (Exception exception) -> bool def halt_execution?(exception) breakpoints = task.class.settings[:breakpoints] || task.class.settings[:task_breakpoints] breakpoints = Array(breakpoints).map(&:to_s).uniq @@ -119,6 +137,8 @@ def halt_execution?(exception) # # @example # retry_execution?(standard_error) + # + # @rbs (Exception exception) -> bool def retry_execution?(exception) available_retries = (task.class.settings[:retries] || 0).to_i return false unless available_retries.positive? @@ -151,6 +171,8 @@ def retry_execution?(exception) # # @example # raise_exception(standard_error) + # + # @rbs (Exception exception) -> void def raise_exception(exception) Chain.clear @@ -165,6 +187,8 @@ def raise_exception(exception) # # @example # invoke_callbacks(:before_execution) + # + # @rbs (Symbol type) -> void def invoke_callbacks(type) task.class.settings[:callbacks].invoke(type, task) end @@ -172,11 +196,15 @@ def invoke_callbacks(type) private # Lazy loaded repeator instance to handle retries. + # + # @rbs () -> untyped def repeator @repeator ||= Repeator.new(task) end # Performs pre-execution tasks including validation and attribute verification. + # + # @rbs () -> void def pre_execution! @pre_execution = true @@ -195,6 +223,8 @@ def pre_execution! end # Executes the main task logic. + # + # @rbs () -> void def execution! invoke_callbacks(:before_execution) @@ -203,6 +233,8 @@ def execution! end # Performs post-execution tasks including callback invocation. + # + # @rbs () -> void def post_execution! invoke_callbacks(:"on_#{task.result.state}") invoke_callbacks(:on_executed) if task.result.executed? @@ -213,6 +245,8 @@ def post_execution! end # Finalizes execution by freezing the task and logging results. + # + # @rbs () -> Result def finalize_execution! log_execution! log_backtrace! if task.class.settings[:backtrace] @@ -222,11 +256,15 @@ def finalize_execution! end # Logs the execution result at the configured log level. + # + # @rbs () -> void def log_execution! task.logger.info { task.result.to_h } end # Logs the backtrace of the exception if the task failed. + # + # @rbs () -> void def log_backtrace! return unless task.result.failed? @@ -244,6 +282,8 @@ def log_backtrace! end # Freezes the task and its associated objects to prevent modifications. + # + # @rbs () -> void def freeze_execution! # Stubbing on frozen objects is not allowed in most test environments. skip_freezing = ENV.fetch("SKIP_CMDX_FREEZING", false) @@ -260,6 +300,9 @@ def freeze_execution! task.chain.freeze end + # Clears the chain if the task is the outermost (top-level) task. + # + # @rbs () -> void def clear_chain! return unless task.result.index.zero? diff --git a/lib/cmdx/faults.rb b/lib/cmdx/faults.rb index 7a15c98e6..d43191754 100644 --- a/lib/cmdx/faults.rb +++ b/lib/cmdx/faults.rb @@ -11,6 +11,14 @@ class Fault < Error extend Forwardable + # Returns the result that caused this fault. + # + # @return [Result] The result instance + # + # @example + # fault.result.reason # => "Validation failed" + # + # @rbs @result: Result attr_reader :result def_delegators :result, :task, :context, :chain @@ -24,6 +32,8 @@ class Fault < Error # @example # fault = Fault.new(task_result) # fault.result.reason # => "Task validation failed" + # + # @rbs (Result result) -> void def initialize(result) @result = result @@ -41,6 +51,8 @@ class << self # @example # Fault.for?(UserTask, AdminUserTask) # # => true if fault.task is a UserTask or AdminUserTask + # + # @rbs (*Class tasks) -> Class def for?(*tasks) temp_fault = Class.new(self) do def self.===(other) @@ -62,6 +74,8 @@ def self.===(other) # @example # Fault.matches? { |fault| fault.result.metadata[:critical] } # # => true if fault has critical metadata + # + # @rbs () { (Fault) -> bool } -> Class def matches?(&block) raise ArgumentError, "block required" unless block_given? diff --git a/lib/cmdx/identifier.rb b/lib/cmdx/identifier.rb index e989eccce..743d20381 100644 --- a/lib/cmdx/identifier.rb +++ b/lib/cmdx/identifier.rb @@ -18,6 +18,8 @@ module Identifier # @example Generate a unique identifier # CMDx::Identifier.generate # # => "01890b2c-1234-5678-9abc-def123456789" + # + # @rbs () -> String def generate if SecureRandom.respond_to?(:uuid_v7) SecureRandom.uuid_v7 diff --git a/lib/cmdx/locale.rb b/lib/cmdx/locale.rb index 816c55c99..9deed1792 100644 --- a/lib/cmdx/locale.rb +++ b/lib/cmdx/locale.rb @@ -8,6 +8,7 @@ module Locale extend self + # @rbs EN: Hash[String, untyped] EN = YAML.load_file(CMDx.gem_path.join("lib/locales/en.yml")).freeze private_constant :EN @@ -34,6 +35,8 @@ module Locale # @example With fallback # Locale.translate("missing.key", default: "Custom fallback message") # # => "Custom fallback message" + # + # @rbs ((String | Symbol) key, **untyped options) -> String def translate(key, **options) options[:default] ||= EN.dig("en", *key.to_s.split(".")) return ::I18n.t(key, **options) if defined?(::I18n) diff --git a/lib/cmdx/log_formatters/json.rb b/lib/cmdx/log_formatters/json.rb index d5da9cd56..3a4b60672 100644 --- a/lib/cmdx/log_formatters/json.rb +++ b/lib/cmdx/log_formatters/json.rb @@ -21,6 +21,8 @@ class JSON # @example Basic usage # logger_formatter.call("INFO", Time.now, "MyApp", "User logged in") # # => '{"severity":"INFO","timestamp":"2024-01-15T10:30:45.123456Z","progname":"MyApp","pid":12345,"message":"User logged in"}\n' + # + # @rbs (String severity, Time time, String? progname, String message) -> String def call(severity, time, progname, message) hash = { severity:, diff --git a/lib/cmdx/log_formatters/key_value.rb b/lib/cmdx/log_formatters/key_value.rb index eec631b08..87e0f4087 100644 --- a/lib/cmdx/log_formatters/key_value.rb +++ b/lib/cmdx/log_formatters/key_value.rb @@ -21,6 +21,8 @@ class KeyValue # @example Basic usage # logger_formatter.call("INFO", Time.now, "MyApp", "User logged in") # # => "severity=INFO timestamp=2024-01-15T10:30:45.123456Z progname=MyApp pid=12345 message=User logged in\n" + # + # @rbs (String severity, Time time, String? progname, String message) -> String def call(severity, time, progname, message) hash = { severity:, diff --git a/lib/cmdx/log_formatters/line.rb b/lib/cmdx/log_formatters/line.rb index eb61f9d54..a58b32ad4 100644 --- a/lib/cmdx/log_formatters/line.rb +++ b/lib/cmdx/log_formatters/line.rb @@ -21,6 +21,8 @@ class Line # @example Basic usage # logger_formatter.call("INFO", Time.now, "MyApp", "User logged in") # # => "I, [2024-01-15T10:30:45.123456Z #12345] INFO -- MyApp: User logged in\n" + # + # @rbs (String severity, Time time, String? progname, String message) -> String def call(severity, time, progname, message) "#{severity[0]}, [#{time.utc.iso8601(6)} ##{Process.pid}] #{severity} -- #{progname}: #{message}\n" end diff --git a/lib/cmdx/log_formatters/logstash.rb b/lib/cmdx/log_formatters/logstash.rb index 79408eed7..4df2b9082 100644 --- a/lib/cmdx/log_formatters/logstash.rb +++ b/lib/cmdx/log_formatters/logstash.rb @@ -22,6 +22,8 @@ class Logstash # @example Basic usage # logger_formatter.call("INFO", Time.now, "MyApp", "User logged in") # # => '{"severity":"INFO","progname":"MyApp","pid":12345,"message":"User logged in","@version":"1","@timestamp":"2024-01-15T10:30:45.123456Z"}\n' + # + # @rbs (String severity, Time time, String? progname, String message) -> String def call(severity, time, progname, message) hash = { severity:, diff --git a/lib/cmdx/log_formatters/raw.rb b/lib/cmdx/log_formatters/raw.rb index 673aaa40f..57a43fcdc 100644 --- a/lib/cmdx/log_formatters/raw.rb +++ b/lib/cmdx/log_formatters/raw.rb @@ -22,6 +22,8 @@ class Raw # @example Basic usage # logger_formatter.call("INFO", Time.now, "MyApp", "User logged in") # # => "User logged in\n" + # + # @rbs (String severity, Time time, String? progname, String message) -> String def call(severity, time, progname, message) "#{message}\n" end diff --git a/lib/cmdx/middleware_registry.rb b/lib/cmdx/middleware_registry.rb index 3713609b4..4f3845193 100644 --- a/lib/cmdx/middleware_registry.rb +++ b/lib/cmdx/middleware_registry.rb @@ -9,6 +9,14 @@ module CMDx # they were registered. class MiddlewareRegistry + # Returns the ordered collection of middleware entries. + # + # @return [Array] Array of middleware-options pairs + # + # @example + # registry.registry # => [[LoggingMiddleware, {level: :debug}], [AuthMiddleware, {}]] + # + # @rbs @registry: Array[Array[untyped]] attr_reader :registry alias to_a registry @@ -19,6 +27,8 @@ class MiddlewareRegistry # @example # registry = MiddlewareRegistry.new # registry = MiddlewareRegistry.new([[MyMiddleware, {option: 'value'}]]) + # + # @rbs (?Array[Array[untyped]] registry) -> void def initialize(registry = []) @registry = registry end @@ -29,6 +39,8 @@ def initialize(registry = []) # # @example # new_registry = registry.dup + # + # @rbs () -> MiddlewareRegistry def dup self.class.new(registry.map(&:dup)) end @@ -46,6 +58,8 @@ def dup # @example # registry.register(LoggingMiddleware, at: 0, log_level: :debug) # registry.register(AuthMiddleware, at: -1, timeout: 30) + # + # @rbs (untyped middleware, ?at: Integer, **untyped options) -> self def register(middleware, at: -1, **options) registry.insert(at, [middleware, options]) self @@ -59,6 +73,8 @@ def register(middleware, at: -1, **options) # # @example # registry.deregister(LoggingMiddleware) + # + # @rbs (untyped middleware) -> self def deregister(middleware) registry.reject! { |mw, _opts| mw == middleware } self @@ -79,6 +95,8 @@ def deregister(middleware) # result = registry.call!(my_task) do |processed_task| # processed_task.execute # end + # + # @rbs (untyped task) { (untyped) -> untyped } -> untyped def call!(task, &) raise ArgumentError, "block required" unless block_given? @@ -96,6 +114,8 @@ def call!(task, &) # @yieldparam task [Object] The processed task object # # @return [Object] Result of the block execution or next middleware call + # + # @rbs (Integer index, untyped task) { (untyped) -> untyped } -> untyped def recursively_call_middleware(index, task, &block) return yield(task) if index >= registry.size diff --git a/lib/cmdx/middlewares/correlate.rb b/lib/cmdx/middlewares/correlate.rb index d304e4ddb..f9d46762b 100644 --- a/lib/cmdx/middlewares/correlate.rb +++ b/lib/cmdx/middlewares/correlate.rb @@ -12,6 +12,7 @@ module Correlate extend self + # @rbs THREAD_KEY: Symbol THREAD_KEY = :cmdx_correlate # Retrieves the current correlation ID from thread-local storage. @@ -20,6 +21,8 @@ module Correlate # # @example Get current correlation ID # Correlate.id # => "550e8400-e29b-41d4-a716-446655440000" + # + # @rbs () -> String? def id Thread.current[THREAD_KEY] end @@ -31,6 +34,8 @@ def id # # @example Set correlation ID # Correlate.id = "abc-123-def" + # + # @rbs (String id) -> String def id=(id) Thread.current[THREAD_KEY] = id end @@ -41,6 +46,8 @@ def id=(id) # # @example Clear correlation ID # Correlate.clear + # + # @rbs () -> nil def clear Thread.current[THREAD_KEY] = nil end @@ -58,6 +65,8 @@ def clear # perform_operation # end # # Previous ID is restored + # + # @rbs (String new_id) { () -> untyped } -> untyped def use(new_id) old_id = id self.id = new_id @@ -92,6 +101,8 @@ def use(new_id) # Correlate.call(task, id: -> { "dynamic-#{Time.now.to_i}" }, &block) # @example Conditional correlation # Correlate.call(task, if: :enable_correlation, &block) + # + # @rbs (Task task, **untyped options) { () -> untyped } -> untyped def call(task, **options, &) return yield unless Utils::Condition.evaluate(task, options) diff --git a/lib/cmdx/middlewares/runtime.rb b/lib/cmdx/middlewares/runtime.rb index aa9442b5f..f86952ef9 100644 --- a/lib/cmdx/middlewares/runtime.rb +++ b/lib/cmdx/middlewares/runtime.rb @@ -32,6 +32,8 @@ module Runtime # Runtime.call(task, if: :enable_profiling, &block) # @example Disable runtime measurement # Runtime.call(task, unless: :skip_profiling, &block) + # + # @rbs (Task task, **untyped options) { () -> untyped } -> untyped def call(task, **options) return yield unless Utils::Condition.evaluate(task, options) @@ -49,6 +51,8 @@ def call(task, **options) # timing measurements that are not affected by system clock changes. # # @return [Integer] Current monotonic time in milliseconds + # + # @rbs () -> Integer def monotonic_time Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) end diff --git a/lib/cmdx/middlewares/timeout.rb b/lib/cmdx/middlewares/timeout.rb index 877f91eba..02fd2dafe 100644 --- a/lib/cmdx/middlewares/timeout.rb +++ b/lib/cmdx/middlewares/timeout.rb @@ -21,6 +21,8 @@ module Timeout extend self # Default timeout limit in seconds when none is specified. + # + # @rbs DEFAULT_LIMIT: Integer DEFAULT_LIMIT = 3 # Middleware entry point that enforces execution time limits. @@ -51,6 +53,8 @@ module Timeout # Timeout.call(task, seconds: -> { calculate_timeout }, &block) # @example Conditional timeout control # Timeout.call(task, if: :enable_timeout, &block) + # + # @rbs (Task task, **untyped options) { () -> untyped } -> untyped def call(task, **options, &) return yield unless Utils::Condition.evaluate(task, options) diff --git a/lib/cmdx/pipeline.rb b/lib/cmdx/pipeline.rb index f78d5a234..3ce8a28ac 100644 --- a/lib/cmdx/pipeline.rb +++ b/lib/cmdx/pipeline.rb @@ -6,7 +6,14 @@ module CMDx # and handling breakpoints that can interrupt execution at specific task statuses. class Pipeline - # @return [Workflow] The workflow instance being executed + # Returns the workflow being executed by this pipeline. + # + # @return [Workflow] The workflow instance + # + # @example + # pipeline.workflow.context[:status] # => "processing" + # + # @rbs @workflow: Workflow attr_reader :workflow # @param workflow [Workflow] The workflow to execute @@ -15,6 +22,8 @@ class Pipeline # # @example # pipeline = Pipeline.new(my_workflow) + # + # @rbs (Workflow workflow) -> void def initialize(workflow) @workflow = workflow end @@ -27,6 +36,8 @@ def initialize(workflow) # # @example # Pipeline.execute(my_workflow) + # + # @rbs (Workflow workflow) -> void def self.execute(workflow) new(workflow).execute end @@ -40,6 +51,8 @@ def self.execute(workflow) # @example # pipeline = Pipeline.new(my_workflow) # pipeline.execute + # + # @rbs () -> void def execute workflow.class.pipeline.each do |group| next unless Utils::Condition.evaluate(workflow, group.options) @@ -65,6 +78,8 @@ def execute # # @example # execute_group_tasks(group, ["failed", "skipped"]) + # + # @rbs (untyped group, Array[String] breakpoints) -> void def execute_group_tasks(group, breakpoints) case strategy = group.options[:strategy] when NilClass, /sequential/ then execute_tasks_in_sequence(group, breakpoints) @@ -85,6 +100,8 @@ def execute_group_tasks(group, breakpoints) # # @example # execute_tasks_in_sequence(group, ["failed", "skipped"]) + # + # @rbs (untyped group, Array[String] breakpoints) -> void def execute_tasks_in_sequence(group, breakpoints) group.tasks.each do |task| task_result = task.execute(workflow.context) @@ -107,6 +124,8 @@ def execute_tasks_in_sequence(group, breakpoints) # # @example # execute_tasks_in_parallel(group, ["failed"]) + # + # @rbs (untyped group, Array[String] breakpoints) -> void def execute_tasks_in_parallel(group, breakpoints) raise "install the `parallel` gem to use this feature" unless defined?(Parallel) diff --git a/lib/cmdx/railtie.rb b/lib/cmdx/railtie.rb index b2047fa54..a33fe9ab8 100644 --- a/lib/cmdx/railtie.rb +++ b/lib/cmdx/railtie.rb @@ -21,6 +21,8 @@ class Railtie < Rails::Railtie # # This initializer runs automatically when Rails starts # # It will load locales like en.yml, es.yml, fr.yml if they exist # # in the CMDx gem's locales directory + # + # @rbs (untyped app) -> void initializer("cmdx.configure_locales") do |app| Array(app.config.i18n.available_locales).each do |locale| path = CMDx.gem_path.join("lib/locales/#{locale}.yml") @@ -35,6 +37,8 @@ class Railtie < Rails::Railtie # Configures the backtrace cleaner for CMDx in a Rails environment. # # Sets the backtrace cleaner to the Rails backtrace cleaner. + # + # @rbs () -> void initializer("cmdx.backtrace_cleaner") do CMDx.configuration.backtrace_cleaner = lambda do |backtrace| Rails.backtrace_cleaner.clean(backtrace) diff --git a/lib/cmdx/result.rb b/lib/cmdx/result.rb index 0cf9ec5c3..baffdf4fc 100644 --- a/lib/cmdx/result.rb +++ b/lib/cmdx/result.rb @@ -11,18 +11,22 @@ class Result extend Forwardable + # @rbs STATES: Array[String] STATES = [ INITIALIZED = "initialized", # Initial state before execution EXECUTING = "executing", # Currently executing task logic COMPLETE = "complete", # Successfully completed execution INTERRUPTED = "interrupted" # Execution was halted due to failure ].freeze + + # @rbs STATUSES: Array[String] STATUSES = [ SUCCESS = "success", # Task completed successfully SKIPPED = "skipped", # Task was skipped intentionally FAILED = "failed" # Task failed due to error or validation ].freeze + # @rbs STRIP_FAILURE: Proc STRIP_FAILURE = proc do |hash, result, key| unless result.send(:"#{key}?") # Strip caused/threw failures since its the same info as the log line @@ -31,7 +35,65 @@ class Result end.freeze private_constant :STRIP_FAILURE - attr_reader :task, :state, :status, :metadata, :reason, :cause + # Returns the task instance associated with this result. + # + # @return [CMDx::Task] The task instance + # + # @example + # result.task.id # => "users/create" + # + # @rbs @task: Task + attr_reader :task + + # Returns the current execution state of the result. + # + # @return [String] One of: "initialized", "executing", "complete", "interrupted" + # + # @example + # result.state # => "complete" + # + # @rbs @state: String + attr_reader :state + + # Returns the execution status of the result. + # + # @return [String] One of: "success", "skipped", "failed" + # + # @example + # result.status # => "success" + # + # @rbs @status: String + attr_reader :status + + # Returns additional metadata about the result. + # + # @return [Hash{Symbol => Object}] Metadata hash + # + # @example + # result.metadata # => { duration: 1.5, retries: 2 } + # + # @rbs @metadata: Hash[Symbol, untyped] + attr_reader :metadata + + # Returns the reason for interruption (skip or failure). + # + # @return [String, nil] The reason message, or nil if not interrupted + # + # @example + # result.reason # => "Validation failed" + # + # @rbs @reason: (String | nil) + attr_reader :reason + + # Returns the exception that caused the interruption. + # + # @return [Exception, nil] The causing exception, or nil if not interrupted + # + # @example + # result.cause # => # + # + # @rbs @cause: (Exception | nil) + attr_reader :cause def_delegators :task, :context, :chain, :errors alias ctx context @@ -45,6 +107,8 @@ class Result # @example # result = CMDx::Result.new(my_task) # result.state # => "initialized" + # + # @rbs (Task) -> void def initialize(task) raise TypeError, "must be a CMDx::Task" unless task.is_a?(CMDx::Task) @@ -62,6 +126,8 @@ def initialize(task) # @example # result.initialized? # => true # result.executing? # => false + # + # @rbs () -> bool define_method(:"#{s}?") { state == s } # @param block [Proc] Block to execute conditionally @@ -75,6 +141,8 @@ def initialize(task) # @example # result.handle_initialized { |r| puts "Starting execution" } # result.handle_complete { |r| puts "Task completed" } + # + # @rbs () { (Result) -> void } -> self define_method(:"handle_#{s}") do |&block| raise ArgumentError, "block required" unless block @@ -87,6 +155,8 @@ def initialize(task) # # @example # result.executed! # Transitions to complete or interrupted + # + # @rbs () -> self def executed! success? ? complete! : interrupt! end @@ -95,6 +165,8 @@ def executed! # # @example # result.executed? # => true if complete? || interrupted? + # + # @rbs () -> bool def executed? complete? || interrupted? end @@ -109,6 +181,8 @@ def executed? # # @example # result.handle_executed { |r| puts "Task finished: #{r.outcome}" } + # + # @rbs () { (Result) -> void } -> self def handle_executed(&) raise ArgumentError, "block required" unless block_given? @@ -120,6 +194,8 @@ def handle_executed(&) # # @example # result.executing! # Transitions from initialized to executing + # + # @rbs () -> void def executing! return if executing? @@ -132,6 +208,8 @@ def executing! # # @example # result.complete! # Transitions from executing to complete + # + # @rbs () -> void def complete! return if complete? @@ -144,6 +222,8 @@ def complete! # # @example # result.interrupt! # Transitions from executing to interrupted + # + # @rbs () -> void def interrupt! return if interrupted? @@ -158,6 +238,8 @@ def interrupt! # @example # result.success? # => true # result.failed? # => false + # + # @rbs () -> bool define_method(:"#{s}?") { status == s } # @param block [Proc] Block to execute conditionally @@ -171,6 +253,8 @@ def interrupt! # @example # result.handle_success { |r| puts "Task succeeded" } # result.handle_failed { |r| puts "Task failed: #{r.reason}" } + # + # @rbs () { (Result) -> void } -> self define_method(:"handle_#{s}") do |&block| raise ArgumentError, "block required" unless block @@ -183,6 +267,8 @@ def interrupt! # # @example # result.good? # => true if !failed? + # + # @rbs () -> bool def good? !failed? end @@ -198,6 +284,8 @@ def good? # # @example # result.handle_good { |r| puts "Task completed successfully" } + # + # @rbs () { (Result) -> void } -> self def handle_good(&) raise ArgumentError, "block required" unless block_given? @@ -209,6 +297,8 @@ def handle_good(&) # # @example # result.bad? # => true if !success? + # + # @rbs () -> bool def bad? !success? end @@ -223,6 +313,8 @@ def bad? # # @example # result.handle_bad { |r| puts "Task had issues: #{r.reason}" } + # + # @rbs () { (Result) -> void } -> self def handle_bad(&) raise ArgumentError, "block required" unless block_given? @@ -240,6 +332,8 @@ def handle_bad(&) # @example # result.skip!("Dependencies not met", cause: dependency_error) # result.skip!("Already processed", halt: false) + # + # @rbs (?String? reason, halt: bool, cause: Exception?, **untyped metadata) -> void def skip!(reason = nil, halt: true, cause: nil, **metadata) return if skipped? @@ -264,6 +358,8 @@ def skip!(reason = nil, halt: true, cause: nil, **metadata) # @example # result.fail!("Validation failed", cause: validation_error) # result.fail!("Network timeout", halt: false, timeout: 30) + # + # @rbs (?String? reason, halt: bool, cause: Exception?, **untyped metadata) -> void def fail!(reason = nil, halt: true, cause: nil, **metadata) return if failed? @@ -283,6 +379,8 @@ def fail!(reason = nil, halt: true, cause: nil, **metadata) # # @example # result.halt! # Raises appropriate fault based on status + # + # @rbs () -> void def halt! return if success? @@ -315,6 +413,8 @@ def halt! # @example # other_result = OtherTask.execute # result.throw!(other_result, cause: upstream_error) + # + # @rbs (Result result, halt: bool, cause: Exception?, **untyped metadata) -> void def throw!(result, halt: true, cause: nil, **metadata) raise TypeError, "must be a CMDx::Result" unless result.is_a?(Result) @@ -332,6 +432,8 @@ def throw!(result, halt: true, cause: nil, **metadata) # @example # cause = result.caused_failure # puts "Caused by: #{cause.task.id}" if cause + # + # @rbs () -> Result? def caused_failure return unless failed? @@ -344,6 +446,8 @@ def caused_failure # if result.caused_failure? # puts "This task caused the failure" # end + # + # @rbs () -> bool def caused_failure? return false unless failed? @@ -355,6 +459,8 @@ def caused_failure? # @example # thrown = result.threw_failure # puts "Thrown by: #{thrown.task.id}" if thrown + # + # @rbs () -> Result? def threw_failure return unless failed? @@ -369,6 +475,8 @@ def threw_failure # if result.threw_failure? # puts "This task threw the failure" # end + # + # @rbs () -> bool def threw_failure? return false unless failed? @@ -381,6 +489,8 @@ def threw_failure? # if result.thrown_failure? # puts "This failure was thrown from another task" # end + # + # @rbs () -> bool def thrown_failure? failed? && !caused_failure? end @@ -390,6 +500,8 @@ def thrown_failure? # @example # position = result.index # puts "Task #{position + 1} of #{chain.results.count}" + # + # @rbs () -> Integer def index chain.index(self) end @@ -398,6 +510,8 @@ def index # # @example # result.outcome # => "success" or "interrupted" + # + # @rbs () -> String def outcome initialized? || thrown_failure? ? state : status end @@ -407,6 +521,8 @@ def outcome # @example # result.to_h # # => {state: "complete", status: "success", outcome: "success", metadata: {}} + # + # @rbs () -> Hash[Symbol, untyped] def to_h task.to_h.merge!( state:, @@ -430,6 +546,8 @@ def to_h # # @example # result.to_s # => "task_id=my_task state=complete status=success" + # + # @rbs () -> String def to_s Utils::Format.to_str(to_h) do |key, value| case key @@ -446,6 +564,8 @@ def to_s # @example # state, status = result.deconstruct # puts "State: #{state}, Status: #{status}" + # + # @rbs (*untyped) -> Array[untyped] def deconstruct(*) [state, status, reason, cause, metadata] end @@ -461,6 +581,8 @@ def deconstruct(*) # in {bad: true} # puts "Task had issues" # end + # + # @rbs (*untyped) -> Hash[Symbol, untyped] def deconstruct_keys(*) { state: state, diff --git a/lib/cmdx/task.rb b/lib/cmdx/task.rb index 7f87c8d5f..4a22fbdc0 100644 --- a/lib/cmdx/task.rb +++ b/lib/cmdx/task.rb @@ -8,10 +8,68 @@ class Task extend Forwardable - attr_reader :attributes, :errors, :id, :context, :result, :chain + # Returns the hash of processed attribute values for this task. + # + # @return [Hash{Symbol => Object}] Hash of attribute names to their values + # + # @example + # task.attributes # => { user_id: 42, user_name: "John" } + # + # @rbs @attributes: Hash[Symbol, untyped] + attr_reader :attributes + + # Returns the collection of validation and execution errors. + # + # @return [Errors] The errors collection + # + # @example + # task.errors.to_h # => { email: ["must be valid"] } + # + # @rbs @errors: Errors + attr_reader :errors + + # Returns the unique identifier for this task instance. + # + # @return [String] The task identifier + # + # @example + # task.id # => "abc123xyz" + # + # @rbs @id: String + attr_reader :id + + # Returns the execution context for this task. + # + # @return [Context] The context instance + # + # @example + # task.context[:user_id] # => 42 + # + # @rbs @context: Context + attr_reader :context alias ctx context + + # Returns the execution result for this task. + # + # @return [Result] The result instance + # + # @example + # task.result.status # => "success" + # + # @rbs @result: Result + attr_reader :result alias res result + # Returns the execution chain containing all task results. + # + # @return [Chain] The chain instance + # + # @example + # task.chain.results.size # => 3 + # + # @rbs @chain: Chain + attr_reader :chain + def_delegators :result, :skip!, :fail!, :throw! # @param context [Hash, Context] The initial context for the task @@ -25,6 +83,8 @@ class Task # @example # task = MyTask.new(name: "example", priority: :high) # task = MyTask.new(Context.build(name: "example")) + # + # @rbs (untyped context) -> void def initialize(context = {}) Deprecator.restrict(self) @@ -47,6 +107,8 @@ class << self # class MyTask < Task # settings deprecate: true, tags: [:experimental] # end + # + # @rbs (**untyped options) -> Hash[Symbol, untyped] def settings(**options) @settings ||= begin hash = @@ -81,6 +143,8 @@ def settings(**options) # @example # register(:attribute, MyAttribute.new) # register(:callback, :before, -> { puts "before" }) + # + # @rbs (Symbol type, untyped object, *untyped) -> void def register(type, object, ...) case type when :attribute then settings[:attributes].register(object, ...) @@ -101,6 +165,8 @@ def register(type, object, ...) # @example # deregister(:attribute, :name) # deregister(:callback, :before, MyCallback) + # + # @rbs (Symbol type, untyped object, *untyped) -> void def deregister(type, object, ...) case type when :attribute then settings[:attributes].deregister(object, ...) @@ -117,6 +183,8 @@ def deregister(type, object, ...) # @example # attributes :name, :email # attributes :age, type: Integer, default: 18 + # + # @rbs (*untyped) -> void def attributes(...) register(:attribute, Attribute.build(...)) end @@ -127,6 +195,8 @@ def attributes(...) # @example # optional :description, :notes # optional :priority, type: Symbol, default: :normal + # + # @rbs (*untyped) -> void def optional(...) register(:attribute, Attribute.optional(...)) end @@ -136,6 +206,8 @@ def optional(...) # @example # required :name, :email # required :age, type: Integer, min: 0 + # + # @rbs (*untyped) -> void def required(...) register(:attribute, Attribute.required(...)) end @@ -144,6 +216,8 @@ def required(...) # # @example # remove_attributes :old_field, :deprecated_field + # + # @rbs (*Symbol names) -> void def remove_attributes(*names) deregister(:attribute, names) end @@ -160,6 +234,8 @@ def remove_attributes(*names) # before { puts "before execution" } # after :cleanup, priority: :high # around ->(task) { task.logger.info("starting") } + # + # @rbs (*untyped callables, **untyped options) ?{ () -> void } -> void define_method(callback) do |*callables, **options, &block| register(:callback, callback, *callables, **options, &block) end @@ -174,6 +250,8 @@ def remove_attributes(*names) # if result.success? # puts "Task completed successfully" # end + # + # @rbs (*untyped args, **untyped kwargs) ?{ (Result) -> void } -> Result def execute(*args, **kwargs) task = new(*args, **kwargs) task.execute(raise: false) @@ -189,6 +267,8 @@ def execute(*args, **kwargs) # @example # result = MyTask.execute!(name: "example") # # Will raise an exception if execution fails + # + # @rbs (*untyped args, **untyped kwargs) ?{ (Result) -> void } -> Result def execute!(*args, **kwargs) task = new(*args, **kwargs) task.execute(raise: true) @@ -204,6 +284,8 @@ def execute!(*args, **kwargs) # @example # result = task.execute # result = task.execute(raise: true) + # + # @rbs (raise: bool) ?{ (Result) -> void } -> Result def execute(raise: false) Executor.execute(self, raise:) block_given? ? yield(result) : result @@ -218,6 +300,8 @@ def execute(raise: false) # puts "Performing work..." # end # end + # + # @rbs () -> void def work raise UndefinedMethodError, "undefined method #{self.class.name}#work" end @@ -227,6 +311,8 @@ def work # @example # logger.info "Starting task execution" # logger.error "Task failed", error: exception + # + # @rbs () -> Logger def logger @logger ||= begin logger = self.class.settings[:logger] || CMDx.configuration.logger @@ -249,6 +335,8 @@ def logger # task_hash = task.to_h # puts "Task type: #{task_hash[:type]}" # puts "Task tags: #{task_hash[:tags].join(', ')}" + # + # @rbs () -> Hash[Symbol, untyped] def to_h { index: result.index, @@ -265,6 +353,8 @@ def to_h # @example # puts task.to_s # # Output: "Task[MyTask] tags: [:important] id: abc123" + # + # @rbs () -> String def to_s Utils::Format.to_str(to_h) end diff --git a/lib/cmdx/utils/call.rb b/lib/cmdx/utils/call.rb index ae4717861..0baf8f4b2 100644 --- a/lib/cmdx/utils/call.rb +++ b/lib/cmdx/utils/call.rb @@ -32,6 +32,8 @@ module Call # @example Invoking a callable object # callable = MyCallable.new # Call.invoke(user, callable, 'data') + # + # @rbs (untyped target, (Symbol | Proc | untyped) callable, *untyped args, **untyped kwargs) ?{ () -> untyped } -> untyped def invoke(target, callable, *args, **kwargs, &) if callable.is_a?(Symbol) target.send(callable, *args, **kwargs, &) diff --git a/lib/cmdx/utils/condition.rb b/lib/cmdx/utils/condition.rb index 52eb0d53f..161921133 100644 --- a/lib/cmdx/utils/condition.rb +++ b/lib/cmdx/utils/condition.rb @@ -11,6 +11,7 @@ module Condition extend self + # @rbs EVAL: Proc EVAL = proc do |target, callable, *args, **kwargs, &block| case callable when NilClass, FalseClass, TrueClass then !!callable @@ -53,6 +54,8 @@ module Condition # @example With arguments and block # Condition.evaluate(user, if: ->(u) { u.has_permission?(:admin) }, :admin) # # => true if the proc returns true when called with user and :admin + # + # @rbs (untyped target, Hash[Symbol, untyped] options, *untyped) ?{ () -> untyped } -> bool def evaluate(target, options, ...) case options in if: if_cond, unless: unless_cond diff --git a/lib/cmdx/utils/format.rb b/lib/cmdx/utils/format.rb index d88bfb740..81b8d864b 100644 --- a/lib/cmdx/utils/format.rb +++ b/lib/cmdx/utils/format.rb @@ -8,6 +8,7 @@ module Format extend self + # @rbs FORMATTER: Proc FORMATTER = proc do |key, value| "#{key}=#{value.inspect}" end.freeze @@ -28,6 +29,8 @@ module Format # @example CMDx object # Format.to_log(CMDx::Task.new(name: "task1")) # # => {name: "task1"} + # + # @rbs (untyped message) -> untyped def to_log(message) if message.respond_to?(:to_h) && message.class.ancestors.any? { |a| a.to_s.start_with?("CMDx") } message.to_h @@ -51,6 +54,8 @@ def to_log(message) # @example Custom formatter # Format.to_str({count: 5, total: 100}) { |k, v| "#{k}:#{v}" } # # => "count:5 total:100" + # + # @rbs (Hash[untyped, untyped] hash) ?{ (untyped, untyped) -> String } -> String def to_str(hash, &block) block ||= FORMATTER hash.map(&block).join(" ") diff --git a/lib/cmdx/validator_registry.rb b/lib/cmdx/validator_registry.rb index 351448d4d..eb21850a8 100644 --- a/lib/cmdx/validator_registry.rb +++ b/lib/cmdx/validator_registry.rb @@ -7,6 +7,14 @@ class ValidatorRegistry extend Forwardable + # Returns the internal registry mapping validator types to classes. + # + # @return [Hash{Symbol => Class}] Hash of validator type names to validator classes + # + # @example + # registry.registry # => { presence: Validators::Presence, format: Validators::Format } + # + # @rbs @registry: Hash[Symbol, Class] attr_reader :registry alias to_h registry @@ -17,6 +25,8 @@ class ValidatorRegistry # @param registry [Hash, nil] Optional hash mapping validator names to validator classes # # @return [ValidatorRegistry] A new validator registry instance + # + # @rbs (?Hash[Symbol, Class]? registry) -> void def initialize(registry = nil) @registry = registry || { exclusion: Validators::Exclusion, @@ -31,6 +41,8 @@ def initialize(registry = nil) # Create a duplicate of the registry with copied internal state. # # @return [ValidatorRegistry] A new validator registry with duplicated registry hash + # + # @rbs () -> ValidatorRegistry def dup self.class.new(registry.dup) end @@ -45,6 +57,8 @@ def dup # @example # registry.register(:custom, CustomValidator) # registry.register("email", EmailValidator) + # + # @rbs ((String | Symbol) name, Class validator) -> self def register(name, validator) registry[name.to_sym] = validator self @@ -59,6 +73,8 @@ def register(name, validator) # @example # registry.deregister(:format) # registry.deregister("presence") + # + # @rbs ((String | Symbol) name) -> self def deregister(name) registry.delete(name.to_sym) self @@ -77,6 +93,8 @@ def deregister(name) # @example # registry.validate(:presence, task, user.name, presence: true) # registry.validate(:length, task, password, { min: 8, allow_nil: false }) + # + # @rbs (Symbol type, Task task, untyped value, untyped options) -> untyped def validate(type, task, value, options = {}) raise TypeError, "unknown validator type #{type.inspect}" unless registry.key?(type) diff --git a/lib/cmdx/validators/exclusion.rb b/lib/cmdx/validators/exclusion.rb index 6a7b0941a..eb9d67647 100644 --- a/lib/cmdx/validators/exclusion.rb +++ b/lib/cmdx/validators/exclusion.rb @@ -32,6 +32,8 @@ module Exclusion # # => raises ValidationError if value is 5 (within 1..10) # @example Exclude with custom message # Exclusion.call("test", in: ["test", "demo"], message: "value %{values} is forbidden") + # + # @rbs (untyped value, Hash[Symbol, untyped] options) -> nil def call(value, options = {}) values = options[:in] || options[:within] diff --git a/lib/cmdx/validators/format.rb b/lib/cmdx/validators/format.rb index bb19001fa..e3902e337 100644 --- a/lib/cmdx/validators/format.rb +++ b/lib/cmdx/validators/format.rb @@ -38,6 +38,8 @@ module Format # @example Validate with custom message # Format.call("invalid", with: /\A\d+\z/, message: "Must contain only digits") # # => raises ValidationError with custom message + # + # @rbs (untyped value, (Hash[Symbol, untyped] | Regexp) options) -> nil def call(value, options = {}) match = if options.is_a?(Regexp) diff --git a/lib/cmdx/validators/inclusion.rb b/lib/cmdx/validators/inclusion.rb index 4706d6e10..8b81d1629 100644 --- a/lib/cmdx/validators/inclusion.rb +++ b/lib/cmdx/validators/inclusion.rb @@ -34,6 +34,8 @@ module Inclusion # # => nil (validation passes - 5 is within 1..10) # @example Include with custom message # Inclusion.call("test", in: ["admin", "user"], message: "must be one of: %{values}") + # + # @rbs (untyped value, Hash[Symbol, untyped] options) -> nil def call(value, options = {}) values = options[:in] || options[:within] diff --git a/lib/cmdx/validators/length.rb b/lib/cmdx/validators/length.rb index d290b2efc..3fd9f438c 100644 --- a/lib/cmdx/validators/length.rb +++ b/lib/cmdx/validators/length.rb @@ -51,6 +51,8 @@ module Length # @example Exclusion validation # Length.call("short", not_in: 1..3) # # => nil (validation passes - length 5 is not in excluded range) + # + # @rbs (untyped value, Hash[Symbol, untyped] options) -> nil def call(value, options = {}) length = value&.length @@ -87,6 +89,8 @@ def call(value, options = {}) # @param options [Hash] Validation options containing custom messages # # @raise [ValidationError] Always raised with appropriate message + # + # @rbs (Integer min, Integer max, Hash[Symbol, untyped] options) -> void def raise_within_validation_error!(min, max, options) message = options[:within_message] || options[:in_message] || options[:message] message %= { min:, max: } unless message.nil? @@ -101,6 +105,8 @@ def raise_within_validation_error!(min, max, options) # @param options [Hash] Validation options containing custom messages # # @raise [ValidationError] Always raised with appropriate message + # + # @rbs (Integer min, Integer max, Hash[Symbol, untyped] options) -> void def raise_not_within_validation_error!(min, max, options) message = options[:not_within_message] || options[:not_in_message] || options[:message] message %= { min:, max: } unless message.nil? @@ -114,6 +120,8 @@ def raise_not_within_validation_error!(min, max, options) # @param options [Hash] Validation options containing custom messages # # @raise [ValidationError] Always raised with appropriate message + # + # @rbs (Integer min, Hash[Symbol, untyped] options) -> void def raise_min_validation_error!(min, options) message = options[:min_message] || options[:message] message %= { min: } unless message.nil? @@ -127,6 +135,8 @@ def raise_min_validation_error!(min, options) # @param options [Hash] Validation options containing custom messages # # @raise [ValidationError] Always raised with appropriate message + # + # @rbs (Integer max, Hash[Symbol, untyped] options) -> void def raise_max_validation_error!(max, options) message = options[:max_message] || options[:message] message %= { max: } unless message.nil? @@ -140,6 +150,8 @@ def raise_max_validation_error!(max, options) # @param options [Hash] Validation options containing custom messages # # @raise [ValidationError] Always raised with appropriate message + # + # @rbs (Integer is, Hash[Symbol, untyped] options) -> void def raise_is_validation_error!(is, options) message = options[:is_message] || options[:message] message %= { is: } unless message.nil? @@ -153,6 +165,8 @@ def raise_is_validation_error!(is, options) # @param options [Hash] Validation options containing custom messages # # @raise [ValidationError] Always raised with appropriate message + # + # @rbs (Integer is_not, Hash[Symbol, untyped] options) -> void def raise_is_not_validation_error!(is_not, options) message = options[:is_not_message] || options[:message] message %= { is_not: } unless message.nil? diff --git a/lib/cmdx/validators/numeric.rb b/lib/cmdx/validators/numeric.rb index 16137020c..687040d48 100644 --- a/lib/cmdx/validators/numeric.rb +++ b/lib/cmdx/validators/numeric.rb @@ -48,6 +48,8 @@ module Numeric # @example Validate value exclusion # Numeric.call(5, not_in: 1..10) # # => nil (validation passes - 5 is not in 1..10) + # + # @rbs (Numeric value, Hash[Symbol, untyped] options) -> nil def call(value, options = {}) case options in within: @@ -82,6 +84,8 @@ def call(value, options = {}) # @param options [Hash] Validation options containing custom messages # # @raise [ValidationError] With appropriate error message + # + # @rbs (Numeric min, Numeric max, Hash[Symbol, untyped] options) -> void def raise_within_validation_error!(min, max, options) message = options[:within_message] || options[:in_message] || options[:message] message %= { min:, max: } unless message.nil? @@ -96,6 +100,8 @@ def raise_within_validation_error!(min, max, options) # @param options [Hash] Validation options containing custom messages # # @raise [ValidationError] With appropriate error message + # + # @rbs (Numeric min, Numeric max, Hash[Symbol, untyped] options) -> void def raise_not_within_validation_error!(min, max, options) message = options[:not_within_message] || options[:not_in_message] || options[:message] message %= { min:, max: } unless message.nil? @@ -109,6 +115,8 @@ def raise_not_within_validation_error!(min, max, options) # @param options [Hash] Validation options containing custom messages # # @raise [ValidationError] With appropriate error message + # + # @rbs (Numeric min, Hash[Symbol, untyped] options) -> void def raise_min_validation_error!(min, options) message = options[:min_message] || options[:message] message %= { min: } unless message.nil? @@ -122,6 +130,8 @@ def raise_min_validation_error!(min, options) # @param options [Hash] Validation options containing custom messages # # @raise [ValidationError] With appropriate error message + # + # @rbs (Numeric max, Hash[Symbol, untyped] options) -> void def raise_max_validation_error!(max, options) message = options[:max_message] || options[:message] message %= { max: } unless message.nil? @@ -135,6 +145,8 @@ def raise_max_validation_error!(max, options) # @param options [Hash] Validation options containing custom messages # # @raise [ValidationError] With appropriate error message + # + # @rbs (Numeric is, Hash[Symbol, untyped] options) -> void def raise_is_validation_error!(is, options) message = options[:is_message] || options[:message] message %= { is: } unless message.nil? @@ -148,6 +160,8 @@ def raise_is_validation_error!(is, options) # @param options [Hash] Validation options containing custom messages # # @raise [ValidationError] With appropriate error message + # + # @rbs (Numeric is_not, Hash[Symbol, untyped] options) -> void def raise_is_not_validation_error!(is_not, options) message = options[:is_not_message] || options[:message] message %= { is_not: } unless message.nil? diff --git a/lib/cmdx/validators/presence.rb b/lib/cmdx/validators/presence.rb index 002a77934..942f7f81e 100644 --- a/lib/cmdx/validators/presence.rb +++ b/lib/cmdx/validators/presence.rb @@ -38,6 +38,8 @@ module Presence # @example Validate with custom message # Presence.call(nil, message: "Value cannot be blank") # # => raises ValidationError with custom message + # + # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> nil def call(value, options = {}) match = if value.is_a?(String) diff --git a/lib/cmdx/version.rb b/lib/cmdx/version.rb index 9ed9670b0..30166d518 100644 --- a/lib/cmdx/version.rb +++ b/lib/cmdx/version.rb @@ -2,6 +2,9 @@ module CMDx + # @return [String] the version of the CMDx gem + # + # @rbs return: String VERSION = "1.9.0" end diff --git a/lib/cmdx/workflow.rb b/lib/cmdx/workflow.rb index acab93bdb..618e60758 100644 --- a/lib/cmdx/workflow.rb +++ b/lib/cmdx/workflow.rb @@ -20,6 +20,8 @@ module ClassMethods # # This would raise an error: # # def work; end # end + # + # @rbs (Symbol method_name) -> void def method_added(method_name) raise "cannot redefine #{name}##{method_name} method" if method_name == :work @@ -37,6 +39,8 @@ def method_added(method_name) # task Task2 # puts pipeline.size # => 2 # end + # + # @rbs () -> Array[ExecutionGroup] def pipeline @pipeline ||= [] end @@ -55,6 +59,8 @@ def pipeline # include CMDx::Workflow # tasks ValidateTask, ProcessTask, NotifyTask, breakpoints: [:failure, :halt] # end + # + # @rbs (*untyped tasks, **untyped options) -> void def tasks(*tasks, **options) pipeline << ExecutionGroup.new( tasks.map do |task| @@ -83,6 +89,8 @@ def tasks(*tasks, **options) # include CMDx::Workflow # # Now has access to task, tasks, and work methods # end + # + # @rbs (Class base) -> void def self.included(base) base.extend(ClassMethods) end @@ -100,6 +108,8 @@ def self.included(base) # # workflow = MyWorkflow.new # result = workflow.work + # + # @rbs () -> void def work Pipeline.execute(self) end diff --git a/lib/generators/cmdx/locale_generator.rb b/lib/generators/cmdx/locale_generator.rb index fa2b310a8..77b4e54bf 100644 --- a/lib/generators/cmdx/locale_generator.rb +++ b/lib/generators/cmdx/locale_generator.rb @@ -30,7 +30,6 @@ class LocaleGenerator < Rails::Generators::Base # # Copy Spanish locale file # rails generate cmdx:locale es # # => Creates config/locales/es.yml - # def copy_locale_files copy_file("#{locale}.yml", "config/locales/#{locale}.yml") end