README.md in domainic-attributer-0.1.0 vs README.md in domainic-attributer-0.2.0

- old
+ new

@@ -1,396 +1,83 @@ # Domainic::Attributer -[![Domainic::Attributer Version](https://badge.fury.io/rb/domainic-attributer.svg)](https://rubygems.org/gems/domainic-attributer) +[![Domainic::Attributer Version](https://img.shields.io/gem/v/domainic-attributer?style=for-the-badge&logo=rubygems&logoColor=white&logoSize=auto&label=Gem%20Version)](https://rubygems.org/gems/domainic-attributer) +[![Domainic::Attributer License](https://img.shields.io/github/license/domainic/domainic?style=for-the-badge&logo=opensourceinitiative&logoColor=white&logoSize=auto)](./LICENSE) +[![Domainic::Attributer Docs](https://img.shields.io/badge/rubydoc-blue?style=for-the-badge&logo=readthedocs&logoColor=white&logoSize=auto&label=docs)](https://rubydoc.info/gems/domainic-attributer/0.1.0) +[![Domainic::Attributer Open Issues](https://img.shields.io/github/issues-search/domainic/domainic?query=state%3Aopen%20label%3Adomainic-attributer&style=for-the-badge&logo=github&logoColor=white&logoSize=auto&label=issues&color=red)](https://github.com/domainic/domainic/issues?q=state%3Aopen%20label%3Adomainic-attributer%20) -Domainic::Attributer is a powerful toolkit for Ruby that brings clarity and safety to your class attributes. It's -designed to solve common Domain-Driven Design (DDD) challenges by making your class attributes self-documenting, -type-safe, and well-behaved. Ever wished your class attributes could: +Domainic::Attributer is a powerful toolkit that brings clarity and safety to your Ruby class attributes. +Ever wished your class attributes could: * Validate themselves to ensure they only accept correct values? * Transform input data automatically into the right format? * Have clear, enforced visibility rules? * Handle their own default values intelligently? * Tell you when they change? * Distinguish between required arguments and optional settings? -That's exactly what Domainic::Attributer does! It's particularly useful when building domain models, value objects, or -any Ruby classes where data integrity and clear interfaces matter. Instead of writing repetitive validation code, manual -type checking, and custom attribute methods, let Domainic::Attributer handle the heavy lifting while you focus on your -domain logic. +That's exactly what Domainic::Attributer does! It provides a declarative way to define and manage attributes +in your Ruby classes, ensuring data integrity and clear interfaces. It's particularly valuable for: -Think of it as giving your attributes a brain - they know what they want, how they should behave, and they're not afraid -to speak up when something's not right! +* Domain models and value objects +* Service objects and command patterns +* Configuration objects +* Any class where attribute behavior matters -## Installation +Think of it as giving your attributes a brain - they know what they want, how they should behave, and +they're not afraid to speak up when something's not right! -Add this line to your application's Gemfile: +## Quick Start ```ruby -gem 'domainic-attributer' -``` - -Or install it yourself as: - -```bash -gem install domainic-attributer -``` - -## Usage - -### Basic Attributes - -Getting started with Domainic::Attributer is as easy as including the module and declaring your attributes: - -```ruby -class Person +class SuperDev include Domainic::Attributer - argument :name - option :age, default: nil -end + argument :code_name, String + option :power_level, Integer, default: 9000 -person = Person.new("Alice", age: 30) -person.name # => "Alice" -person.age # => 30 -``` - -### Arguments vs Options - -Domainic::Attributer gives you two ways to define attributes: - -* `argument`: Required positional parameters that must be provided in order -* `option`: Named parameters that can be provided in any order (and are optional by default) - -```ruby -class Hero - include Domainic::Attributer - - argument :name # Required, must be first - argument :power # Required, must be second - option :catchphrase # Optional, can be provided by name - option :sidekick # Optional, can be provided by name -end - -# All valid ways to create a hero: -Hero.new("Spider-Man", "Web-slinging", catchphrase: "With great power...") -Hero.new("Batman", "Being rich", sidekick: "Robin") -Hero.new("Wonder Woman", "Super strength") -``` - -#### Argument Ordering and Default Values - -Arguments in Domainic::Attributer follow special ordering rules based on whether they have defaults: - -* Arguments without defaults are required and are automatically moved to the front of the argument list -* Arguments with defaults are optional and are moved to the end of the argument list -* Within each group (with/without defaults), arguments maintain their order of declaration - -This means the actual position when providing arguments to the constructor will be different from their declaration -order: - -```ruby -class EmailMessage - include Domainic::Attributer - - # This will be the first argument (no default) - argument :to - - # This will be the third argument (has default) - argument :priority, default: :normal - - # This will be the second argument (no default) - argument :subject -end - -# Arguments must be provided in their sorted order, -# with required arguments first: -EmailMessage.new("user@example.com", "Welcome!", :high) -# => #<EmailMessage:0x00007f9b1b8b3b10 @to="user@example.com", @priority=:high, @subject="Welcome!"> - -# If you try to provide the arguments in their declaration order, you'll get undesired results: -EmailMessage.new("user@example.com", :high, "Welcome!") -# => #<EmailMessage:0x00007f9b1b8b3b10 @to="user@example.com", @priority="Welcome!", @subject=:high> -``` - -This behavior ensures that required arguments are provided first and optional arguments (those with defaults) come -after, making argument handling more predictable. You can rely on this ordering regardless of how you declare the -arguments in your class. Best practice is to declare arguments without defaults first, followed by those with defaults. - -### Nilability And Option Requirements - -Be explicit about nil values: - -```ruby -class User - include Domainic::Attributer - - argument :email do - non_nilable # or not_null, non_null, etc. - end - - option :nickname do - default nil # Explicitly allow nil - end -end -``` - -Ensure certain options are always provided: - -```ruby -class Order - include Domainic::Attributer - - option :items, required: true - option :status, Symbol -end - -Order.new(option: ['item1', 'item2']) # OK -Order.new(status: :pending) # Raises ArgumentError -``` - -#### Required vs NonNilable - -`required` and `non_nilable` are similar but not identical. `required` means the option must be provided when the object -is created, while `non_nilable` means the option must not be nil. A `required` option can still be nil if it's provided. - -```ruby -class User - include Domainic::Attributer - - option :email, String do - required + option :favorite_gem do + validate_with ->(val) { val.to_s.end_with?('ruby') } + coerce_with ->(val) { val.to_s.downcase } non_nilable end - - option :nickname, String do - required - end end -User.new(email: 'example@example.com', nickname: nil) # OK -User.new(email: nil, nickname: 'example') # Raises ArgumentError because email is non_nilable -User.new(email: 'example@example.com') # Raises ArgumentError because nickname is required - -user = User.new(email: 'example@example.com', nickname: 'example') -user.nickname = nil # OK -user.email = nil # Raises ArgumentError because email is non_nilable +dev = SuperDev.new('RubyNinja', favorite_gem: 'RAILS_RUBY') +dev.favorite_gem # => "rails_ruby" +dev.power_level = 9001 +dev.power_level = 'over 9000' # Raises ArgumentError: invalid value for Integer ``` -### Type Validation +## Installation -Keep your data clean with built-in type validation: +Add this line to your application's Gemfile: ```ruby -class BankAccount - include Domainic::Attributer - - argument :account_name, String # Direct class validation - argument :opened_at, Time # Another direct class example - option :balance, Integer, default: 0 # Combining class validation with defaults - option :status, ->(val) { [:active, :closed].include?(val) } # Custom validation -end - -# Will raise ArgumentError: -BankAccount.new(:my_account_name, Time.now) -BankAccount.new("my_account_name", "not a time") -BankAccount.new("my_account_name", Time.now, balance: "not an integer") -BankAccount.new("my_account_name", Time.now, balance: 100, status: :not_included_in_the_allow_list) +gem 'domainic-attributer' ``` -### Documentation +Or install it yourself as: -Make your attributes self-documenting: - -```ruby -class Car - include Domainic::Attributer - - argument :make, String do - desc "The make of the car" - end - - argument :model, String do - description "The model of the car" - end - - argument :year, ->(value) { value.is_a?(Integer) && value >= 1900 && value <= Time.now.year } do - description "The year the car was made" - end -end +```bash +gem install domainic-attributer ``` -### Value Coercion +## Documentation -Transform input values automatically: +For detailed usage instructions and examples, see [USAGE.md](./docs/USAGE.md). -```ruby -class Temperature - include Domainic::Attributer +## Contributing - argument :celsius do |value| - coerce_with ->(val) { val.to_f } - validate_with ->(val) { val.is_a?(Float) } - end +We welcome contributions! Please see our +[Contributing Guidelines](https://github.com/domainic/domainic/wiki/CONTRIBUTING) for: - option :unit, default: "C" do |value| - validate_with ->(val) { ["C", "F"].include?(val) } - end -end +* Development setup and workflow +* Code style and documentation standards +* Testing requirements +* Pull request process -temp = Temperature.new("24.5") # Automatically converted to Float -temp.celsius # => 24.5 -``` +Before contributing, please review our [Code of Conduct](https://github.com/domainic/domainic/wiki/CODE_OF_CONDUCT). -### Custom Validation - -Domainic::Attributer provides flexible validation options that can be combined to create sophisticated validation rules. -You can: - -* Use Ruby classes directly to validate types -* Use Procs/lambdas for custom validation logic -* Chain multiple validations -* Combine validations with coercions - -```ruby -class BankTransfer - include Domainic::Attributer - - # Combine coercion and multiple validations - argument :amount do - coerce_with ->(val) { val.to_f } # First coerce to float - validate_with Float # Then validate it's a float - validate_with ->(val) { val.positive? } # And validate it's positive - end - - # Different validation styles - argument :status do - validate_with Symbol # Must be a Symbol - validate_with ->(val) { [:pending, :completed, :failed].include?(val) } # Must be one of these values - end - - # Validation with custom error handling - argument :reference_number do - validate_with ->(val) { - raise ArgumentError, "Reference must be 8 characters" unless val.length == 8 - true - } - end -end - -# These will work: -BankTransfer.new("50.0", :pending, "12345678") # amount coerced to 50.0 -BankTransfer.new(75.25, :completed, "ABCD1234") # amount already a float - -# These will raise ArgumentError: -BankTransfer.new(-10, :pending, "12345678") # amount must be positive -BankTransfer.new(100, :invalid, "12345678") # invalid status -BankTransfer.new(100, :pending, "123") # invalid reference number -``` - -Validations are run in the order they're defined, after any coercions. This lets you build up complex validation rules -while keeping them readable and maintainable. - -### Visibility Control - -Control access to your attributes: - -```ruby -class SecretAgent - include Domainic::Attributer - - argument :code_name - option :real_name do - private_read # Can't read real_name from outside - private_write # Can't write real_name from outside - end - option :mission do - protected # Both read and write are protected - end -end -``` - -### Change Callbacks - -React to attribute changes: - -```ruby -class Thermostat - include Domainic::Attributer - - option :temperature do - default 20 - on_change ->(old_val, new_val) { - puts "Temperature changing from #{old_val}°C to #{new_val}°C" - } - end -end -``` - -### Default Values - -Provide static defaults or generate them dynamically: - -```ruby -class Order - include Domainic::Attributer - - argument :items - option :created_at do - default { Time.now } # Dynamic default - end - option :status do - default "pending" # Static default - end -end -``` - -### Custom Method Names - -Don't like `argument` and `option`? Create your own interface: - -```ruby -class Configuration - include Domainic.Attributer(argument: :param, option: :setting) - - param :environment - setting :debug_mode, default: false -end -``` - -or turn off one of the methods entirely: - -```ruby -class Configuration - include Domainic.Attributer(argument: nil) - - option :environment -end -``` - -### Serialization - -Convert your objects to hashes easily: - -```ruby -class Product - include Domainic::Attributer - - argument :name - argument :price - option :description, default: "" - option :internal_id do - private # Won't be included in to_h output - end -end - -product = Product.new("Widget", 9.99, description: "A fantastic widget") -product.to_h # => { name: "Widget", price: 9.99, description: "A fantastic widget" } -``` - -## Contributing - -Bug reports and pull requests are welcome on GitHub. - ## License -The gem is available as open source under the terms of the [MIT License](LICENSE). +The gem is available as open source under the terms of the [MIT License](./LICENSE).