├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .travis.yml ├── Changelog.md ├── Gemfile ├── Guardfile ├── LICENSE ├── README.md ├── Rakefile ├── concord.gemspec ├── config ├── flay.yml ├── flog.yml ├── mutant.yml ├── reek.yml ├── roodi.yml ├── rubocop.yml └── yardstick.yml ├── lib └── concord.rb ├── mutant.sh └── spec ├── spec_helper.rb └── unit └── concord └── universal_spec.rb /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: push 4 | 5 | jobs: 6 | base: 7 | name: Base steps 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Check Whitespace 12 | run: git diff --check -- HEAD~1 13 | ruby-unit-spec: 14 | name: Unit Specs 15 | runs-on: ${{ matrix.os }} 16 | timeout-minutes: 5 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | ruby: ['2.6'] 21 | os: [ubuntu-latest] 22 | steps: 23 | - uses: actions/checkout@v2 24 | - uses: actions/setup-ruby@v1 25 | with: 26 | ruby-version: ${{ matrix.ruby }} 27 | - run: | 28 | gem install bundler 29 | bundle install 30 | - run: bundle exec rspec spec/unit 31 | ruby-mutant: 32 | name: Mutation coverage 33 | runs-on: ${{ matrix.os }} 34 | timeout-minutes: 5 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | ruby: ['2.6'] 39 | os: [ubuntu-latest] 40 | steps: 41 | - uses: actions/checkout@v2 42 | with: 43 | fetch-depth: 0 44 | - uses: actions/setup-ruby@v1 45 | with: 46 | ruby-version: ${{ matrix.ruby }} 47 | - run: | 48 | gem install bundler 49 | bundle install 50 | - run: ./mutant.sh 51 | ruby-rubocop: 52 | name: Rubocop 53 | runs-on: ${{ matrix.os }} 54 | timeout-minutes: 5 55 | strategy: 56 | fail-fast: false 57 | matrix: 58 | ruby: ['2.6'] 59 | os: [ubuntu-latest] 60 | steps: 61 | - uses: actions/checkout@v2 62 | - uses: actions/setup-ruby@v1 63 | with: 64 | ruby-version: ${{ matrix.ruby }} 65 | - run: bundle install 66 | - run: bundle exec rubocop 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /tmp 2 | /Gemfile.lock 3 | /.bundle 4 | /vendor 5 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format progress 3 | --profile 4 | --order random 5 | --fail-fast 6 | --warnings 7 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | Include: 3 | - 'Gemfile' 4 | Exclude: 5 | - 'vendor/**' 6 | - 'benchmarks/**' 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | script: 'bundle exec rake ci' 3 | rvm: 4 | - '1.9' 5 | - '2.0' 6 | - '2.1' 7 | - '2.2' 8 | - rbx 9 | matrix: 10 | include: 11 | - rvm: jruby 12 | env: JRUBY_OPTS="$JRUBY_OPTS --debug --1.9" 13 | - rvm: jruby 14 | env: JRUBY_OPTS="$JRUBY_OPTS --debug --2.0" 15 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # v0.1.6 2020-09-10 2 | 3 | * Change packaging to avoid git interaction 4 | 5 | # v0.1.5 2014-04-10 6 | 7 | * Dependency bumps 8 | * Support calling (z)super from custom initialize 9 | 10 | # v0.1.4 2013-09-22 11 | 12 | * Dependency bumps 13 | 14 | # v0.1.3 2013-08-31 15 | 16 | * Dependency bumps 17 | 18 | # v0.1.2 2013-08-07 19 | 20 | * Dependency bumps 21 | * Internal refactorings 22 | 23 | # v0.1.1 2013-05-15 24 | 25 | + Add Concord::Public mixin defaulting to public attr_readers 26 | 27 | # v0.1.0 2013-05-15 28 | 29 | * [change] Set default attribute visibility to protected 30 | 31 | # v0.0.3 2013-03-08 32 | 33 | * [fix] 1.9.2 visibility problem 34 | 35 | # v0.0.2 2013-03-07 36 | 37 | * [change] remove unneded backports dependency 38 | 39 | # v0.0.1 2013-03-06 40 | 41 | * First public release! 42 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gemspec 6 | 7 | gem 'mutant' 8 | gem 'mutant-rspec' 9 | 10 | source 'https://oss:Px2ENN7S91OmWaD5G7MIQJi1dmtmYrEh@gem.mutant.dev' do 11 | gem 'mutant-license' 12 | end 13 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | guard 'rspec' do 5 | watch(%r{^spec/.+_spec\.rb$}) 6 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } 7 | watch('spec/spec_helper.rb') { "spec" } 8 | end 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Markus Schirp 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | concord 2 | ======= 3 | 4 | [![Gem Version](http://img.shields.io/gem/v/concord.svg)][https://rubygems.org/gems/concord] 5 | ![CI](https://github.com/mbj/concord/workflows/CI/badge.svg) 6 | 7 | Library to transform this: 8 | 9 | ```ruby 10 | class ComposedObject 11 | include Equalizer.new(:foo, :bar) 12 | 13 | # Return foo 14 | # 15 | # @return [Foo] 16 | # 17 | # @api private 18 | # 19 | attr_reader :foo 20 | protected :foo 21 | 22 | # Return bar 23 | # 24 | # @return [Bar] 25 | # 26 | # @api private 27 | # 28 | attr_reader :bar 29 | protected :bar 30 | 31 | # Initialize object 32 | # 33 | # @param [Foo] foo 34 | # @param [Bar] bar 35 | # 36 | # @return [undefined] 37 | # 38 | # @api private 39 | # 40 | def initialize(foo, bar) 41 | @foo, @bar = foo, bar 42 | end 43 | end 44 | ``` 45 | 46 | Into shorter and easier to parse by eyes: 47 | 48 | ```ruby 49 | class ComposedObject 50 | include Concord.new(:foo, :bar) 51 | end 52 | ``` 53 | 54 | You can still add YARD docs for generated interface. 55 | 56 | Rubies 57 | ------ 58 | 59 | Tested under all >= 1.9 rubies. 60 | 61 | Installation 62 | ------------ 63 | 64 | Install the gem `concord` via your preferred method. 65 | 66 | Credits 67 | ------- 68 | 69 | * [mbj](https://github.com/mbj) 70 | 71 | Contributing 72 | ------------- 73 | 74 | * Fork the project. 75 | * Make your feature addition or bug fix. 76 | * Add tests for it. This is important so I don't break it in a 77 | future version unintentionally. 78 | * Commit, do not mess with Rakefile or version 79 | (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) 80 | * Send me a pull request. Bonus points for topic branches. 81 | 82 | License 83 | ------- 84 | 85 | See LICENSE 86 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'devtools' 2 | Devtools.init_rake_tasks 3 | 4 | Rake.application.load_imports 5 | task('metrics:mutant').clear 6 | 7 | namespace :metrics do 8 | task :mutant => :coverage do 9 | $stderr.puts 'Concord is a dependency of mutant and zombification is currently defunkt :(' 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /concord.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'concord' 3 | s.version = '0.1.6' 4 | s.authors = ['Markus Schirp'] 5 | s.email = ['mbj@schirp-dso.com'] 6 | s.homepage = 'https://github.com/mbj/concord' 7 | s.summary = 'Helper for object composition' 8 | s.license = 'MIT' 9 | s.description = s.summary 10 | 11 | s.files = Dir.glob('lib/**/*') 12 | s.require_paths = %w(lib) 13 | 14 | s.required_ruby_version = '>= 1.9.3' 15 | 16 | s.add_dependency('adamantium', '~> 0.2.0') 17 | s.add_dependency('equalizer', '~> 0.0.9') 18 | 19 | s.add_development_dependency('devtools', '~> 0.1.26') 20 | end 21 | -------------------------------------------------------------------------------- /config/flay.yml: -------------------------------------------------------------------------------- 1 | --- 2 | threshold: 3 3 | total_score: 18.0 4 | -------------------------------------------------------------------------------- /config/flog.yml: -------------------------------------------------------------------------------- 1 | --- 2 | threshold: 25.5 3 | -------------------------------------------------------------------------------- /config/mutant.yml: -------------------------------------------------------------------------------- 1 | --- 2 | integration: rspec 3 | -------------------------------------------------------------------------------- /config/reek.yml: -------------------------------------------------------------------------------- 1 | UncommunicativeParameterName: 2 | accept: [] 3 | exclude: [] 4 | enabled: true 5 | reject: 6 | - !ruby/regexp /^.$/ 7 | - !ruby/regexp /[0-9]$/ 8 | - !ruby/regexp /[A-Z]/ 9 | TooManyMethods: 10 | max_methods: 14 11 | enabled: true 12 | exclude: [] 13 | max_instance_variables: 4 14 | UncommunicativeMethodName: 15 | accept: [] 16 | exclude: [] 17 | enabled: true 18 | reject: 19 | - !ruby/regexp /^[a-z]$/ 20 | - !ruby/regexp /[0-9]$/ 21 | - !ruby/regexp /[A-Z]/ 22 | LongParameterList: 23 | max_params: 2 24 | exclude: [] 25 | enabled: true 26 | overrides: {} 27 | FeatureEnvy: 28 | exclude: 29 | - Concord#define_readers 30 | enabled: true 31 | ClassVariable: 32 | exclude: [] 33 | enabled: true 34 | BooleanParameter: 35 | exclude: [] 36 | enabled: true 37 | IrresponsibleModule: 38 | exclude: [] 39 | enabled: true 40 | UncommunicativeModuleName: 41 | accept: [] 42 | exclude: [] 43 | enabled: true 44 | reject: 45 | - !ruby/regexp /^.$/ 46 | - !ruby/regexp /[0-9]$/ 47 | NestedIterators: 48 | ignore_iterators: [] 49 | exclude: 50 | - Concord#define_initialize 51 | enabled: true 52 | max_allowed_nesting: 1 53 | TooManyStatements: 54 | max_statements: 7 55 | exclude: 56 | - Concord#define_initialize 57 | enabled: true 58 | DuplicateMethodCall: 59 | allow_calls: [] 60 | exclude: [] 61 | enabled: true 62 | max_calls: 1 63 | UtilityFunction: 64 | max_helper_calls: 0 65 | exclude: [] 66 | enabled: true 67 | Attribute: 68 | exclude: [] 69 | enabled: false 70 | UncommunicativeVariableName: 71 | accept: [] 72 | exclude: [] 73 | enabled: true 74 | reject: 75 | - !ruby/regexp /^.$/ 76 | - !ruby/regexp /[0-9]$/ 77 | - !ruby/regexp /[A-Z]/ 78 | RepeatedConditional: 79 | enabled: true 80 | max_ifs: 1 81 | DataClump: 82 | exclude: [] 83 | enabled: true 84 | max_copies: 0 85 | min_clump_size: 2 86 | ControlParameter: 87 | exclude: [] 88 | enabled: true 89 | LongYieldList: 90 | max_params: 0 91 | exclude: [] 92 | enabled: true 93 | -------------------------------------------------------------------------------- /config/roodi.yml: -------------------------------------------------------------------------------- 1 | --- 2 | AbcMetricMethodCheck: 3 | score: 25.1 4 | AssignmentInConditionalCheck: { } 5 | CaseMissingElseCheck: { } 6 | ClassLineCountCheck: { line_count: 317 } 7 | ClassNameCheck: 8 | pattern: !ruby/regexp /\A(?:[A-Z]+|[A-Z][a-z](?:[A-Z]?[a-z])+)\z/ 9 | ClassVariableCheck: { } 10 | CyclomaticComplexityBlockCheck: { complexity: 3 } 11 | CyclomaticComplexityMethodCheck: { complexity: 4 } 12 | EmptyRescueBodyCheck: { } 13 | ForLoopCheck: { } 14 | MethodLineCountCheck: { line_count: 14 } 15 | MethodNameCheck: 16 | pattern: !ruby/regexp /\A(?:[a-z\d](?:_?[a-z\d])+[?!=]?|\[\]=?|==|<=>|<<|[+*&|-])\z/ 17 | ModuleLineCountCheck: { line_count: 320 } 18 | ModuleNameCheck: 19 | pattern: !ruby/regexp /\A(?:[A-Z]+|[A-Z][a-z](?:[A-Z]?[a-z])+)\z/ 20 | ParameterNumberCheck: { parameter_count: 3 } 21 | -------------------------------------------------------------------------------- /config/rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: ../.rubocop.yml 2 | 3 | # Avoid parameter lists longer than five parameters. 4 | ParameterLists: 5 | Max: 3 6 | CountKeywordArgs: true 7 | 8 | # Avoid more than `Max` levels of nesting. 9 | BlockNesting: 10 | Max: 3 11 | 12 | # Align with the style guide. 13 | CollectionMethods: 14 | PreferredMethods: 15 | collect: 'map' 16 | inject: 'reduce' 17 | find: 'detect' 18 | find_all: 'select' 19 | 20 | # Do not force public/protected/private keyword to be indented at the same 21 | # level as the def keyword. My personal preference is to outdent these keywords 22 | # because I think when scanning code it makes it easier to identify the 23 | # sections of code and visually separate them. When the keyword is at the same 24 | # level I think it sort of blends in with the def keywords and makes it harder 25 | # to scan the code and see where the sections are. 26 | AccessModifierIndentation: 27 | Enabled: false 28 | 29 | # Limit line length 30 | LineLength: 31 | Max: 106 32 | 33 | # Disable documentation checking until a class needs to be documented once 34 | Documentation: 35 | Enabled: false 36 | 37 | # Do not always use &&/|| instead of and/or. 38 | AndOr: 39 | Enabled: false 40 | 41 | # Do not favor modifier if/unless usage when you have a single-line body 42 | IfUnlessModifier: 43 | Enabled: false 44 | 45 | # Allow case equality operator (in limited use within the specs) 46 | CaseEquality: 47 | Enabled: false 48 | 49 | # Constants do not always have to use SCREAMING_SNAKE_CASE 50 | ConstantName: 51 | Enabled: false 52 | 53 | # Not all trivial readers/writers can be defined with attr_* methods 54 | TrivialAccessors: 55 | Enabled: false 56 | 57 | # Allow empty lines around class body 58 | EmptyLinesAroundClassBody: 59 | Enabled: false 60 | 61 | # Allow empty lines around module body 62 | EmptyLinesAroundModuleBody: 63 | Enabled: false 64 | 65 | # Allow empty lines around block body 66 | EmptyLinesAroundBlockBody: 67 | Enabled: false 68 | 69 | # Allow multiple line operations to not require indentation 70 | MultilineOperationIndentation: 71 | Enabled: false 72 | 73 | # Prefer String#% over Kernel#sprintf 74 | FormatString: 75 | Enabled: false 76 | 77 | # Use square brackets for literal Array objects 78 | PercentLiteralDelimiters: 79 | PreferredDelimiters: 80 | '%': '{}' 81 | '%i': '[]' 82 | '%q': () 83 | '%Q': () 84 | '%r': '{}' 85 | '%s': () 86 | '%w': '[]' 87 | '%W': '[]' 88 | '%x': () 89 | 90 | # Align if/else blocks with the variable assignment 91 | EndAlignment: 92 | AlignWith: variable 93 | 94 | # Do not always align parameters when it is easier to read 95 | AlignParameters: 96 | Exclude: 97 | - spec/**/*_spec.rb 98 | 99 | # Prefer #kind_of? over #is_a? 100 | ClassCheck: 101 | EnforcedStyle: kind_of? 102 | 103 | # Do not prefer double quotes to be used when %q or %Q is more appropriate 104 | UnneededPercentQ: 105 | Enabled: false 106 | 107 | # Allow a maximum ABC score 108 | Metrics/AbcSize: 109 | Max: 21.02 110 | 111 | # Do not prefer lambda.call(...) over lambda.(...) 112 | LambdaCall: 113 | Enabled: false 114 | -------------------------------------------------------------------------------- /config/yardstick.yml: -------------------------------------------------------------------------------- 1 | --- 2 | threshold: 100 3 | -------------------------------------------------------------------------------- /lib/concord.rb: -------------------------------------------------------------------------------- 1 | require 'adamantium' 2 | require 'equalizer' 3 | 4 | # A mixin to define a composition 5 | class Concord < Module 6 | include Adamantium::Flat, Equalizer.new(:names) 7 | 8 | # The maximum number of objects the hosting class is composed of 9 | MAX_NR_OF_OBJECTS = 3 10 | 11 | # Return names 12 | # 13 | # @return [Enumerable] 14 | # 15 | # @api private 16 | # 17 | attr_reader :names 18 | 19 | private 20 | 21 | # Initialize object 22 | # 23 | # @return [undefined] 24 | # 25 | # @api private 26 | # 27 | def initialize(*names) 28 | if names.length > MAX_NR_OF_OBJECTS 29 | fail "Composition of more than #{MAX_NR_OF_OBJECTS} objects is not allowed" 30 | end 31 | 32 | @names, @module = names, Module.new 33 | define_initialize 34 | define_readers 35 | define_equalizer 36 | end 37 | 38 | # Hook run when module is included 39 | # 40 | # @return [undefined] 41 | # 42 | # @api private 43 | # 44 | def included(descendant) 45 | descendant.send(:include, @module) 46 | end 47 | 48 | # Define equalizer 49 | # 50 | # @return [undefined] 51 | # 52 | # @api private 53 | # 54 | def define_equalizer 55 | @module.send(:include, Equalizer.new(*@names)) 56 | end 57 | 58 | # Define readers 59 | # 60 | # @return [undefined] 61 | # 62 | # @api private 63 | # 64 | def define_readers 65 | attribute_names = names 66 | @module.class_eval do 67 | attr_reader(*attribute_names) 68 | protected(*attribute_names) 69 | end 70 | end 71 | 72 | # Define initialize method 73 | # 74 | # @return [undefined] 75 | # 76 | # @api private 77 | # 78 | # rubocop:disable MethodLength 79 | # 80 | def define_initialize 81 | ivars, size = instance_variable_names, names.size 82 | @module.class_eval do 83 | define_method :initialize do |*args| 84 | args_size = args.size 85 | if args_size != size 86 | fail ArgumentError, "wrong number of arguments (#{args_size} for #{size})" 87 | end 88 | ivars.zip(args) { |ivar, arg| instance_variable_set(ivar, arg) } 89 | end 90 | private :initialize 91 | end 92 | end 93 | 94 | # Return instance variable names 95 | # 96 | # @return [String] 97 | # 98 | # @api private 99 | # 100 | def instance_variable_names 101 | names.map { |name| "@#{name}" } 102 | end 103 | 104 | # Mixin for public attribute readers 105 | class Public < self 106 | 107 | # Hook called when module is included 108 | # 109 | # @param [Class,Module] descendant 110 | # 111 | # @return [undefined] 112 | # 113 | # @api private 114 | # 115 | def included(descendant) 116 | super 117 | @names.each do |name| 118 | descendant.send(:public, name) 119 | end 120 | end 121 | end # Public 122 | end # Concord 123 | -------------------------------------------------------------------------------- /mutant.sh: -------------------------------------------------------------------------------- 1 | #/usr/bin/bash -ex 2 | 3 | bundle exec mutant \ 4 | --zombie \ 5 | --ignore-subject Concord#define_equalizer \ 6 | --ignore-subject Concord#define_initialize \ 7 | --ignore-subject Concord#define_readers \ 8 | --ignore-subject Concord#included \ 9 | --ignore-subject Concord::Public#included \ 10 | -- 'Concord*' 11 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'concord' 2 | -------------------------------------------------------------------------------- /spec/unit/concord/universal_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Concord do 4 | 5 | let(:class_under_test) do 6 | Class.new do 7 | include Concord.new(:foo, :bar) 8 | end 9 | end 10 | 11 | let(:instance_a) { class_under_test.new(foo, bar) } 12 | let(:instance_b) { class_under_test.new(foo, bar) } 13 | let(:instance_c) { class_under_test.new(foo, double('Baz')) } 14 | 15 | let(:foo) { double('Foo') } 16 | let(:bar) { double('Bar') } 17 | 18 | context 'initializer lines' do 19 | it 'creates a private #initialize method' do 20 | mod = Module.new 21 | expect { mod.send(:include, Concord.new) } 22 | .to change { mod.private_method_defined?(:initialize) } 23 | .from(false).to(true) 24 | end 25 | 26 | it 'creates an initializer that asserts the number of arguments' do 27 | expect { class_under_test.new(1) } 28 | .to raise_error(ArgumentError, 'wrong number of arguments (1 for 2)') 29 | end 30 | 31 | it 'creates an initializer that allows 2 arguments' do 32 | expect { class_under_test.new(1, 2) }.to_not raise_error 33 | end 34 | 35 | it 'creates an initializer that is callable via super' do 36 | class_under_test.class_eval do 37 | attr_reader :baz 38 | public :foo 39 | public :bar 40 | 41 | def initialize(foo, bar) 42 | @baz = foo + bar 43 | super(foo, bar) 44 | end 45 | end 46 | 47 | instance = class_under_test.new(1, 2) 48 | expect(instance.foo).to eql(1) 49 | expect(instance.bar).to eql(2) 50 | expect(instance.baz).to eql(3) 51 | end 52 | 53 | it 'creates an initializer that is callable via zsuper' do 54 | class_under_test.class_eval do 55 | attr_reader :baz 56 | public :foo 57 | public :bar 58 | 59 | def initialize(foo, bar) 60 | @baz = foo + bar 61 | super 62 | end 63 | end 64 | 65 | instance = class_under_test.new(1, 2) 66 | expect(instance.foo).to eql(1) 67 | expect(instance.bar).to eql(2) 68 | expect(instance.baz).to eql(3) 69 | end 70 | 71 | it 'creates an initializer that sets the instance variables' do 72 | instance = class_under_test.new(1, 2) 73 | expect(instance.instance_variable_get(:@foo)).to be(1) 74 | expect(instance.instance_variable_get(:@bar)).to be(2) 75 | end 76 | end 77 | 78 | context 'with no objects to compose' do 79 | it 'assigns no ivars' do 80 | instance = Class.new { include Concord.new }.new 81 | expect(instance.instance_variables).to be_empty 82 | end 83 | end 84 | 85 | context 'visibility' do 86 | it 'should set attribute readers to protected' do 87 | protected_methods = class_under_test.protected_instance_methods 88 | expect(protected_methods).to match_array([:foo, :bar]) 89 | end 90 | end 91 | 92 | context 'attribute behavior' do 93 | subject { instance_a } 94 | 95 | specify { expect(subject.send(:foo)).to be(foo) } 96 | specify { expect(subject.send(:bar)).to be(bar) } 97 | end 98 | 99 | context 'equalization behavior' do 100 | specify 'composed objects are equalized on attributes' do 101 | expect(instance_a).to eql(instance_b) 102 | expect(instance_a.hash).to eql(instance_b.hash) 103 | expect(instance_a).to eql(instance_b) 104 | expect(instance_a).to_not be(instance_c) 105 | expect(instance_a).to_not eql(instance_c) 106 | end 107 | end 108 | 109 | context 'when composing too many objects' do 110 | specify 'it raises an error' do 111 | expect do 112 | Concord.new(:a, :b, :c, :d) 113 | end.to raise_error(RuntimeError, 'Composition of more than 3 objects is not allowed') 114 | expect do 115 | Concord.new(:a, :b, :c) 116 | end.to_not raise_error 117 | end 118 | end 119 | 120 | context Concord::Public do 121 | let(:class_under_test) do 122 | Class.new do 123 | include Concord::Public.new(:foo, :bar) 124 | end 125 | end 126 | 127 | it 'should create public attr readers' do 128 | object = class_under_test.new(:foo, :bar) 129 | expect(object.foo).to eql(:foo) 130 | expect(object.bar).to eql(:bar) 131 | end 132 | end 133 | end 134 | --------------------------------------------------------------------------------