Sinclair is a Ruby gem that provides developers with a variety of utility modules and classes to simplify common tasks, reusability and avoid boilerplate code. Whether you need to class methods to create methods on the fly, create custom comparators, configure your application, create powerfull options, Sinclair has got you covered.
Employing Sinclair in your applications helps you streamline your development workflow and enhance your development process through more efficient, cleaner code
Current Release: 2.1.1
https://www.rubydoc.info/gems/sinclair/2.1.1
- Install it
gem install sinclair
- Or add Sinclair to your
Gemfile
andbundle install
:
gem 'sinclair'
bundle install sinclair
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
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
# http_json_model.rb
class HttpJsonModel
attr_reader :json
class << self
def parse(attribute, path: [])
keys = (path + [attribute]).map(&:to_s)
Sinclair.build(self) do
add_method(attribute) do
keys.inject(hash) { |h, key| h[key] }
end
end
end
end
def initialize(json)
@json = json
end
def hash
@hash ||= JSON.parse(json)
end
end
# http_person.rb
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
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
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?
.
valid_object = MyClass.new(
name: :name,
surname: 'surname',
age: 20,
legs: 2
)
valid_object.valid? # returns true
invalid_object = MyClass.new(
name: 'name',
surname: 'surname',
age: 20,
legs: 2
)
invalid_object.valid? # returns false
There are different ways to add a method, each accepting different options
Define method using block
Block methods accepts, as option
- cache: defining the cashing of results
klass = Class.new
instance = klass.new
Sinclair.build(klass) do
add_method(:random_number) { Random.rand(10..20) }
end
instance.random_number # returns a number between 10 and 20
Define method using string
String methods accepts, as option
- cache: defining the cashing of results
- parameters: defining accepted parameters
- named_parameters: defining accepted named parameters
# 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
# Example with parameters
class MyClass
end
Sinclair.build(MyClass) do
add_class_method(
:function, 'a ** b + c', parameters: [:a], named_parameters: [:b, { c: 15 }]
)
end
MyClass.function(10, b: 2) # returns 115
Define method using a call to the class
Call method definitions right now have no options available
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
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
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
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'
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
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
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'
EnvSettable is a convenient utility that allows you to read environment variables using Ruby class methods.
With this tool, you can define the usage of environment variables for your application in a single location allowing the use of prefixes to isolate groups of variables.
This not only makes your code more readable and maintainable but also adds layer of security by ensuring that sensitive information like API keys and passwords are not exposed in your source code.
EnvSettable allows accessing those variables thorugh a simple meta-programable way
Using env settable example
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 #<ServiceClient:0x0000556fa1b366e8 @username="my-login", @password=nil, @port=80, @hostname="host.com">'
Options allows projects to have an easy to configure option object
Example of using Options
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
Comparable allows a class to implement quickly a ==
method comparing given attributes
Example of Comparable usage
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
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
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
class Tv < Sinclair::Model
initialize_with :model, writter: false, comparable: false
end
tv1 = Tv.new(model: 'Sans Sunga Xt')
tv2 = Tv.new(model: 'Sans Sunga Xt')
tv1 == tv2 # returns false
You can use the provided matcher to check that your builder is adding a method correctly
Sample of specs over adding methods
# spec_helper.rb
RSpec.configure do |config|
config.include Sinclair::Matchers
end
# default_value.rb
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
# default_value_spec.rb
RSpec.describe DefaultValue 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
> bundle exec rspec
Sinclair::Matchers
when the builder runs
should add method 'the_method' to #<Class:0x000055e5d9b7f150> instances
when the builder runs
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>