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
3 changes: 3 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ jobs:
matrix:
ruby:
- '3.1.4'
- '3.2.9'
- '3.3.10'
- '3.4.7'

steps:
- uses: actions/checkout@v4
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
## [Unreleased]

# v0.4.0

- Refactored Castkit internals to build on Cattri registries for attributes, configuration, contracts, and attribute definitions
- Added Cattri-powered introspection helpers, serialization toggle behavior and enhanced type/validator DSLs, plus coverage for the new plugin helpers

## [0.3.0] - 2025-04-16

### Added
Expand Down
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,14 @@ Castkit comes with built-in support for primitive types and allows registration

```ruby
{
array: Castkit::Types::Collection,
boolean: Castkit::Types::Boolean,
date: Castkit::Types::Date,
datetime: Castkit::Types::DateTime,
float: Castkit::Types::Float,
hash: Castkit::Types::Base,
integer: Castkit::Types::Integer,
string: Castkit::Types::String
array: Castkit::Types::Collection,
boolean: Castkit::Types::Boolean,
date: Castkit::Types::Date,
datetime: Castkit::Types::DateTime,
float: Castkit::Types::Float,
hash: Castkit::Types::Base,
integer: Castkit::Types::Integer,
string: Castkit::Types::String
}
```

Expand Down
2 changes: 2 additions & 0 deletions castkit.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,14 @@ Gem::Specification.new do |spec|
spec.require_paths = ["lib"]

# Runtime dependencies
spec.add_dependency "cattri", ">=0.2.3"
spec.add_dependency "thor"

# Development dependencies
spec.add_development_dependency "rspec"
spec.add_development_dependency "rubocop"
spec.add_development_dependency "simplecov"
spec.add_development_dependency "simplecov-cobertura"
spec.add_development_dependency "simplecov-html"
spec.add_development_dependency "yard"
end
23 changes: 11 additions & 12 deletions lib/castkit/attribute.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ module Castkit
# @see Castkit::DSL::Attribute::Validation
class Attribute
include Castkit::DSL::Attribute
include Cattri

cattri :field, nil, expose: :read, final: true
cattri :type, nil, expose: :read, final: true
cattri :options, nil, expose: :read, final: true

class << self
# Defines a reusable attribute definition via a DSL wrapper.
Expand Down Expand Up @@ -71,26 +76,20 @@ def process_type(type)
end
end

# @return [Symbol] the attribute name
attr_reader :field

# @return [Symbol, Class, Array] the declared or normalized type
attr_reader :type

# @return [Hash] full option hash, including merged defaults
attr_reader :options

# Initializes a new attribute definition.
#
# @param field [Symbol] the attribute name
# @param type [Symbol, Class, Array<Symbol, Class>] the type (or list of types)
# @param default [Object, Proc, nil] optional static or callable default
# @param options [Hash] additional attribute options
def initialize(field, type, default: nil, **options)
@field = field
@type = self.class.normalize_type(type)
super()

cattri_variable_set(:field, field, final: true)
cattri_variable_set(:type, self.class.normalize_type(type), final: true)

@default = default
@options = populate_options(options)
cattri_variable_set(:options, populate_options(options), final: true)

validate!
end
Expand Down
1 change: 1 addition & 0 deletions lib/castkit/castkit.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# frozen_string_literal: true

require "cattri"
require_relative "core/attribute_types"

# Castkit is a lightweight, type-safe data object system for Ruby.
Expand Down
73 changes: 25 additions & 48 deletions lib/castkit/configuration.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
# frozen_string_literal: true

require_relative "types"
require "cattri"

module Castkit
# Configuration container for global Castkit settings.
#
# This includes type registration, validation, and enforcement flags
# used throughout Castkit's attribute system.
class Configuration
include Cattri

# Default mapping of primitive type definitions.
#
# @return [Hash{Symbol => Castkit::Types::Base}]
Expand Down Expand Up @@ -36,56 +39,30 @@ class Configuration
uuid: :string
}.freeze

# @return [Hash{Symbol => Castkit::Types::Base}] registered types
attr_reader :types

# Set default plugins that will be used globally in all Castkit::DataObject subclasses.
# This is equivalent to calling `enable_plugins` in every class.
#
# @return [Array<Symbol>] default plugin names to be applied to all DataObject subclasses
attr_accessor :default_plugins

# Whether to raise an error if values should be validated before deserializing, e.g. true -> "true"
# @return [Boolean]
attr_accessor :enforce_typing

# Whether to raise an error if access mode is not recognized.
# @return [Boolean]
attr_accessor :enforce_attribute_access

# Whether to raise an error if a prefix is defined without `unwrapped: true`.
# @return [Boolean]
attr_accessor :enforce_unwrapped_prefix

# Whether to raise an error if an array attribute is missing the `of:` type.
# @return [Boolean]
attr_accessor :enforce_array_options

# Whether to raise an error for unknown and invalid type definitions.
# @return [Boolean]
attr_accessor :raise_type_errors

# Whether to emit warnings when Castkit detects misconfigurations.
# @return [Boolean]
attr_accessor :enable_warnings

# Whether the strict flag is enabled by default for all DataObjects and Contracts.
# @return [Boolean]
attr_accessor :strict_by_default
cattri :types, -> { DEFAULT_TYPES.dup }, expose: :read_write
cattri :default_plugins, [], expose: :read_write
cattri :enforce_typing, true, expose: :read_write
cattri :enforce_attribute_access, true, expose: :read_write
cattri :enforce_unwrapped_prefix, true, expose: :read_write
cattri :enforce_array_options, true, expose: :read_write
cattri :raise_type_errors, true, expose: :read_write
cattri :enable_warnings, true, expose: :read_write
cattri :strict_by_default, true, expose: :read_write

# Initializes the configuration with default types and enforcement flags.
#
# @return [void]
def initialize
@types = DEFAULT_TYPES.dup
@enforce_typing = true
@enforce_attribute_access = true
@enforce_unwrapped_prefix = true
@enforce_array_options = true
@raise_type_errors = true
@enable_warnings = true
@strict_by_default = true
@default_plugins = []
super
self.types = DEFAULT_TYPES.dup
self.enforce_typing = true
self.enforce_attribute_access = true
self.enforce_unwrapped_prefix = true
self.enforce_array_options = true
self.raise_type_errors = true
self.enable_warnings = true
self.strict_by_default = true
self.default_plugins = []

apply_type_aliases!
end
Expand Down Expand Up @@ -136,7 +113,7 @@ def register_plugin(name, plugin)
# @return [Castkit::Types::Base]
# @raise [Castkit::TypeError] if the type is not registered
def fetch_type(type)
@types.fetch(type.to_sym) do
types.fetch(type.to_sym) do
raise Castkit::TypeError, "Unknown type `#{type.inspect}`" if raise_type_errors
end
end
Expand All @@ -146,14 +123,14 @@ def fetch_type(type)
# @param type [Symbol]
# @return [Boolean]
def type_registered?(type)
@types.key?(type.to_sym)
types.key?(type.to_sym)
end

# Restores the type registry to its default state.
#
# @return [void]
def reset_types!
@types = DEFAULT_TYPES.dup
self.types = DEFAULT_TYPES.dup
apply_type_aliases!
end

Expand Down
21 changes: 9 additions & 12 deletions lib/castkit/contract/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ class Base
].freeze

class << self
include Cattri

# Defines an attribute for the contract.
#
# Only a subset of options is allowed inside a contract.
Expand Down Expand Up @@ -71,15 +73,9 @@ def validate!(input)
Castkit::Contract::Result.new(definition[:name].to_s, input)
end

# Returns internal contract metadata.
#
# @return [Hash]
def definition
@definition ||= {
name: :ephemeral,
attributes: {}
}
end
cattri :definition,
-> { { name: :ephemeral, attributes: {} } },
scope: :class, expose: :read_write

# Returns the defined attributes.
#
Expand Down Expand Up @@ -117,8 +113,8 @@ def define(name = :ephemeral, source = nil, validation_rules: {}, &block)
def define_from_source(name, source)
source_attributes = source.attributes.dup

@definition = {
name: name,
self.definition = {
name: name.to_sym,
attributes: source_attributes.transform_values do |attr|
Castkit::Attribute.new(attr.field, attr.type, **attr.options.slice(*ATTRIBUTE_OPTIONS))
end
Expand All @@ -131,7 +127,8 @@ def define_from_source(name, source)
# @yield [block]
# @return [void]
def define_from_block(name, &block)
definition[:name] = name
contract_name = name ? name.to_sym : :ephemeral
self.definition = { name: contract_name, attributes: {} }

@__castkit_contract_dsl = true
instance_eval(&block)
Expand Down
16 changes: 10 additions & 6 deletions lib/castkit/contract/result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,28 @@ module Contract
# Provides access to the validation outcome, including whether it succeeded or failed,
# and includes the full list of errors if any.
class Result
include Cattri

# @return [Symbol] the name of the contract
attr_reader :contract
cattri :contract, nil, expose: :read

# @return [Hash{Symbol => Object}] the validated input
attr_reader :input
cattri :input, nil, expose: :read

# @return [Hash{Symbol => Object}] the validation error hash
attr_reader :errors
cattri :errors, {}, expose: :read

# Initializes a new result object.
#
# @param contract [Symbol, String] the name of the contract
# @param input [Hash{Symbol => Object}] the validated input
# @param errors [Hash{Symbol => Object}] the validation errors
def initialize(contract, input, errors: {})
@contract = contract.to_sym.freeze
@input = input.freeze
@errors = errors.freeze
super()

cattri_variable_set(:contract, contract.to_sym.freeze)
cattri_variable_set(:input, input.freeze)
cattri_variable_set(:errors, errors.freeze)
end

# A debug-friendly representation of the validation result.
Expand Down
4 changes: 3 additions & 1 deletion lib/castkit/core/attribute_types.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ def array(field, **options)
#
# @param field [Symbol]
# @param options [Hash]
def hash(field, **options)
def hash(field = nil, **options)
return super() if field.nil?

attribute(field, :hash, **options)
end

Expand Down
Loading