Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .cursor/prompts/yardoc.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions LLM.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions docs/getting_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 8 additions & 0 deletions lib/cmdx.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
83 changes: 82 additions & 1 deletion lib/cmdx/attribute.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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<Attribute>] Array of child attributes
#
# @example
# attribute.children # => [#<Attribute @name=:street>, #<Attribute @name=:city>]
#
# @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 # => #<Attribute @name=:address>
#
# @rbs @parent: (Attribute | nil)
attr_reader :parent

# Returns the expected type(s) for this attribute's value.
#
# @return [Array<Class>] 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.
#
Expand All @@ -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
Expand Down Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -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
Expand All @@ -110,6 +175,8 @@ def required(*names, **options, &)
#
# @example
# attribute.required? # => true
#
# @rbs () -> bool
def required?
!!@required
end
Expand All @@ -120,6 +187,8 @@ def required?
#
# @example
# attribute.source # => :context
#
# @rbs () -> untyped
def source
@source ||= parent&.method_name || begin
value = options[:source]
Expand All @@ -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}_" }
Expand All @@ -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

Expand All @@ -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)
Expand All @@ -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
Expand All @@ -204,13 +281,17 @@ 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

# 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
Expand Down
20 changes: 20 additions & 0 deletions lib/cmdx/attribute_registry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ module CMDx
# in a hierarchical structure, supporting nested attribute definitions.
class AttributeRegistry

# Returns the collection of registered attributes.
#
# @return [Array<Attribute>] Array of registered attributes
#
# @example
# registry.registry # => [#<Attribute @name=:name>, #<Attribute @name=:email>]
#
# @rbs @registry: Array[Attribute]
attr_reader :registry
alias to_a registry

Expand All @@ -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
Expand All @@ -28,6 +38,8 @@ def initialize(registry = [])
#
# @example
# new_registry = registry.dup
#
# @rbs () -> AttributeRegistry
def dup
self.class.new(registry.dup)
end
Expand All @@ -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
Expand All @@ -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) }
Expand All @@ -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
Expand All @@ -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

Expand Down
Loading