[
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: Ruby CI\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    branches: [ main ]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        ruby-version: ['3.4', '3.3', '3.2', '3.1', '3.0', '2.7']\n    steps:\n      - uses: actions/checkout@v3\n      - name: Set up Ruby ${{ matrix.ruby-version }}\n        uses: ruby/setup-ruby@v1\n        with:\n          ruby-version: ${{ matrix.ruby-version }}\n      - name: Setup dependencies\n        run: |\n          gem install bundler -v 2.4.22\n          sudo apt-get update --allow-releaseinfo-change\n          bundle config set --local path 'vendor/bundle'\n          bundle install\n      - name: Run tests\n        run: |\n          bundle exec rake test\n"
  },
  {
    "path": ".gitignore",
    "content": "Gemfile.lock\n.ruby-version\npkg/\n*.gem\ncoverage/\n.byebug_history\n\n"
  },
  {
    "path": "Gemfile",
    "content": "source \"https://rubygems.org\"\n\ngemspec\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "Copyright (c) 2021 Paweł Urbanek\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Smart Init - Simple service objects in Ruby [![Gem Version](https://badge.fury.io/rb/smart_init.svg)](https://badge.fury.io/rb/smart_init) [![GH Actions](https://github.com/pawurb/smart_init/actions/workflows/ci.yml/badge.svg)](https://github.com/pawurb/smart_init/actions)\n\nDo you find yourself writing a lot of boilerplate code like this?\n\n```ruby\ndef initialize(network_provider, api_token)\n  @network_provider = network_provider\n  @api_token = api_token\nend\n\ndef self.call(network_provider, api_token)\n  new(network_provider, api_token).call\nend\n```\n\nThis gem provides a simple DSL for getting rid of it. It offers an alternative to using `Struct.new` which does not check for number of parameters provided in initializer, exposes getters and instantiates unecessary class instances.\n\n**Smart Init** offers a unified API convention for stateless service objects, accepting values in initializer and exposing one public class method `call` which instantiates new objects and accepts arguments passed to initializer.\n\nCheck out [this blog post](https://pawelurbanek.com/2018/02/12/ruby-on-rails-service-objects-and-testing-in-isolation/) for my reasoning behind this approach to service object pattern.\n\n## Installation\n\nIn your Gemfile\n\n```ruby\ngem 'smart_init'\n```\n\n## API\n\nYou can use it either by extending a module:\n\n```ruby\nrequire 'smart_init'\n\nclass ApiClient\n  extend SmartInit\n\n  initialize_with :network_provider, :api_token\nend\n```\n\nor subclassing:\n\n```ruby\nclass ApiClient < SmartInit::Base\n  initialize_with :network_provider, :api_token\nend\n```\n\nNow you can just:\n\n```ruby\nobject = ApiClient.new(network_provider: Faraday.new, api_token: 'secret_token')\n# <ApiClient:0x007fa16684ec20 @network_provider=Faraday<...>, @api_token=\"secret_token\">\n```\n\nIf you omit a required attribute an `ArgumentError` will be thrown:\n\n```ruby\nclient = ApiClient.new(network_provider: Faraday.new)\n\n# ArgumentError (missing required attribute api_token)\n```\n\n### Making the object callable\n\nYou can use the `is_callable` method:\n\n```ruby\nclass Calculator < SmartInit::Base\n  initialize_with :data\n  is_callable\n\n  def call\n    ...\n    result\n  end\nend\n\nCalculator.call(data: data) => result\n```\n\nOptionally you can customize a callable method name:\n\n```ruby\nclass Routine < SmartInit::Base\n  initialize_with :params\n  is_callable method_name: :run!\n\n  def run!\n    ...\n  end\nend\n\nRoutine.run!(params: params)\n```\n\n### Default arguments\n\nYou can use hash based, default argument values:\n\n```ruby\nclass Adder < SmartInit::Base\n  initialize_with :num_a, num_b: 2\n  is_callable\n\n  def call\n    num_a + num_b\n  end\nend\n\nAdder.call(num_a: 2) => 4\nAdder.call(num_a: 2, num_b: 3) => 5\n```\n\n### Readers access\n\nContrary to using Struct, by default the reader methods are not publicly exposed:\n\n```ruby\nclient = ApiClient.new(network_provider: Faraday.new, api_token: 'secret_token')\nclient.api_token => # NoMethodError (private method `api_token' called for #<ApiClient:0x000..>)\n```\n\nOptionally you can make all or subset of readers public using the `public_readers` config option. It accepts `true` or an array of method names as an argument.\n\n```ruby\nclass PublicApiClient < SmartInit::Base\n  initialize_with :network_provider, :api_token, public_readers: true\nend\n\nclient = PublicApiClient.new(network_provider: Faraday.new, api_token: 'secret_token')\nclient.network_provider => #<Faraday::Connection:0x000...>\nclient.api_token => 'secret_token'\n```\n\n```ruby\nclass SemiPublicApiClient < SmartInit::Base\n  initialize_with :network_provider, :api_token, public_readers: [:network_provider]\nend\n\nclient = SemiPublicApiClient.new(network_provider: Faraday.new, api_token: 'secret_token')\nclient.network_provider => #<Faraday::Connection:0x000...>\nclient.api_token => 'secret_token' => # NoMethodError (private method `api_token' called for #<SemiPublicApiClient:0x000...>)\n```\n\n### Accessors access\n\nSimilarly, this is how it would look if you tried to use a writer method:\n\n```ruby\nclient = ApiClient.new(network_provider: Faraday.new, api_token: 'secret_token')\nclient.api_token = 'new_token' => # NoMethodError (private method `api_token=' called for #<ApiClient:0x000..>)\n```\n\nOptionally you can make all or subset of accessors public using the `public_accessors` config option. It accepts `true` or an array of method names as an argument. This will provide both reader and writer methods publicly.\n\n```ruby\nclass PublicApiClient < SmartInit::Base\n  initialize_with :network_provider, :api_token, public_accessors: true\nend\n\nclient = PublicApiClient.new(network_provider: Faraday.new, api_token: 'secret_token')\nclient.network_provider => #<Faraday::Connection:0x000...>\nclient.network_provider = Typhoeus::Request.new(...) => #<Typhoeus::Request:0x000...>\nclient.api_token => 'secret_token'\nclient.api_token = 'new_token' => 'new_token'\n```\n\n```ruby\nclass SemiPublicApiClient < SmartInit::Base\n  initialize_with :network_provider, :api_token, public_accessors: [:network_provider]\nend\n\nclient = SemiPublicApiClient.new(network_provider: Faraday.new, api_token: 'secret_token')\nclient.network_provider => #<Faraday::Connection:0x000...>\nclient.network_provider = Typhoeus::Request.new(...) => #<Typhoeus::Request:0x000...>\nclient.api_token => # NoMethodError (private method `api_token' called for #<SemiPublicApiClient:0x000...>)\nclient.api_token = 'new_token' => # NoMethodError (undefined method `api_token=' called for #<SemiPublicApiClient:0x000...>)\n```\n\nFinally, you can mix them together like this:\n\n```ruby\nclass PublicReadersSemiPublicAccessorsApiClient < SmartInit::Base\n  initialize_with :network_provider, :api_token, :timeout,\n                  public_readers: true, public_accessors: [:network_provider]\nend\n\nclient = PublicReadersSemiPublicAccessorsApiClient.new(\n           network_provider: Faraday.new, api_token: 'secret_token', timeout_length: 100\n         )\nclient.network_provider => #<Faraday::Connection:0x000...>\nclient.network_provider = Typhoeus::Request.new(...) => #<Typhoeus::Request:0x000...>\nclient.api_token => 'secret_token'\nclient.api_token = 'new_token' => # NoMethodError (undefined method `api_token=' called for #<SemiPublicApiClient:0x000...>)\nclient.timeout_length => 100\nclient.timeout_length = 150 => # NoMethodError (undefined method `timeout_length=' called for #<SemiPublicApiClient:0x000...>)\n```\n\n```ruby\nclass SemiPublicReadersSemiPublicAccessorsApiClient < SmartInit::Base\n  initialize_with :network_provider, :api_token, :timeout,\n                  public_readers: [:timeout], public_accessors: [:network_provider]\nend\n\nclient = SemiPublicReadersSemiPublicAccessorsApiClient.new(\n           network_provider: Faraday.new, api_token: 'secret_token', timeout_length: 100\n         )\nclient.network_provider => #<Faraday::Connection:0x000...>\nclient.network_provider = Typhoeus::Request.new(...) => #<Typhoeus::Request:0x000...>\nclient.api_token => # NoMethodError (private method `api_token' called for #<SemiPublicReadersSemiPublicAccessorsApiClient:0x000...>)\nclient.api_token = 'new_token' => # NoMethodError (undefined method `api_token=' called for #<SemiPublicReadersSemiPublicAccessorsApiClient:0x000...>)\nclient.timeout_length => 100\nclient.timeout_length = 150 => # NoMethodError (undefined method `timeout_length=' called for #<SemiPublicReadersSemiPublicAccessorsApiClient:0x000...>)\n```\n"
  },
  {
    "path": "Rakefile",
    "content": "require \"bundler/gem_tasks\"\nrequire \"rake/testtask\"\n\nRake::TestTask.new do |t|\n  t.libs << \"test\"\nend\n\ndesc \"Run tests\"\ntask :default => :test\n"
  },
  {
    "path": "lib/smart_init/main.rb",
    "content": "# frozen_string_literal: true\n\nmodule SmartInit\n  def is_callable(opts = {})\n    method_name = if name_from_opts = opts[:method_name]\n        name_from_opts.to_sym\n      else\n        :call\n      end\n\n    define_singleton_method method_name do |**parameters|\n      new(**parameters).public_send(method_name)\n    end\n  end\n\n  def initialize_with_hash(*required_attrs, **default_values_and_opts)\n    public_readers = default_values_and_opts.delete(:public_readers) || []\n    public_accessors = default_values_and_opts.delete(:public_accessors) || []\n    if public_readers == true || public_accessors == true\n      public_readers = required_attrs + default_values_and_opts.keys\n      public_accessors = required_attrs + default_values_and_opts.keys if public_accessors == true\n    else\n      public_readers += public_accessors\n    end\n\n    define_method :initialize do |**parameters|\n      required_attrs.each do |required_attr|\n        unless parameters.has_key?(required_attr)\n          raise ArgumentError, \"missing required attribute #{required_attr}\"\n        end\n      end\n\n      parameters.keys.each do |param|\n        if !(required_attrs + [:public_readers, :public_accessors]).include?(param) && !default_values_and_opts.keys.include?(param)\n          raise ArgumentError, \"invalid attribute '#{param}'\"\n        end\n      end\n\n      (required_attrs + default_values_and_opts.keys).each do |attribute|\n        value = if parameters.has_key?(attribute)\n            parameters.fetch(attribute)\n          else\n            default_values_and_opts[attribute]\n          end\n\n        instance_variable_set(\"@#{attribute}\", value)\n      end\n    end\n\n    instance_eval do\n      all_readers = (required_attrs + default_values_and_opts.keys)\n      attr_reader(*all_readers)\n      (all_readers - public_readers).each do |reader|\n        private reader\n      end\n      attr_writer(*public_accessors)\n    end\n  end\n\n  alias initialize_with initialize_with_hash\nend\n\nclass SmartInit::Base\n  extend SmartInit\nend\n"
  },
  {
    "path": "lib/smart_init/version.rb",
    "content": "# frozen_string_literal: true\n\nmodule SmartInit\n  VERSION = \"5.1.0\"\nend\n"
  },
  {
    "path": "lib/smart_init.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"smart_init/main\"\n\nmodule SmartInit\nend\n"
  },
  {
    "path": "smart_init.gemspec",
    "content": "# -*- encoding: utf-8 -*-\nlib = File.expand_path(\"../lib\", __FILE__)\n$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)\nrequire \"smart_init/version\"\n\nGem::Specification.new do |s|\n  s.name = \"smart_init\"\n  s.version = SmartInit::VERSION\n  s.authors = [\"pawurb\"]\n  s.email = [\"p.urbanek89@gmail.com\"]\n  s.summary = %q{ Remove Ruby initializer boilerplate code }\n  s.description = %q{ A smart DSL for ruby initializers boilerplate }\n  s.homepage = \"http://github.com/pawurb/smart_init\"\n  s.files = `git ls-files`.split(\"\\n\")\n  s.test_files = s.files.grep(%r{^(test)/})\n  s.require_paths = [\"lib\"]\n  s.license = \"MIT\"\n  s.add_development_dependency \"rake\"\n  s.add_development_dependency \"byebug\"\n  s.add_development_dependency \"dbg-rb\"\n  s.add_development_dependency \"test-unit\"\n  s.add_development_dependency \"rufo\"\n\n  if s.respond_to?(:metadata=)\n    s.metadata = { \"rubygems_mfa_required\" => \"true\" }\n  end\nend\n"
  },
  {
    "path": "test/test_hash_api.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"test/unit\"\nrequire_relative \"../lib/smart_init/main\"\n\nclass TestService\n  extend SmartInit\n  initialize_with :attribute_1, :attribute_2\n  is_callable\n\n  def call\n    [attribute_1, attribute_2]\n  end\nend\n\nclass TestServiceDefaults\n  extend SmartInit\n  initialize_with :attribute_1, attribute_2: \"default_value_2\", attribute_3: \"default_value_3\"\n  is_callable\n\n  def call\n    [attribute_1, attribute_2, attribute_3]\n  end\nend\n\nclass TestServiceAllDefaults\n  extend SmartInit\n  initialize_with attribute_1: \"default_value_1\", attribute_2: \"default_value_2\", attribute_3: \"default_value_3\"\n  is_callable\n\n  def call\n    [attribute_1, attribute_2, attribute_3]\n  end\nend\n\nclass TestHashIntegerDefaults\n  extend SmartInit\n  initialize_with :attribute_1, attribute_2: 2\n  is_callable\n\n  def call\n    [attribute_1, attribute_2]\n  end\nend\n\nclass HashApiTest < Test::Unit::TestCase\n  def test_keywords\n    assert_equal TestService.call(attribute_1: \"a\", attribute_2: \"b\"), [\"a\", \"b\"]\n\n    assert_raise ArgumentError do\n      TestService.new(\n        attribute_1: \"a\",\n      )\n    end\n  end\n\n  def test_keywords_defaults\n    assert_equal TestServiceDefaults.call(attribute_1: \"a\"), [\"a\", \"default_value_2\", \"default_value_3\"]\n    assert_equal TestServiceDefaults.call(attribute_1: \"a\", attribute_2: \"b\"), [\"a\", \"b\", \"default_value_3\"]\n  end\n\n  def test_private_readers\n    service = TestServiceDefaults.new(attribute_1: \"a\")\n    error = assert_raise NoMethodError do\n      service.attribute_2\n    end\n\n    assert_match(\"private method\", error.message)\n  end\n\n  def test_integer_defaults\n    assert_equal TestHashIntegerDefaults.call(attribute_1: 1), [1, 2]\n  end\n\n  def test_missing_attributes\n    assert_raise ArgumentError do\n      TestService.call(attribute_1: \"a\", invalid_attribute: \"b\")\n    end\n  end\n\n  def test_invalid_input\n    assert_raise ArgumentError do\n      TestService.call(\"invalid_input here\")\n    end\n  end\n\n  def test_all_defaults\n    assert_equal TestServiceAllDefaults.call, [\"default_value_1\", \"default_value_2\", \"default_value_3\"]\n  end\n\n  def test_falsey_values\n    assert_equal TestService.call(attribute_1: false, attribute_2: nil), [false, nil]\n  end\n\n  def test_invalid_keywords\n    assert_raise ArgumentError do\n      TestService.call(attribute_1: \"a\", attribute_2: \"b\", invalid_attribute: \"c\")\n    end\n  end\nend\n"
  },
  {
    "path": "test/test_hash_public_accessors.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"test/unit\"\nrequire_relative \"../lib/smart_init/main\"\n\nclass TestAllPublicAccessors\n  extend SmartInit\n  initialize_with :attribute_1, :attribute_2, public_accessors: true\n  is_callable\n\n  def call\n    [attribute_1, attribute_2]\n  end\nend\n\nclass TestSomePublicAccessors\n  extend SmartInit\n  initialize_with :attribute_1, :attribute_2, public_accessors: [:attribute_1]\n  is_callable\n\n  def call\n    [attribute_1, attribute_2]\n  end\nend\n\nclass TestDefaultPublicAccessors\n  extend SmartInit\n  initialize_with :attribute_1, attribute_2: 2, public_accessors: [:attribute_2]\n\n  def call\n    [attribute_1, attribute_2]\n  end\nend\n\nclass HashApiPublicTestAccessors < Test::Unit::TestCase\n  def test_all_public\n    service = TestAllPublicAccessors.new(attribute_1: \"a\", attribute_2: \"b\")\n    assert_nothing_raised do\n      service.attribute_1 = \"c\"\n      service.attribute_2 = \"d\"\n    end\n    assert_equal service.attribute_1, \"c\"\n    assert_equal service.attribute_2, \"d\"\n  end\n\n  def test_some_public\n    service = TestSomePublicAccessors.new(attribute_1: \"a\", attribute_2: \"b\")\n    assert_nothing_raised do\n      service.attribute_1 = \"c\"\n    end\n    assert_equal service.attribute_1, \"c\"\n    assert_raise NoMethodError do\n      service.attribute_2\n    end\n    assert_raise NoMethodError do\n      service.attribute_2 = \"d\"\n    end\n  end\n\n  def test_default_public\n    service = TestDefaultPublicAccessors.new(attribute_1: \"a\")\n    assert_nothing_raised do\n      service.attribute_2 = 3\n    end\n    assert_equal service.attribute_2, 3\n    assert_raise NoMethodError do\n      service.attribute_1 = \"b\"\n    end\n    assert_raise NoMethodError do\n      service.attribute_1\n    end\n  end\nend\n"
  },
  {
    "path": "test/test_hash_public_mixed.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"test/unit\"\nrequire_relative \"../lib/smart_init/main\"\n\nclass TestSomePublicMixed\n  extend SmartInit\n  initialize_with :attribute_1, :attribute_2, :attribute_3, :attribute_4,\n                  public_readers: [:attribute_1],\n                  public_accessors: [:attribute_2, :attribute_3]\n  is_callable\n\n  def call\n    [attribute_1, attribute_2, attribute_3, attribute_4]\n  end\nend\n\nclass TestAllReadersSomeAccessorsPublic\n  extend SmartInit\n  initialize_with :attribute_1, :attribute_2, public_readers: true, public_accessors: [:attribute_2]\n\n  def call\n    [attribute_1, attribute_2]\n  end\nend\n\nclass HashApiPublicTest < Test::Unit::TestCase\n  def test_readers_some_public_mixed\n    service = TestSomePublicMixed.new(\n      attribute_1: \"a\", attribute_2: \"b\",\n      attribute_3: \"c\", attribute_4: \"d\",\n    )\n    assert_nothing_raised do\n      service.attribute_1\n      service.attribute_2\n      service.attribute_3\n    end\n    assert_raise NoMethodError do\n      service.attribute_4\n    end\n  end\n\n  def test_writers_some_public_mixed\n    service = TestSomePublicMixed.new(\n      attribute_1: \"a\", attribute_2: \"b\",\n      attribute_3: \"c\", attribute_4: \"d\",\n    )\n    assert_nothing_raised do\n      service.attribute_2 = \"e\"\n      service.attribute_3 = \"f\"\n    end\n    assert_equal service.attribute_2, \"e\"\n    assert_equal service.attribute_3, \"f\"\n    assert_raise NoMethodError do\n      service.attribute_4 = \"g\"\n    end\n  end\n\n  def test_readers_all_readers_some_accessors_public\n    service = TestAllReadersSomeAccessorsPublic.new(\n      attribute_1: \"a\", attribute_2: \"b\",\n    )\n    assert_nothing_raised do\n      service.attribute_1\n      service.attribute_2\n    end\n  end\n\n  def test_writers_all_readers_some_accessors_public\n    service = TestAllReadersSomeAccessorsPublic.new(\n      attribute_1: \"a\", attribute_2: \"b\",\n    )\n    assert_raise NoMethodError do\n      service.attribute_1 = \"c\"\n    end\n    assert_nothing_raised do\n      service.attribute_2 = \"d\"\n    end\n    assert_equal service.attribute_2, \"d\"\n  end\nend\n"
  },
  {
    "path": "test/test_hash_public_readers.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"test/unit\"\nrequire_relative \"../lib/smart_init/main\"\n\nclass TestAllPublic\n  extend SmartInit\n  initialize_with :attribute_1, :attribute_2, public_readers: true\n  is_callable\n\n  def call\n    [attribute_1, attribute_2]\n  end\nend\n\nclass TestSomePublic\n  extend SmartInit\n  initialize_with :attribute_1, :attribute_2, public_readers: [:attribute_1]\n  is_callable\n\n  def call\n    [attribute_1, attribute_2]\n  end\nend\n\nclass TestDefaultPublic\n  extend SmartInit\n  initialize_with :attribute_1, attribute_2: 2, public_readers: [:attribute_2]\n\n  def call\n    [attribute_1, attribute_2]\n  end\nend\n\nclass TestDefaultAllPublic\n  extend SmartInit\n  initialize_with :attribute_1, attribute_2: 2, public_readers: true\n\n  def call\n    [attribute_1, attribute_2]\n  end\nend\n\nclass HashApiPublicTest < Test::Unit::TestCase\n  def test_all_public\n    service = TestAllPublic.new(attribute_1: \"a\", attribute_2: \"b\")\n    assert_equal service.attribute_1, \"a\"\n    assert_equal service.attribute_2, \"b\"\n  end\n\n  def test_some_public\n    service = TestSomePublic.new(attribute_1: \"a\", attribute_2: \"b\")\n    assert_equal service.attribute_1, \"a\"\n    assert_raise NoMethodError do\n      service.attribute_2\n    end\n  end\n\n  def test_default_public\n    service = TestDefaultPublic.new(attribute_1: \"a\")\n    assert_equal service.attribute_2, 2\n\n    assert_raise NoMethodError do\n      service.attribute_1\n    end\n  end\n\n  def test_default_all_public\n    service = TestDefaultAllPublic.new(attribute_1: \"a\")\n    assert_equal service.attribute_1, \"a\"\n    assert_equal service.attribute_2, 2\n  end\nend\n"
  }
]