Project

sinclair

0.0
No release in over a year
Gem for easy concern creation
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

>= 2.3.25
= 13.0.1
= 0.80.1
= 0.9.9
= 0.14.1
= 1.0.0
= 6.0.3
= 3.11.0
= 3.11.0
= 3.11.1
= 4.7.0
= 0.21.2
= 0.9.27

Runtime

 Project Readme

Sinclair

Code Climate Test Coverage Issue Count Gem Version Codacy Badge Inline docs

sinclair

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

Next release

Yard Documentation

https://www.rubydoc.info/gems/sinclair/2.1.1

Installation

  • Install it
  gem install sinclair
  • Or add Sinclair to your Gemfile and bundle install:
  gem 'sinclair'
  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
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

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: 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

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
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'

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
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'

Sinclair::EnvSettable

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">'

Sinclair::Options

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

Sinclair::Comparable

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

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
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

RSspec matcher

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>

Projects Using