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) 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://github.com/darthjee/sinclair/tree/2.1.1) [Next release](https://github.com/darthjee/sinclair/compare/2.1.1...master) Yard Documentation ------------------- [https://www.rubydoc.info/gems/sinclair/2.1.1](https://www.rubydoc.info/gems/sinclair/2.1.1) 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 # 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 ``` ```ruby # 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 ``` ```ruby 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 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](#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 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 ```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 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 ```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 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 ```ruby # spec_helper.rb RSpec.configure do |config| config.include Sinclair::Matchers end ``` ```ruby # 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 ``` ```ruby # 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 ``` ```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)