├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── lib ├── smart_init.rb └── smart_init │ ├── main.rb │ └── version.rb ├── smart_init.gemspec └── test ├── test_hash_api.rb ├── test_hash_public_accessors.rb ├── test_hash_public_mixed.rb └── test_hash_public_readers.rb /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Ruby CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | ruby-version: ['3.4', '3.3', '3.2', '3.1', '3.0', '2.7'] 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Set up Ruby ${{ matrix.ruby-version }} 19 | uses: ruby/setup-ruby@v1 20 | with: 21 | ruby-version: ${{ matrix.ruby-version }} 22 | - name: Setup dependencies 23 | run: | 24 | gem install bundler -v 2.4.22 25 | sudo apt-get update --allow-releaseinfo-change 26 | bundle config set --local path 'vendor/bundle' 27 | bundle install 28 | - name: Run tests 29 | run: | 30 | bundle exec rake test 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | .ruby-version 3 | pkg/ 4 | *.gem 5 | coverage/ 6 | .byebug_history 7 | 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Paweł Urbanek 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 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) 2 | 3 | Do you find yourself writing a lot of boilerplate code like this? 4 | 5 | ```ruby 6 | def initialize(network_provider, api_token) 7 | @network_provider = network_provider 8 | @api_token = api_token 9 | end 10 | 11 | def self.call(network_provider, api_token) 12 | new(network_provider, api_token).call 13 | end 14 | ``` 15 | 16 | This 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. 17 | 18 | **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. 19 | 20 | Check 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. 21 | 22 | ## Installation 23 | 24 | In your Gemfile 25 | 26 | ```ruby 27 | gem 'smart_init' 28 | ``` 29 | 30 | ## API 31 | 32 | You can use it either by extending a module: 33 | 34 | ```ruby 35 | require 'smart_init' 36 | 37 | class ApiClient 38 | extend SmartInit 39 | 40 | initialize_with :network_provider, :api_token 41 | end 42 | ``` 43 | 44 | or subclassing: 45 | 46 | ```ruby 47 | class ApiClient < SmartInit::Base 48 | initialize_with :network_provider, :api_token 49 | end 50 | ``` 51 | 52 | Now you can just: 53 | 54 | ```ruby 55 | object = ApiClient.new(network_provider: Faraday.new, api_token: 'secret_token') 56 | # , @api_token="secret_token"> 57 | ``` 58 | 59 | If you omit a required attribute an `ArgumentError` will be thrown: 60 | 61 | ```ruby 62 | client = ApiClient.new(network_provider: Faraday.new) 63 | 64 | # ArgumentError (missing required attribute api_token) 65 | ``` 66 | 67 | ### Making the object callable 68 | 69 | You can use the `is_callable` method: 70 | 71 | ```ruby 72 | class Calculator < SmartInit::Base 73 | initialize_with :data 74 | is_callable 75 | 76 | def call 77 | ... 78 | result 79 | end 80 | end 81 | 82 | Calculator.call(data: data) => result 83 | ``` 84 | 85 | Optionally you can customize a callable method name: 86 | 87 | ```ruby 88 | class Routine < SmartInit::Base 89 | initialize_with :params 90 | is_callable method_name: :run! 91 | 92 | def run! 93 | ... 94 | end 95 | end 96 | 97 | Routine.run!(params: params) 98 | ``` 99 | 100 | ### Default arguments 101 | 102 | You can use hash based, default argument values: 103 | 104 | ```ruby 105 | class Adder < SmartInit::Base 106 | initialize_with :num_a, num_b: 2 107 | is_callable 108 | 109 | def call 110 | num_a + num_b 111 | end 112 | end 113 | 114 | Adder.call(num_a: 2) => 4 115 | Adder.call(num_a: 2, num_b: 3) => 5 116 | ``` 117 | 118 | ### Readers access 119 | 120 | Contrary to using Struct, by default the reader methods are not publicly exposed: 121 | 122 | ```ruby 123 | client = ApiClient.new(network_provider: Faraday.new, api_token: 'secret_token') 124 | client.api_token => # NoMethodError (private method `api_token' called for #) 125 | ``` 126 | 127 | Optionally 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. 128 | 129 | ```ruby 130 | class PublicApiClient < SmartInit::Base 131 | initialize_with :network_provider, :api_token, public_readers: true 132 | end 133 | 134 | client = PublicApiClient.new(network_provider: Faraday.new, api_token: 'secret_token') 135 | client.network_provider => # 136 | client.api_token => 'secret_token' 137 | ``` 138 | 139 | ```ruby 140 | class SemiPublicApiClient < SmartInit::Base 141 | initialize_with :network_provider, :api_token, public_readers: [:network_provider] 142 | end 143 | 144 | client = SemiPublicApiClient.new(network_provider: Faraday.new, api_token: 'secret_token') 145 | client.network_provider => # 146 | client.api_token => 'secret_token' => # NoMethodError (private method `api_token' called for #) 147 | ``` 148 | 149 | ### Accessors access 150 | 151 | Similarly, this is how it would look if you tried to use a writer method: 152 | 153 | ```ruby 154 | client = ApiClient.new(network_provider: Faraday.new, api_token: 'secret_token') 155 | client.api_token = 'new_token' => # NoMethodError (private method `api_token=' called for #) 156 | ``` 157 | 158 | Optionally 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. 159 | 160 | ```ruby 161 | class PublicApiClient < SmartInit::Base 162 | initialize_with :network_provider, :api_token, public_accessors: true 163 | end 164 | 165 | client = PublicApiClient.new(network_provider: Faraday.new, api_token: 'secret_token') 166 | client.network_provider => # 167 | client.network_provider = Typhoeus::Request.new(...) => # 168 | client.api_token => 'secret_token' 169 | client.api_token = 'new_token' => 'new_token' 170 | ``` 171 | 172 | ```ruby 173 | class SemiPublicApiClient < SmartInit::Base 174 | initialize_with :network_provider, :api_token, public_accessors: [:network_provider] 175 | end 176 | 177 | client = SemiPublicApiClient.new(network_provider: Faraday.new, api_token: 'secret_token') 178 | client.network_provider => # 179 | client.network_provider = Typhoeus::Request.new(...) => # 180 | client.api_token => # NoMethodError (private method `api_token' called for #) 181 | client.api_token = 'new_token' => # NoMethodError (undefined method `api_token=' called for #) 182 | ``` 183 | 184 | Finally, you can mix them together like this: 185 | 186 | ```ruby 187 | class PublicReadersSemiPublicAccessorsApiClient < SmartInit::Base 188 | initialize_with :network_provider, :api_token, :timeout, 189 | public_readers: true, public_accessors: [:network_provider] 190 | end 191 | 192 | client = PublicReadersSemiPublicAccessorsApiClient.new( 193 | network_provider: Faraday.new, api_token: 'secret_token', timeout_length: 100 194 | ) 195 | client.network_provider => # 196 | client.network_provider = Typhoeus::Request.new(...) => # 197 | client.api_token => 'secret_token' 198 | client.api_token = 'new_token' => # NoMethodError (undefined method `api_token=' called for #) 199 | client.timeout_length => 100 200 | client.timeout_length = 150 => # NoMethodError (undefined method `timeout_length=' called for #) 201 | ``` 202 | 203 | ```ruby 204 | class SemiPublicReadersSemiPublicAccessorsApiClient < SmartInit::Base 205 | initialize_with :network_provider, :api_token, :timeout, 206 | public_readers: [:timeout], public_accessors: [:network_provider] 207 | end 208 | 209 | client = SemiPublicReadersSemiPublicAccessorsApiClient.new( 210 | network_provider: Faraday.new, api_token: 'secret_token', timeout_length: 100 211 | ) 212 | client.network_provider => # 213 | client.network_provider = Typhoeus::Request.new(...) => # 214 | client.api_token => # NoMethodError (private method `api_token' called for #) 215 | client.api_token = 'new_token' => # NoMethodError (undefined method `api_token=' called for #) 216 | client.timeout_length => 100 217 | client.timeout_length = 150 => # NoMethodError (undefined method `timeout_length=' called for #) 218 | ``` 219 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new do |t| 5 | t.libs << "test" 6 | end 7 | 8 | desc "Run tests" 9 | task :default => :test 10 | -------------------------------------------------------------------------------- /lib/smart_init.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "smart_init/main" 4 | 5 | module SmartInit 6 | end 7 | -------------------------------------------------------------------------------- /lib/smart_init/main.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SmartInit 4 | def is_callable(opts = {}) 5 | method_name = if name_from_opts = opts[:method_name] 6 | name_from_opts.to_sym 7 | else 8 | :call 9 | end 10 | 11 | define_singleton_method method_name do |**parameters| 12 | new(**parameters).public_send(method_name) 13 | end 14 | end 15 | 16 | def initialize_with_hash(*required_attrs, **default_values_and_opts) 17 | public_readers = default_values_and_opts.delete(:public_readers) || [] 18 | public_accessors = default_values_and_opts.delete(:public_accessors) || [] 19 | if public_readers == true || public_accessors == true 20 | public_readers = required_attrs + default_values_and_opts.keys 21 | public_accessors = required_attrs + default_values_and_opts.keys if public_accessors == true 22 | else 23 | public_readers += public_accessors 24 | end 25 | 26 | define_method :initialize do |**parameters| 27 | required_attrs.each do |required_attr| 28 | unless parameters.has_key?(required_attr) 29 | raise ArgumentError, "missing required attribute #{required_attr}" 30 | end 31 | end 32 | 33 | parameters.keys.each do |param| 34 | if !(required_attrs + [:public_readers, :public_accessors]).include?(param) && !default_values_and_opts.keys.include?(param) 35 | raise ArgumentError, "invalid attribute '#{param}'" 36 | end 37 | end 38 | 39 | (required_attrs + default_values_and_opts.keys).each do |attribute| 40 | value = if parameters.has_key?(attribute) 41 | parameters.fetch(attribute) 42 | else 43 | default_values_and_opts[attribute] 44 | end 45 | 46 | instance_variable_set("@#{attribute}", value) 47 | end 48 | end 49 | 50 | instance_eval do 51 | all_readers = (required_attrs + default_values_and_opts.keys) 52 | attr_reader(*all_readers) 53 | (all_readers - public_readers).each do |reader| 54 | private reader 55 | end 56 | attr_writer(*public_accessors) 57 | end 58 | end 59 | 60 | alias initialize_with initialize_with_hash 61 | end 62 | 63 | class SmartInit::Base 64 | extend SmartInit 65 | end 66 | -------------------------------------------------------------------------------- /lib/smart_init/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SmartInit 4 | VERSION = "5.1.0" 5 | end 6 | -------------------------------------------------------------------------------- /smart_init.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "smart_init/version" 5 | 6 | Gem::Specification.new do |s| 7 | s.name = "smart_init" 8 | s.version = SmartInit::VERSION 9 | s.authors = ["pawurb"] 10 | s.email = ["p.urbanek89@gmail.com"] 11 | s.summary = %q{ Remove Ruby initializer boilerplate code } 12 | s.description = %q{ A smart DSL for ruby initializers boilerplate } 13 | s.homepage = "http://github.com/pawurb/smart_init" 14 | s.files = `git ls-files`.split("\n") 15 | s.test_files = s.files.grep(%r{^(test)/}) 16 | s.require_paths = ["lib"] 17 | s.license = "MIT" 18 | s.add_development_dependency "rake" 19 | s.add_development_dependency "byebug" 20 | s.add_development_dependency "dbg-rb" 21 | s.add_development_dependency "test-unit" 22 | s.add_development_dependency "rufo" 23 | 24 | if s.respond_to?(:metadata=) 25 | s.metadata = { "rubygems_mfa_required" => "true" } 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/test_hash_api.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test/unit" 4 | require_relative "../lib/smart_init/main" 5 | 6 | class TestService 7 | extend SmartInit 8 | initialize_with :attribute_1, :attribute_2 9 | is_callable 10 | 11 | def call 12 | [attribute_1, attribute_2] 13 | end 14 | end 15 | 16 | class TestServiceDefaults 17 | extend SmartInit 18 | initialize_with :attribute_1, attribute_2: "default_value_2", attribute_3: "default_value_3" 19 | is_callable 20 | 21 | def call 22 | [attribute_1, attribute_2, attribute_3] 23 | end 24 | end 25 | 26 | class TestServiceAllDefaults 27 | extend SmartInit 28 | initialize_with attribute_1: "default_value_1", attribute_2: "default_value_2", attribute_3: "default_value_3" 29 | is_callable 30 | 31 | def call 32 | [attribute_1, attribute_2, attribute_3] 33 | end 34 | end 35 | 36 | class TestHashIntegerDefaults 37 | extend SmartInit 38 | initialize_with :attribute_1, attribute_2: 2 39 | is_callable 40 | 41 | def call 42 | [attribute_1, attribute_2] 43 | end 44 | end 45 | 46 | class HashApiTest < Test::Unit::TestCase 47 | def test_keywords 48 | assert_equal TestService.call(attribute_1: "a", attribute_2: "b"), ["a", "b"] 49 | 50 | assert_raise ArgumentError do 51 | TestService.new( 52 | attribute_1: "a", 53 | ) 54 | end 55 | end 56 | 57 | def test_keywords_defaults 58 | assert_equal TestServiceDefaults.call(attribute_1: "a"), ["a", "default_value_2", "default_value_3"] 59 | assert_equal TestServiceDefaults.call(attribute_1: "a", attribute_2: "b"), ["a", "b", "default_value_3"] 60 | end 61 | 62 | def test_private_readers 63 | service = TestServiceDefaults.new(attribute_1: "a") 64 | error = assert_raise NoMethodError do 65 | service.attribute_2 66 | end 67 | 68 | assert_match("private method", error.message) 69 | end 70 | 71 | def test_integer_defaults 72 | assert_equal TestHashIntegerDefaults.call(attribute_1: 1), [1, 2] 73 | end 74 | 75 | def test_missing_attributes 76 | assert_raise ArgumentError do 77 | TestService.call(attribute_1: "a", invalid_attribute: "b") 78 | end 79 | end 80 | 81 | def test_invalid_input 82 | assert_raise ArgumentError do 83 | TestService.call("invalid_input here") 84 | end 85 | end 86 | 87 | def test_all_defaults 88 | assert_equal TestServiceAllDefaults.call, ["default_value_1", "default_value_2", "default_value_3"] 89 | end 90 | 91 | def test_falsey_values 92 | assert_equal TestService.call(attribute_1: false, attribute_2: nil), [false, nil] 93 | end 94 | 95 | def test_invalid_keywords 96 | assert_raise ArgumentError do 97 | TestService.call(attribute_1: "a", attribute_2: "b", invalid_attribute: "c") 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /test/test_hash_public_accessors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test/unit" 4 | require_relative "../lib/smart_init/main" 5 | 6 | class TestAllPublicAccessors 7 | extend SmartInit 8 | initialize_with :attribute_1, :attribute_2, public_accessors: true 9 | is_callable 10 | 11 | def call 12 | [attribute_1, attribute_2] 13 | end 14 | end 15 | 16 | class TestSomePublicAccessors 17 | extend SmartInit 18 | initialize_with :attribute_1, :attribute_2, public_accessors: [:attribute_1] 19 | is_callable 20 | 21 | def call 22 | [attribute_1, attribute_2] 23 | end 24 | end 25 | 26 | class TestDefaultPublicAccessors 27 | extend SmartInit 28 | initialize_with :attribute_1, attribute_2: 2, public_accessors: [:attribute_2] 29 | 30 | def call 31 | [attribute_1, attribute_2] 32 | end 33 | end 34 | 35 | class HashApiPublicTestAccessors < Test::Unit::TestCase 36 | def test_all_public 37 | service = TestAllPublicAccessors.new(attribute_1: "a", attribute_2: "b") 38 | assert_nothing_raised do 39 | service.attribute_1 = "c" 40 | service.attribute_2 = "d" 41 | end 42 | assert_equal service.attribute_1, "c" 43 | assert_equal service.attribute_2, "d" 44 | end 45 | 46 | def test_some_public 47 | service = TestSomePublicAccessors.new(attribute_1: "a", attribute_2: "b") 48 | assert_nothing_raised do 49 | service.attribute_1 = "c" 50 | end 51 | assert_equal service.attribute_1, "c" 52 | assert_raise NoMethodError do 53 | service.attribute_2 54 | end 55 | assert_raise NoMethodError do 56 | service.attribute_2 = "d" 57 | end 58 | end 59 | 60 | def test_default_public 61 | service = TestDefaultPublicAccessors.new(attribute_1: "a") 62 | assert_nothing_raised do 63 | service.attribute_2 = 3 64 | end 65 | assert_equal service.attribute_2, 3 66 | assert_raise NoMethodError do 67 | service.attribute_1 = "b" 68 | end 69 | assert_raise NoMethodError do 70 | service.attribute_1 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /test/test_hash_public_mixed.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test/unit" 4 | require_relative "../lib/smart_init/main" 5 | 6 | class TestSomePublicMixed 7 | extend SmartInit 8 | initialize_with :attribute_1, :attribute_2, :attribute_3, :attribute_4, 9 | public_readers: [:attribute_1], 10 | public_accessors: [:attribute_2, :attribute_3] 11 | is_callable 12 | 13 | def call 14 | [attribute_1, attribute_2, attribute_3, attribute_4] 15 | end 16 | end 17 | 18 | class TestAllReadersSomeAccessorsPublic 19 | extend SmartInit 20 | initialize_with :attribute_1, :attribute_2, public_readers: true, public_accessors: [:attribute_2] 21 | 22 | def call 23 | [attribute_1, attribute_2] 24 | end 25 | end 26 | 27 | class HashApiPublicTest < Test::Unit::TestCase 28 | def test_readers_some_public_mixed 29 | service = TestSomePublicMixed.new( 30 | attribute_1: "a", attribute_2: "b", 31 | attribute_3: "c", attribute_4: "d", 32 | ) 33 | assert_nothing_raised do 34 | service.attribute_1 35 | service.attribute_2 36 | service.attribute_3 37 | end 38 | assert_raise NoMethodError do 39 | service.attribute_4 40 | end 41 | end 42 | 43 | def test_writers_some_public_mixed 44 | service = TestSomePublicMixed.new( 45 | attribute_1: "a", attribute_2: "b", 46 | attribute_3: "c", attribute_4: "d", 47 | ) 48 | assert_nothing_raised do 49 | service.attribute_2 = "e" 50 | service.attribute_3 = "f" 51 | end 52 | assert_equal service.attribute_2, "e" 53 | assert_equal service.attribute_3, "f" 54 | assert_raise NoMethodError do 55 | service.attribute_4 = "g" 56 | end 57 | end 58 | 59 | def test_readers_all_readers_some_accessors_public 60 | service = TestAllReadersSomeAccessorsPublic.new( 61 | attribute_1: "a", attribute_2: "b", 62 | ) 63 | assert_nothing_raised do 64 | service.attribute_1 65 | service.attribute_2 66 | end 67 | end 68 | 69 | def test_writers_all_readers_some_accessors_public 70 | service = TestAllReadersSomeAccessorsPublic.new( 71 | attribute_1: "a", attribute_2: "b", 72 | ) 73 | assert_raise NoMethodError do 74 | service.attribute_1 = "c" 75 | end 76 | assert_nothing_raised do 77 | service.attribute_2 = "d" 78 | end 79 | assert_equal service.attribute_2, "d" 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /test/test_hash_public_readers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test/unit" 4 | require_relative "../lib/smart_init/main" 5 | 6 | class TestAllPublic 7 | extend SmartInit 8 | initialize_with :attribute_1, :attribute_2, public_readers: true 9 | is_callable 10 | 11 | def call 12 | [attribute_1, attribute_2] 13 | end 14 | end 15 | 16 | class TestSomePublic 17 | extend SmartInit 18 | initialize_with :attribute_1, :attribute_2, public_readers: [:attribute_1] 19 | is_callable 20 | 21 | def call 22 | [attribute_1, attribute_2] 23 | end 24 | end 25 | 26 | class TestDefaultPublic 27 | extend SmartInit 28 | initialize_with :attribute_1, attribute_2: 2, public_readers: [:attribute_2] 29 | 30 | def call 31 | [attribute_1, attribute_2] 32 | end 33 | end 34 | 35 | class TestDefaultAllPublic 36 | extend SmartInit 37 | initialize_with :attribute_1, attribute_2: 2, public_readers: true 38 | 39 | def call 40 | [attribute_1, attribute_2] 41 | end 42 | end 43 | 44 | class HashApiPublicTest < Test::Unit::TestCase 45 | def test_all_public 46 | service = TestAllPublic.new(attribute_1: "a", attribute_2: "b") 47 | assert_equal service.attribute_1, "a" 48 | assert_equal service.attribute_2, "b" 49 | end 50 | 51 | def test_some_public 52 | service = TestSomePublic.new(attribute_1: "a", attribute_2: "b") 53 | assert_equal service.attribute_1, "a" 54 | assert_raise NoMethodError do 55 | service.attribute_2 56 | end 57 | end 58 | 59 | def test_default_public 60 | service = TestDefaultPublic.new(attribute_1: "a") 61 | assert_equal service.attribute_2, 2 62 | 63 | assert_raise NoMethodError do 64 | service.attribute_1 65 | end 66 | end 67 | 68 | def test_default_all_public 69 | service = TestDefaultAllPublic.new(attribute_1: "a") 70 | assert_equal service.attribute_1, "a" 71 | assert_equal service.attribute_2, 2 72 | end 73 | end 74 | --------------------------------------------------------------------------------