Sinclair
========
[![Code Climate](https://codeclimate.com/github/darthjee/sinclair/badges/gpa.svg)](https://codeclimate.com/github/darthjee/sinclair)
[![Test Coverage](https://codeclimate.com/github/darthjee/sinclair/badges/coverage.svg)](https://codeclimate.com/github/darthjee/sinclair/coverage)
[![Issue Count](https://codeclimate.com/github/darthjee/sinclair/badges/issue_count.svg)](https://codeclimate.com/github/darthjee/sinclair)
[![Gem Version](https://badge.fury.io/rb/sinclair.svg)](https://badge.fury.io/rb/sinclair)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/9836de08612e46b889c7978be2b72a14)](https://www.codacy.com/manual/darthjee/sinclair?utm_source=github.com&utm_medium=referral&utm_content=darthjee/sinclair&utm_campaign=Badge_Grade)
[![Inline docs](http://inch-ci.org/github/darthjee/sinclair.svg?branch=master)](http://inch-ci.org/github/darthjee/sinclair)
![sinclair](https://raw.githubusercontent.com/darthjee/sinclair/master/sinclair.jpg)
This gem helps the creation of complex gems/concerns
that enables creation of methods on the fly through class
methods
Current Release: [1.14.0](https://github.com/darthjee/sinclair/tree/1.14.0)
[Next release](https://github.com/darthjee/sinclair/compare/1.14.0...master)
Yard Documentation
-------------------
[https://www.rubydoc.info/gems/sinclair/1.14.0](https://www.rubydoc.info/gems/sinclair/1.14.0)
Installation
---------------
- Install it
```ruby
gem install sinclair
```
- Or add Sinclair to your `Gemfile` and `bundle install`:
```ruby
gem 'sinclair'
```
```bash
bundle install sinclair
```
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
- extending the builder for more complex logics
Stand Alone usage creating methods on the fly
```ruby
class Clazz
end
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
instance = Clazz.new
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
```
Builder in class method
```ruby
class HttpJsonModel
attr_reader :json
class << self
def parse(attribute, path: [])
builder = Sinclair.new(self)
keys = (path + [attribute]).map(&:to_s)
builder.add_method(attribute) do
keys.inject(hash) { |h, key| h[key] }
end
builder.build
end
end
def initialize(json)
@json = json
end
def hash
@hash ||= JSON.parse(json)
end
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]
end
json = <<-JSON
{
"uid": "12sof511",
"personal_information":{
"name":"Bob",
"age": 21
},
"digital_information":{
"username":"lordbob",
"email":"lord@bob.com"
}
}
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'
```
Class method adding class methods
```ruby
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)
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
end
end
end
class MyServerConfig
extend EnvSettings
env_prefix :server
from_env :host, :port
end
ENV['SERVER_HOST'] = 'myserver.com'
ENV['SERVER_PORT'] = '9090'
MyServerConfig.host # returns 'myserver.com'
MyServerConfig.port # returns '9090'
```
Extending the builder
```ruby
class ValidationBuilder < Sinclair
delegate :expected, to: :options_object
def initialize(klass, options={})
super
end
def add_validation(field)
add_method("#{field}_valid?", "#{field}.is_a?#{expected}")
end
def add_accessors(fields)
klass.send(:attr_accessor, *fields)
end
end
module MyConcern
extend ActiveSupport::Concern
class_methods do
def validate(*fields, expected_class)
builder = ::ValidationBuilder.new(self, expected: expected_class)
validatable_fields.concat(fields)
builder.add_accessors(fields)
fields.each do |field|
builder.add_validation(field)
end
builder.build
end
def validatable_fields
@validatable_fields ||= []
end
end
def valid?
self.class.validatable_fields.all? do |field|
public_send("#{field}_valid?")
end
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
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
```
```ruby
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, each accepting different options
Define method using block
Block methods accepts, as option
- [cache](#caching-the-result): defining the cashing of results
```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
```
Define method using string
String methods accepts, as option
- [cache](#caching-the-result): defining the cashing of results
- parameters: defining accepted parameters
- named_parameters: defining accepted named parameters
```ruby
# Example without parameters
class MyClass
end
instance = MyClass.new
builder = Sinclair.new(MyClass)
builder.add_method(:random_number, "Random.rand(10..20)")
builder.build
instance.random_number # returns a number between 10 and 20
```
```ruby
# Example with parameters
class MyClass
end
builder = described_class.new(MyClass)
builder.add_class_method(
:function, 'a ** b + c', parameters: [:a], named_parameters: [:b, { c: 15 }]
)
builder.build
MyClass.function(10, b: 2) # returns 115
```
Define method using a call to the class
Call method definitions right now have no options available
```ruby
class MyClass
end
builder = Sinclair.new(MyClass)
builder.add_class_method(:attr_accessor, :number, type: :call)
builder.build
MyClass.number # returns nil
MyClass.number = 10
MyClass.number # returns 10
```
#### 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
Example of simple cache usage
```ruby
class MyModel
attr_accessor :base, :expoent
end
builder = Sinclair.new(MyModel)
builder.add_method(:cached_power, cached: true) do
base ** expoent
end
# equivalent of builder.add_method(:cached_power) do
# @cached_power ||= base ** expoent
# end
builder.build
model.base = 3
model.expoent = 2
model.cached_power # returns 9
model.expoent = 3
model.cached_power # returns 9 (from cache)
```
Usage of different cache types
```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
end
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
end
private
delegate :accept_nil, :value, to: :options_object
def cache_type
accept_nil ? :full : :simple
end
end
class Server
extend DefaultValueable
attr_writer :host, :port
default_reader :host, value: 'server.com', accept_nil: false
default_reader :port, value: 80, accept_nil: true
def url
return "http://#{host}" unless port
"http://#{host}:#{port}"
end
end
server = Server.new
server.url # returns 'http://server.com:80'
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'
```
### 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
Using configurable
```ruby
module MyConfigurable
extend Sinclair::Configurable
# port is defaulted to 80
configurable_with :host, port: 80
end
MyConfigurable.configure(port: 5555) do |config|
config.host 'interstella.art'
end
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 with custom values
MyConfigurable.as_options(host: 'other').host # returns 'other'
MyConfigurable.reset_config
MyConfigurable.config.host # returns nil
MyConfigurable.config.port # returns 80
```
Configurations can also be done through custom classes
Using configration class
```ruby
class MyServerConfig < Sinclair::Config
config_attributes :host, :port
def url
if @port
"http://#{@host}:#{@port}"
else
"http://#{@host}"
end
end
end
class Client
extend Sinclair::Configurable
configurable_by MyServerConfig
end
Client.configure do
host 'interstella.com'
end
Client.config.url # returns 'http://interstella.com'
Client.configure do |config|
config.port 8080
end
Client.config.url # returns 'http://interstella.com:8080'
```
### Sinclair::EnvSettable
Settable allows classes to extract configuration from environments through
a simple meta-programable way
Using env settable example
```ruby
class ServiceClient
extend Sinclair::EnvSettable
attr_reader :username, :password, :host, :port
settings_prefix 'SERVICE'
with_settings :username, :password, port: 80, hostname: 'my-host.com'
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
end
ENV['SERVICE_USERNAME'] = 'my-login'
ENV['SERVICE_HOSTNAME'] = 'host.com'
ServiceClient.default # returns #'
```
### Sinclair::Options
Options allows projects to have an easy to configure option object
Example of using Options
```ruby
class ConnectionOptions < Sinclair::Options
with_options :timeout, :retries, port: 443, protocol: 'https'
# skip_validation if you dont want to validate intialization arguments
end
options = ConnectionOptions.new(
timeout: 10,
protocol: 'http'
)
options.timeout # returns 10
options.retries # returns nil
options.protocol # returns 'http'
options.port # returns 443
ConnectionOptions.new(invalid: 10) # raises Sinclair::Exception::InvalidOptions
```
### Sinclair::Comparable
Comparable allows a class to implement quickly a `==` method comparing given attributes
Example of Comparable usage
```ruby
class SampleModel
include Sinclair::Comparable
comparable_by :name
attr_reader :name, :age
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 == model2 # returns true
```
### Sinclair::Model
Model class for quickly creation of plain simple classes/models
When creating a model class, options can be passed
- writter: Adds writter/setter methods (defaults to true)
- comparable: Adds the fields when running a `==` method (defaults to true)
Example of simple usage
```ruby
class Human < Sinclair::Model
initialize_with :name, :age, { gender: :undefined }, **{}
end
human1 = Human.new(name: 'John Doe', age: 22)
human2 = Human.new(name: 'John Doe', age: 22)
human1.name # returns 'John Doe'
human1.age # returns 22
human1.gender # returns :undefined
human1 == human2 # returns true
```
Example with options
```ruby
class Tv < Sinclair::Model.for(:model, writter: false, comparable: false)
end
tv1 = Tv.new(model: 'Sans Sunga Xt')
tv2 = Tv.new(model: 'Sans Sunga Xt')
tv1 == tv2 # returns false
```
RSspec matcher
---------------
You can use the provided matcher to check that your builder is adding a method correctly
Sample of specs over adding methods
```ruby
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
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
end
end
end
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 }
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
end
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
end
end
end
```
```bash
> bundle exec rspec
```
```string
Sinclair::Matchers
when the builder runs
should add method 'the_method' to # instances
when the builder runs
should add method 'the_method' to # instances
when adding class methods
when the builder runs
should add method class_method 'the_method' to #
```
Projects Using
---------------
- [Arstotzka](https://github.com/darthjee/arstotzka)
- [Azeroth](https://github.com/darthjee/azeroth)
- [Magicka](https://github.com/darthjee/magicka)