README.md in sinclair-1.10.0 vs README.md in sinclair-1.11.0

- old
+ new

@@ -11,26 +11,28 @@ This gem helps the creation of complex gems/concerns that enables creation of methods on the fly through class methods -Next release: [1.11.0](https://github.com/darthjee/sinclair/compare/1.10.0...master) +Current Release: [1.11.0](https://github.com/darthjee/sinclair/tree/1.11.0) +[Next release](https://github.com/darthjee/sinclair/compare/1.11.0...master) + Yard Documentation ------------------- -[https://www.rubydoc.info/gems/sinclair/1.10.0](https://www.rubydoc.info/gems/sinclair/1.10.0) +[https://www.rubydoc.info/gems/sinclair/1.11.0](https://www.rubydoc.info/gems/sinclair/1.11.0) Installation --------------- -- Install it + - Install it ```ruby gem install sinclair ``` -- Or add Sinclair to your `Gemfile` and `bundle install`: + - Or add Sinclair to your `Gemfile` and `bundle install`: ```ruby gem 'sinclair' ``` @@ -39,533 +41,620 @@ ``` Usage --------------- ### Sinclair builder -Sinclair can actually be used in several ways, as a stand alone object capable of -adding methods to your class on the fly, as a builder inside a class method -or by extending it for more complex logics +Sinclair can actually be used in several ways + - as a stand alone object capable of adding methods to your class on the fly + - as a builder inside a class method + - extending the builder for more complex logics -#### Stand Alone usage creating methods on the fly +<details> +<summary>Stand Alone usage creating methods on the fly</summary> + ```ruby +class Clazz +end - class Clazz - end +builder = Sinclair.new(Clazz) - builder = Sinclair.new(Clazz) +builder.add_method(:twenty, '10 + 10') +builder.add_method(:eighty) { 4 * twenty } +builder.add_class_method(:one_hundred) { 100 } +builder.add_class_method(:one_hundred_twenty, 'one_hundred + 20') +builder.build - builder.add_method(:twenty, '10 + 10') - builder.add_method(:eighty) { 4 * twenty } - builder.add_class_method(:one_hundred) { 100 } - builder.add_class_method(:one_hundred_twenty, 'one_hundred + 20') - builder.build +instance = Clazz.new - instance = Clazz.new +puts "Twenty => #{instance.twenty}" # Twenty => 20 +puts "Eighty => #{instance.eighty}" # Eighty => 80 - puts "Twenty => #{instance.twenty}" # Twenty => 20 - puts "Eighty => #{instance.eighty}" # Eighty => 80 - - puts "One Hundred => #{Clazz.one_hundred}" # One Hundred => 100 - puts "One Hundred => #{Clazz.one_hundred_twenty}" # One Hundred Twenty => 120 +puts "One Hundred => #{Clazz.one_hundred}" # One Hundred => 100 +puts "One Hundred => #{Clazz.one_hundred_twenty}" # One Hundred Twenty => 120 ``` +</details> -#### Builder in class method +<details> +<summary>Builder in class method</summary> ```ruby - class HttpJsonModel - attr_reader :json +class HttpJsonModel + attr_reader :json - class << self - def parse(attribute, path: []) - builder = Sinclair.new(self) + class << self + def parse(attribute, path: []) + builder = Sinclair.new(self) - keys = (path + [attribute]).map(&:to_s) + keys = (path + [attribute]).map(&:to_s) - builder.add_method(attribute) do - keys.inject(hash) { |h, key| h[key] } - end - - builder.build + builder.add_method(attribute) do + keys.inject(hash) { |h, key| h[key] } end - end - def initialize(json) - @json = json + builder.build end + end - def hash - @hash ||= JSON.parse(json) - end + def initialize(json) + @json = json end - class HttpPerson < HttpJsonModel - parse :uid - parse :name, path: [:personal_information] - parse :age, path: [:personal_information] - parse :username, path: [:digital_information] - parse :email, path: [:digital_information] + def hash + @hash ||= JSON.parse(json) end +end - json = <<-JSON - { - "uid": "12sof511", - "personal_information":{ - "name":"Bob", - "age": 21 - }, - "digital_information":{ - "username":"lordbob", - "email":"lord@bob.com" - } +class HttpPerson < HttpJsonModel + parse :uid + parse :name, path: [:personal_information] + parse :age, path: [:personal_information] + parse :username, path: [:digital_information] + parse :email, path: [:digital_information] +end + +json = <<-JSON + { + "uid": "12sof511", + "personal_information":{ + "name":"Bob", + "age": 21 + }, + "digital_information":{ + "username":"lordbob", + "email":"lord@bob.com" } - JSON + } +JSON - person = HttpPerson.new(json) +person = HttpPerson.new(json) - person.uid # returns '12sof511' - person.name # returns 'Bob' - person.age # returns 21 - person.username # returns 'lordbob' - person.email # returns 'lord@bob.com' +person.uid # returns '12sof511' +person.name # returns 'Bob' +person.age # returns 21 +person.username # returns 'lordbob' +person.email # returns 'lord@bob.com' ``` +</details> +<details> +<summary>Class method adding class methods</summary> + ```ruby - module EnvSettings - def env_prefix(new_prefix=nil) - @env_prefix = new_prefix if new_prefix - @env_prefix - end +module EnvSettings + def env_prefix(new_prefix=nil) + @env_prefix = new_prefix if new_prefix + @env_prefix + end - def from_env(*method_names) - builder = Sinclair.new(self) + def from_env(*method_names) + builder = Sinclair.new(self) - method_names.each do |method_name| - env_key = [env_prefix, method_name].compact.join('_').upcase + method_names.each do |method_name| + env_key = [env_prefix, method_name].compact.join('_').upcase - builder.add_class_method(method_name, cached: true) do - ENV[env_key] - end - - builder.build + builder.add_class_method(method_name, cached: true) do + ENV[env_key] end + + builder.build end end +end - class MyServerConfig - extend EnvSettings +class MyServerConfig + extend EnvSettings - env_prefix :server + env_prefix :server - from_env :host, :port - end + from_env :host, :port +end - ENV['SERVER_HOST'] = 'myserver.com' - ENV['SERVER_PORT'] = '9090' +ENV['SERVER_HOST'] = 'myserver.com' +ENV['SERVER_PORT'] = '9090' - MyServerConfig.host # returns 'myserver.com' - MyServerConfig.port # returns '9090' +MyServerConfig.host # returns 'myserver.com' +MyServerConfig.port # returns '9090' ``` +</details> -#### Extending the builder +<details> +<summary>Extending the builder</summary> ```ruby +class ValidationBuilder < Sinclair + delegate :expected, to: :options_object - class ValidationBuilder < Sinclair - delegate :expected, to: :options_object + def initialize(klass, options={}) + super + end - def initialize(klass, options={}) - super - end + def add_validation(field) + add_method("#{field}_valid?", "#{field}.is_a?#{expected}") + end - def add_validation(field) - add_method("#{field}_valid?", "#{field}.is_a?#{expected}") - end - - def add_accessors(fields) - klass.send(:attr_accessor, *fields) - end + def add_accessors(fields) + klass.send(:attr_accessor, *fields) end +end - module MyConcern - extend ActiveSupport::Concern +module MyConcern + extend ActiveSupport::Concern - class_methods do - def validate(*fields, expected_class) - builder = ::ValidationBuilder.new(self, expected: expected_class) + class_methods do + def validate(*fields, expected_class) + builder = ::ValidationBuilder.new(self, expected: expected_class) - validatable_fields.concat(fields) - builder.add_accessors(fields) + validatable_fields.concat(fields) + builder.add_accessors(fields) - fields.each do |field| - builder.add_validation(field) - end - - builder.build + fields.each do |field| + builder.add_validation(field) end - def validatable_fields - @validatable_fields ||= [] - end + builder.build end - def valid? - self.class.validatable_fields.all? do |field| - public_send("#{field}_valid?") - end + def validatable_fields + @validatable_fields ||= [] end end - class MyClass - include MyConcern - validate :name, :surname, String - validate :age, :legs, Integer - - def initialize(name: nil, surname: nil, age: nil, legs: nil) - @name = name - @surname = surname - @age = age - @legs = legs + def valid? + self.class.validatable_fields.all? do |field| + public_send("#{field}_valid?") end end +end - instance = MyClass.new +class MyClass + include MyConcern + validate :name, :surname, String + validate :age, :legs, Integer + + def initialize(name: nil, surname: nil, age: nil, legs: nil) + @name = name + @surname = surname + @age = age + @legs = legs + end +end + +instance = MyClass.new ``` the instance will respond to the methods ```name``` ```name=``` ```name_valid?``` ```surname``` ```surname=``` ```surname_valid?``` ```age``` ```age=``` ```age_valid?``` ```legs``` ```legs=``` ```legs_valid?``` ```valid?```. ```ruby - valid_object = MyClass.new( - name: :name, - surname: 'surname', - age: 20, - legs: 2 - ) - valid_object.valid? # returns true +valid_object = MyClass.new( + name: :name, + surname: 'surname', + age: 20, + legs: 2 +) +valid_object.valid? # returns true ``` ```ruby +invalid_object = MyClass.new( + name: 'name', + surname: 'surname', + age: 20, + legs: 2 +) +invalid_object.valid? # returns false +``` +</details> - invalid_object = MyClass.new( - name: 'name', - surname: 'surname', - age: 20, - legs: 2 - ) - invalid_object.valid? # returns false +#### Different ways of adding the methods +There are different ways to add a method +<details> +<summary>Define method using block</summary> + +```ruby +klass = Class.new +instance = klass.new + +builder = Sinclair.new(klass) +builder.add_method(:random_number) { Random.rand(10..20) } +builder.build + +instance.random_number # returns a number between 10 and 20 ``` +</details> +<details> +<summary>Define method using string</summary> + +```ruby +klass = Class.new +instance = klass.new + +builder = Sinclair.new(klass) +builder.add_method(:random_number, "Random.rand(10..20)") +builder.build + +instance.random_number # returns a number between 10 and 20 +``` +</details> + +<details> +<summary>Define method using a call to the class</summary> + +```ruby +klass = Class.new + +builder = Sinclair.new(klass) +builder.add_class_method(:attr_accessor, :number, type: :call) +builder.build + +klass.number # returns nil +klass.number = 10 +klass.number # returns 10 +``` +</details> + #### Caching the result If wanted, the result of the method can be stored in an instance variable with the same name. When caching, you can cache with type `:full` so that even `nil` values are cached +<details> +<summary>Example of simple cache usage</summary> + ```ruby - class MyModel - attr_accessor :base, :expoent - end +class MyModel + attr_accessor :base, :expoent +end - builder = Sinclair.new(MyModel) +builder = Sinclair.new(MyModel) - builder.add_method(:cached_power, cached: true) do - base ** expoent - end +builder.add_method(:cached_power, cached: true) do + base ** expoent +end - # equivalent of builder.add_method(:cached_power) do - # @cached_power ||= base ** expoent - # end +# equivalent of builder.add_method(:cached_power) do +# @cached_power ||= base ** expoent +# end - builder.build +builder.build - model.base = 3 - model.expoent = 2 +model.base = 3 +model.expoent = 2 - model.cached_power # returns 9 - model.expoent = 3 - model.cached_power # returns 9 (from cache) +model.cached_power # returns 9 +model.expoent = 3 +model.cached_power # returns 9 (from cache) ``` +</details> +<details> +<summary>Usage of different cache types</summary> + ```ruby - module DefaultValueable - def default_reader(*methods, value:, accept_nil: false) - DefaultValueBuilder.new( - self, value: value, accept_nil: accept_nil - ).add_default_values(*methods) - end +module DefaultValueable + def default_reader(*methods, value:, accept_nil: false) + DefaultValueBuilder.new( + self, value: value, accept_nil: accept_nil + ).add_default_values(*methods) end +end - class DefaultValueBuilder < Sinclair - def add_default_values(*methods) - default_value = value +class DefaultValueBuilder < Sinclair + def add_default_values(*methods) + default_value = value - methods.each do |method| - add_method(method, cached: cache_type) { default_value } - end - - build + methods.each do |method| + add_method(method, cached: cache_type) { default_value } end - private + build + end - delegate :accept_nil, :value, to: :options_object + private - def cache_type - accept_nil ? :full : :simple - end + delegate :accept_nil, :value, to: :options_object + + def cache_type + accept_nil ? :full : :simple end +end - class Server - extend DefaultValueable +class Server + extend DefaultValueable - attr_writer :host, :port + attr_writer :host, :port - default_reader :host, value: 'server.com', accept_nil: false - default_reader :port, value: 80, accept_nil: true + default_reader :host, value: 'server.com', accept_nil: false + default_reader :port, value: 80, accept_nil: true - def url - return "http://#{host}" unless port + def url + return "http://#{host}" unless port - "http://#{host}:#{port}" - end + "http://#{host}:#{port}" end +end - server = Server.new +server = Server.new - server.url # returns 'http://server.com:80' +server.url # returns 'http://server.com:80' - server.host = 'interstella.com' - server.port = 5555 - server.url # returns 'http://interstella.com:5555' +server.host = 'interstella.com' +server.port = 5555 +server.url # returns 'http://interstella.com:5555' - server.host = nil - server.port = nil - server.url # return 'http://server.com' +server.host = nil +server.port = nil +server.url # return 'http://server.com' ``` +</details> ### Sinclair::Configurable Configurable is a module that, when used, can add configurations to your classes/modules. Configurations are read-only objects that can only be set using the `configurable#configure` method which accepts a block or hash +<details> +<summary>Using configurable</summary> + ```ruby - module MyConfigurable - extend Sinclair::Configurable +module MyConfigurable + extend Sinclair::Configurable - # port is defaulted to 80 - configurable_with :host, port: 80 - end + # port is defaulted to 80 + configurable_with :host, port: 80 +end - MyConfigurable.configure(port: 5555) do |config| - config.host 'interstella.art' - end +MyConfigurable.configure(port: 5555) do |config| + config.host 'interstella.art' +end - MyConfigurable.config.host # returns 'interstella.art' - MyConfigurable.config.port # returns 5555 +MyConfigurable.config.host # returns 'interstella.art' +MyConfigurable.config.port # returns 5555 - # Configurable enables options that can be passed - MyConfigurable.as_options.host # returns 'interstella.art' +# Configurable enables options that can be passed +MyConfigurable.as_options.host # returns 'interstella.art' - # Configurable enables options that can be passed with custom values - MyConfigurable.as_options(host: 'other').host # returns 'other' +# Configurable enables options that can be passed with custom values +MyConfigurable.as_options(host: 'other').host # returns 'other' - MyConfigurable.reset_config +MyConfigurable.reset_config - MyConfigurable.config.host # returns nil - MyConfigurable.config.port # returns 80 +MyConfigurable.config.host # returns nil +MyConfigurable.config.port # returns 80 ``` +</details> Configurations can also be done through custom classes +<details> +<summary>Using configration class</summary> + ```ruby - class MyServerConfig < Sinclair::Config - config_attributes :host, :port +class MyServerConfig < Sinclair::Config + config_attributes :host, :port - def url - if @port - "http://#{@host}:#{@port}" - else - "http://#{@host}" - end + def url + if @port + "http://#{@host}:#{@port}" + else + "http://#{@host}" end end +end - class Client - extend Sinclair::Configurable +class Client + extend Sinclair::Configurable - configurable_by MyServerConfig - end + configurable_by MyServerConfig +end - Client.configure do - host 'interstella.com' - end +Client.configure do + host 'interstella.com' +end - Client.config.url # returns 'http://interstella.com' +Client.config.url # returns 'http://interstella.com' - Client.configure do |config| - config.port 8080 - end +Client.configure do |config| + config.port 8080 +end - Client.config.url # returns 'http://interstella.com:8080' +Client.config.url # returns 'http://interstella.com:8080' ``` +</details> ### Sinclair::EnvSettable Settable allows classes to extract configuration from environments through a simple meta-programable way +<details> +<summary>Using env settable example</summary> + ```ruby - class ServiceClient - extend Sinclair::EnvSettable - attr_reader :username, :password, :host, :port +class ServiceClient + extend Sinclair::EnvSettable + attr_reader :username, :password, :host, :port - settings_prefix 'SERVICE' + settings_prefix 'SERVICE' - with_settings :username, :password, port: 80, hostname: 'my-host.com' + with_settings :username, :password, port: 80, hostname: 'my-host.com' - def self.default - @default ||= new - end + def self.default + @default ||= new + end - def initialize( - username: self.class.username, - password: self.class.password, - port: self.class.port, - hostname: self.class.hostname - ) - @username = username - @password = password - @port = port - @hostname = hostname - end + def initialize( + username: self.class.username, + password: self.class.password, + port: self.class.port, + hostname: self.class.hostname + ) + @username = username + @password = password + @port = port + @hostname = hostname end +end - ENV['SERVICE_USERNAME'] = 'my-login' - ENV['SERVICE_HOSTNAME'] = 'host.com' +ENV['SERVICE_USERNAME'] = 'my-login' +ENV['SERVICE_HOSTNAME'] = 'host.com' - ServiceClient.default # returns #<ServiceClient:0x0000556fa1b366e8 @username="my-login", @password=nil, @port=80, @hostname="host.com">' +ServiceClient.default # returns #<ServiceClient:0x0000556fa1b366e8 @username="my-login", @password=nil, @port=80, @hostname="host.com">' ``` +</details> ### Sinclair::Options Options allows projects to have an easy to configure option object +<details> +<summary>Example of using Options</summary> + ```ruby - class ConnectionOptions < Sinclair::Options - with_options :timeout, :retries, port: 443, protocol: 'https' +class ConnectionOptions < Sinclair::Options + with_options :timeout, :retries, port: 443, protocol: 'https' - # skip_validation if you dont want to validate intialization arguments - end + # skip_validation if you dont want to validate intialization arguments +end - options = ConnectionOptions.new( - timeout: 10, - protocol: 'http' - ) +options = ConnectionOptions.new( + timeout: 10, + protocol: 'http' +) - options.timeout # returns 10 - options.retries # returns nil - options.protocol # returns 'http' - options.port # returns 443 +options.timeout # returns 10 +options.retries # returns nil +options.protocol # returns 'http' +options.port # returns 443 - ConnectionOptions.new(invalid: 10) # raises Sinclair::Exception::InvalidOptions +ConnectionOptions.new(invalid: 10) # raises Sinclair::Exception::InvalidOptions ``` +</details> ### Sinclair::Comparable Comparable allows a class to implement quickly a `==` method comparing given attributes +<details> +<summary>Example of Comparable usage</summary> + ```ruby - class SampleModel - include Sinclair::Comparable +class SampleModel + include Sinclair::Comparable - comparable_by :name - attr_reader :name, :age + comparable_by :name + attr_reader :name, :age - def initialize(name: nil, age: nil) - @name = name - @age = age - end + def initialize(name: nil, age: nil) + @name = name + @age = age end +end - model1 = model_class.new(name: 'jack', age: 21) - model2 = model_class.new(name: 'jack', age: 23) +model1 = model_class.new(name: 'jack', age: 21) +model2 = model_class.new(name: 'jack', age: 23) - model1 == model2 # returns true +model1 == model2 # returns true ``` +</details> RSspec matcher --------------- You can use the provided matcher to check that your builder is adding a method correctly +<details> +<summary>Sample of specs over adding methods</summary> + ```ruby - class DefaultValue - delegate :build, to: :builder - attr_reader :klass, :method, :value, :class_method +class DefaultValue + delegate :build, to: :builder + attr_reader :klass, :method, :value, :class_method - def initialize(klass, method, value, class_method: false) - @klass = klass - @method = method - @value = value - @class_method = class_method - end + def initialize(klass, method, value, class_method: false) + @klass = klass + @method = method + @value = value + @class_method = class_method + end - private + private - def builder - @builder ||= Sinclair.new(klass).tap do |b| - if class_method - b.add_class_method(method) { value } - else - b.add_method(method) { value } - end + def builder + @builder ||= Sinclair.new(klass).tap do |b| + if class_method + b.add_class_method(method) { value } + else + b.add_method(method) { value } end end end +end - RSpec.describe Sinclair::Matchers do - subject(:builder_class) { DefaultValue } +RSpec.describe Sinclair::Matchers do + subject(:builder_class) { DefaultValue } - let(:klass) { Class.new } - let(:method) { :the_method } - let(:value) { Random.rand(100) } - let(:builder) { builder_class.new(klass, method, value) } - let(:instance) { klass.new } + let(:klass) { Class.new } + let(:method) { :the_method } + let(:value) { Random.rand(100) } + let(:builder) { builder_class.new(klass, method, value) } + let(:instance) { klass.new } - context 'when the builder runs' do - it do - expect { builder.build }.to add_method(method).to(instance) - end + context 'when the builder runs' do + it do + expect { builder.build }.to add_method(method).to(instance) end + end - context 'when the builder runs' do - it do - expect { builder.build }.to add_method(method).to(klass) - end + context 'when the builder runs' do + it do + expect { builder.build }.to add_method(method).to(klass) end + end - context 'when adding class methods' do - subject(:builder) { builder_class.new(klass, method, value, class_method: true) } + context 'when adding class methods' do + subject(:builder) { builder_class.new(klass, method, value, class_method: true) } - context 'when the builder runs' do - it do - expect { builder.build }.to add_class_method(method).to(klass) - end + context 'when the builder runs' do + it do + expect { builder.build }.to add_class_method(method).to(klass) end end end +end ``` ```bash - > bundle exec rspec ``` ```string Sinclair::Matchers @@ -575,12 +664,13 @@ should add method 'the_method' to #<Class:0x000055e5d9b8c0a8> instances when adding class methods when the builder runs should add method class_method 'the_method' to #<Class:0x000055e5d9b95d88> ``` +</details> Projects Using --------------- -- [Arstotzka](https://github.com/darthjee/arstotzka) -- [Azeroth](https://github.com/darthjee/azeroth) -- [Magicka](https://github.com/darthjee/magicka) + - [Arstotzka](https://github.com/darthjee/arstotzka) + - [Azeroth](https://github.com/darthjee/azeroth) + - [Magicka](https://github.com/darthjee/magicka)