diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..ffce818 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,46 @@ +--- +name: Tests +'on': + push: + branches: + - master + pull_request: + branches: + - master +jobs: + test: + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + include: + - ruby: 2.5.7 + gemfile: Gemfile + - ruby: 2.6.10 + gemfile: Gemfile + - ruby: 2.7.4 + gemfile: Gemfile + - ruby: 3.0.2 + gemfile: Gemfile + - ruby: 3.1.2 + gemfile: Gemfile + - ruby: 3.2.1 + gemfile: Gemfile + - ruby: 3.3.6 + gemfile: Gemfile + - ruby: 3.4.1 + gemfile: Gemfile + env: + BUNDLE_GEMFILE: "${{ matrix.gemfile }}" + steps: + - uses: actions/checkout@v2 + - name: Install ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: "${{ matrix.ruby }}" + - name: Bundle + run: | + gem install bundler:2.3.27 + bundle install --no-deployment + - name: Run tests + run: bundle exec rspec diff --git a/.gitignore b/.gitignore index 6d88d9c..8c0f972 100755 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,3 @@ doc pkg *.gem .idea -Gemfile.lock diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..c99d2e7 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..35d16fb --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +2.5.7 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..908d6e4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,45 @@ +# Changelog +All notable changes to this project will be documented in this file. + +This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## Unreleased + +### Breaking changes + +### Compatible changes + +- Add support for Ruby 3.3 +- Add support for Ruby 3.4 + +## 3.2.0 - 2023-03-01 + +### Compatible changes + +- Add support for Ruby 3.2 + + +## 3.1.0 - 2022-06-01 + +### Compatible changes + +- Add support for separation of positional and keyword arguments in ruby 3 + + +## 3.0.1 - 2022-03-09 + +### Compatible changes + +- Activate Rubygems MFA + + +## 3.0.0 - 2021-08-24 + +### Breaking changes + +- Removed migration guide for modularity version 1. +- Removed support for Ruby < `2.5.0`. + +### Compatible changes + +- Added this CHANGELOG file. diff --git a/Gemfile b/Gemfile index c80ee36..f0d494f 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,8 @@ -source "http://rubygems.org" +source 'http://rubygems.org' gemspec + +gem 'rake' +gem 'rspec' +gem 'pry-byebug' +gem 'gemika' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..28e2709 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,47 @@ +PATH + remote: . + specs: + modularity (3.2.0) + +GEM + remote: http://rubygems.org/ + specs: + byebug (11.1.3) + coderay (1.1.3) + diff-lcs (1.4.4) + gemika (0.8.1) + method_source (1.0.0) + pry (0.13.1) + coderay (~> 1.1) + method_source (~> 1.0) + pry-byebug (3.9.0) + byebug (~> 11.0) + pry (~> 0.13.0) + rake (13.0.6) + rspec (3.10.0) + rspec-core (~> 3.10.0) + rspec-expectations (~> 3.10.0) + rspec-mocks (~> 3.10.0) + rspec-core (3.10.1) + rspec-support (~> 3.10.0) + rspec-expectations (3.10.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.10.0) + rspec-mocks (3.10.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.10.0) + rspec-support (3.10.2) + +PLATFORMS + ruby + x86_64-linux + +DEPENDENCIES + gemika + modularity! + pry-byebug + rake + rspec + +BUNDLED WITH + 2.3.27 diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..646e18a --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2009 Henning Koch + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/MIT-LICENSE b/MIT-LICENSE deleted file mode 100755 index 30adc46..0000000 --- a/MIT-LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (c) 2009 Henning Koch - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100755 index 0000000..8937d56 --- /dev/null +++ b/README.md @@ -0,0 +1,128 @@ +[![Tests](https://github.com/makandra/modularity/workflows/Tests/badge.svg)](https://github.com/makandra/modularity/actions) + +# Modularity - Traits and partial classes for Ruby + +Modularity enhances Ruby's [`Module`](http://apidock.com/ruby/Module) so it can be used traits and partial classes. +This allows very simple definition of meta-programming macros like the +`has_many` that you know from Rails. + +Modularity also lets you organize large models into multiple source files +in a way that is less awkward than using modules. + +## Installation + +Add the following to your `Gemfile`: + +```ruby +gem 'modularity' +``` + +Now run `bundle install`. + +## Example 1: Easy meta-programming macros + +Ruby allows you to construct classes using meta-programming macros like +`acts_as_tree` or `has_many :items`. These macros will add methods, +callbacks, etc. to the calling class. However, right now Ruby (and Rails) makes it awkward to define +such macros in your project as part of your application domain. + +Modularity allows you to extract common behaviour into reusable macros by defining traits with parameters. +Your macros can live in your application, allowing you to express your application domain in both classes +and macros. + +Here is an example of a `strip_field` macro, which created setter methods that remove leading and trailing whitespace from newly assigned values: + +```ruby +# app/models/article.rb +class Article < ActiveRecord::Base + include DoesStripFields[:name, :brand] +end + +# app/models/shared/does_strip_fields.rb +module DoesStripFields + as_trait do |*fields| + fields.each do |field| + define_method("#{field}=") do |value| + self[field] = value.strip + end + end + end +end +``` + +Notice the `as_trait` block. + +We like to add `app/models/shared` and `app/controllers/shared` to the load paths of our Rails projects. +These are great places to store macros that are re-used from multiple classes. + +## Example 2: Mixins with class methods + +Using a module to add both instance methods and class methods is +[very awkward](http://redcorundum.blogspot.com/2006/06/mixing-in-class-methods.html). +Modularity does away with the clutter and lets you say this: + +```ruby +# app/models/model.rb +class Model + include Mixin +end + +# app/models/mixin.rb +module Mixin + as_trait do + def instance_method + # ... + end + def self.class_method + # .. + end + end +end +``` + +`private` and `protected` will also work as expected when defining a trait. + +## Example 3: Splitting a model into multiple source files + +Models are often concerned with multiple themes like "authentication", "contact info" or "permissions", each requiring +a couple of validations and callbacks here, and some method there. Modularity lets you organize your model into multiple +partial classes, so each file can deal with a single aspect of your model: + +```ruby +# app/models/user.rb +class User < ActiveRecord::Base + include DoesAuthentication + include DoesPermissions +end + +# app/models/user/does_authentication.rb +module User::DoesAuthentication + as_trait do + # methods, validations, etc. regarding usernames and passwords go here + end +end + +# app/models/user/does_permissions.rb +module User::DoesPermissions + as_trait do + # methods, validations, etc. regarding contact information go here + end +end +``` + +Some criticism has been raised for splitting large models into files like this. +Essentially, even though have an easier time navigating your code, you will still +have one giant model with many side effects. + +There are [many better ways](http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/) +to decompose a huge Ruby class. + +## Development + +* Install Bundler 2 `gem install bundler:2.2.22` and run `bundle install` to have a working development setup. +* Running tests for the current Ruby version: `bundle exec rake` +* Running tests for all supported Ruby version: Push the changes to Github in a feature branch, open a merge request and have a look at the test matrix in Github actions + +## Credits + +Henning Koch from [makandra.com](http://makandra.com/) diff --git a/README.rdoc b/README.rdoc deleted file mode 100755 index 3190ea7..0000000 --- a/README.rdoc +++ /dev/null @@ -1,107 +0,0 @@ -= modularity - Traits and partial classes for Ruby - -Modularity provides traits and partial classes for Ruby. -This allows very simple definition of meta-programming macros, -as you might now from acts_as_something type of plugins, -or the macros Rails provides for your models. This also lets you organize -large models into multiple source files in a way that is less awkward -than using modules. - -Modularity traits are to your models what partials are for your Rails views. - -== Example 1: Easy meta-programming macros - -Ruby allows you to construct classes using meta-programming macros like acts_as_tree or has_many :items. -These macros will add methods, callbacks, etc. to the calling class. However, right now Ruby (and Rails) makes it awkward to define -such macros in your project as part of your application domain. - -Modularity allows you to extract common behaviour into reusable macros by defining traits with parameters. Your macros can live in your -application, allowing you to express your application domain in both classes and macros. - -Here is an example of a strip_field macro, which created setter methods that remove leading and trailing whitespace from newly assigned values: - - # app/models/article.rb - class Article - does "strip_fields", :name, :brand - end - - # app/models/shared/strip_fields_trait.rb - module StripFieldsTrait - as_trait do |*fields| - fields.each do |field| - define_method("#{field}=") do |value| - self[field] = value.strip - end - end - end - end - -We like to add app/models/shared and app/controllers/shared to the load paths of our Rails projects. These are great places to store macros -that are re-used from multiple classes. - -== Example 2: Mixins with class methods - -Using a module to add both instance methods and class methods is {very awkward}[http://redcorundum.blogspot.com/2006/06/mixing-in-class-methods.html]. -Modularity does away with the clutter and lets you say this: - - # app/models/model.rb - class Model - does "mixin" - end - - # app/models/mixin_trait.rb - module MixinTrait - as_trait do - def instance_method - # ... - end - def self.class_method - # .. - end - end - -private and protected will also work as expected when defining a trait. - -== Example 3: Splitting a model into multiple source files - -Models are often concerned with multiple themes like "authentication", "contact info" or "permissions", each requiring -a couple of validations and callbacks here, and some method there. Modularity lets you organize your model into multiple -partial classes, so each file can deal with a single aspect of your model: - - # app/models/user.rb - class User < ActiveRecord::Base - does "user/authentication" - does "user/address" - end - - # app/models/user/authentication_trait.rb - module User::AuthenticationTrait - as_trait do - # methods, validations, etc. regarding usernames and passwords go here - end - end - - # app/models/user/permissions_trait.rb - module User::PermissionsTrait - as_trait do - # methods, validations, etc. regarding contact information go here - end - end - -== Installation - - sudo gem install modularity - - -== Note if you're still on Ruby 1.8.6 - -Modularity requires Ruby 1.8.7. Earlier versions are missing class_exec. You might be able to hack in class_exec -using {this}[http://github.com/brynary/rspec/blob/f80d61a399b34f58084a378c85a43a95ff484619/lib/spec/extensions/instance_exec.rb] as a guide, but it's not pretty. - -== Credits - -Henning Koch - -{makandra.com}[http://makandra.com/] - -{gem-session.com}[http://gem-session.com/] diff --git a/Rakefile b/Rakefile index 8c158fe..666a2e5 100755 --- a/Rakefile +++ b/Rakefile @@ -1,12 +1,5 @@ -require 'rake' -require 'spec/rake/spectask' require 'bundler/gem_tasks' +require 'bundler/setup' +require 'gemika/tasks' -desc 'Default: Run all specs.' -task :default => :spec - -desc "Run all specs" -Spec::Rake::SpecTask.new() do |t| - t.spec_opts = ['--options', "\"spec/spec.opts\""] - t.spec_files = FileList['spec/**/*_spec.rb'] -end +task default: 'matrix:spec' diff --git a/bin/console b/bin/console new file mode 100755 index 0000000..5f3a90a --- /dev/null +++ b/bin/console @@ -0,0 +1,9 @@ +#!/usr/bin/env ruby + +require 'bundler/setup' +require 'modularity' +require 'pry' + +# You can add fixtures and/or initialization code here to make experimenting +# with your gem easier. You can also use a different console, if you like. +Pry.start diff --git a/lib/modularity.rb b/lib/modularity.rb index 39e4ef5..402ce75 100755 --- a/lib/modularity.rb +++ b/lib/modularity.rb @@ -1,2 +1 @@ -require 'modularity/does' - +require 'modularity/as_trait' diff --git a/lib/modularity/as_trait.rb b/lib/modularity/as_trait.rb index f26ae95..bbc3f0a 100755 --- a/lib/modularity/as_trait.rb +++ b/lib/modularity/as_trait.rb @@ -1,9 +1,52 @@ module Modularity + + class ParametrizedTrait < Module + + def initialize(blank_trait, args, kwargs) + @args = args + @kwargs = kwargs + @macro = blank_trait.instance_variable_get(:@modularity_macro) + include(blank_trait) + end + + def included(base) + if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.7') + base.class_exec(*@args, &@macro) + else + base.class_exec(*@args, **@kwargs, &@macro) + end + end + + end + module AsTrait - def as_trait(&block) - @trait_macro = block + + def as_trait(¯o) + + @modularity_macro = macro + + def self.included(base) + unless base.is_a?(ParametrizedTrait) + base.class_exec(&@modularity_macro) + end + + end + + if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.7') + def self.[](*args) + blank_trait = self + ParametrizedTrait.new(blank_trait, args, {}) + end + else + def self.[](*args, **kwargs) + blank_trait = self + ParametrizedTrait.new(blank_trait, args, kwargs) + end + end + end + end end -Object.send :include, Modularity::AsTrait +Module.send(:include, Modularity::AsTrait) diff --git a/lib/modularity/does.rb b/lib/modularity/does.rb deleted file mode 100755 index c14b7c2..0000000 --- a/lib/modularity/does.rb +++ /dev/null @@ -1,23 +0,0 @@ -require 'modularity/inflector' -require 'modularity/as_trait' - -module Modularity - module Does - - def self.included(base) - base.extend ClassMethods - end - - module ClassMethods - def does(trait_name, *args) - trait_name = "#{Modularity::Inflector.camelize(trait_name.to_s)}Trait" - trait = Modularity::Inflector.constantize(trait_name) - macro = trait.instance_variable_get("@trait_macro") or raise "Missing trait directive in #{trait_name}" - class_exec(*args, ¯o) - end - end - - end -end - -Object.send :include, Modularity::Does diff --git a/lib/modularity/inflector.rb b/lib/modularity/inflector.rb deleted file mode 100755 index 5706eee..0000000 --- a/lib/modularity/inflector.rb +++ /dev/null @@ -1,60 +0,0 @@ -# These methods are backported from Rails so modularity works with plain Ruby. - -module Modularity - class Inflector - class << self - - # File activesupport/lib/active_support/inflector.rb, line 178 - def camelize(lower_case_and_underscored_word, first_letter_in_uppercase = true) - if first_letter_in_uppercase - lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase } - else - lower_case_and_underscored_word.first.downcase + camelize(lower_case_and_underscored_word)[1..-1] - end - end - - - if Module.method(:const_get).arity == 1 - # Tries to find a constant with the name specified in the argument string: - # - # "Module".constantize # => Module - # "Test::Unit".constantize # => Test::Unit - # - # The name is assumed to be the one of a top-level constant, no matter whether - # it starts with "::" or not. No lexical context is taken into account: - # - # C = 'outside' - # module M - # C = 'inside' - # C # => 'inside' - # "C".constantize # => 'outside', same as ::C - # end - # - # NameError is raised when the name is not in CamelCase or the constant is - # unknown. - def constantize(camel_cased_word) - names = camel_cased_word.split('::') - names.shift if names.empty? || names.first.empty? - - constant = Object - names.each do |name| - constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name) - end - constant - end - else - def constantize(camel_cased_word) #:nodoc: - names = camel_cased_word.split('::') - names.shift if names.empty? || names.first.empty? - - constant = Object - names.each do |name| - constant = constant.const_defined?(name, false) ? constant.const_get(name) : constant.const_missing(name) - end - constant - end - end - - end - end -end diff --git a/lib/modularity/version.rb b/lib/modularity/version.rb index b84bf07..d40bd34 100644 --- a/lib/modularity/version.rb +++ b/lib/modularity/version.rb @@ -1,3 +1,3 @@ module Modularity - VERSION = '0.6.1' + VERSION = '3.2.0' end diff --git a/modularity.gemspec b/modularity.gemspec index 89a9fd0..77d626f 100755 --- a/modularity.gemspec +++ b/modularity.gemspec @@ -1,22 +1,28 @@ -# -*- encoding: utf-8 -*- -$:.push File.expand_path("../lib", __FILE__) +lib = File.expand_path('lib', __dir__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'modularity/version' -Gem::Specification.new do |s| - s.name = %q{modularity} - s.version = Modularity::VERSION - s.authors = ["Henning Koch"] - s.email = %q{github@makandra.de} - s.homepage = %q{http://github.com/makandra/modularity} - s.summary = %q{Traits and partial classes for Ruby} - s.description = %q{Traits and partial classes for Ruby} +Gem::Specification.new do |spec| + spec.name = 'modularity' + spec.version = Modularity::VERSION + spec.required_ruby_version = '>= 2.5.0' + spec.authors = ['Henning Koch'] + spec.email = ['henning.koch@makandra.de'] - s.files = `git ls-files`.split("\n") - s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") - s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } - s.require_paths = ["lib"] + spec.summary = 'Traits and partial classes for Ruby' + spec.description = 'Traits and partial classes for Ruby' + spec.homepage = 'https://github.com/makandra/modularity' + spec.license = 'MIT' + spec.metadata = { 'rubygems_mfa_required' => 'true' } - s.add_development_dependency('rake') - s.add_development_dependency('rspec', '<2') + # Specify which files should be added to the gem when it is released. + # The `git ls-files -z` loads the files in the RubyGem that have been added into git. + spec.files = Dir.chdir(File.expand_path(__dir__)) do + `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } + end + spec.bindir = 'exe' + spec.executables = spec.files.grep(%r(^exe/)) { |f| File.basename(f) } + spec.require_paths = ['lib'] + # Development dependencies are defined in the Gemfile (therefore no `spec.add_development_dependency` directives) end diff --git a/spec/as_trait_spec.rb b/spec/as_trait_spec.rb deleted file mode 100755 index 48383ea..0000000 --- a/spec/as_trait_spec.rb +++ /dev/null @@ -1,29 +0,0 @@ -require 'spec_helper' - -module Trait - as_trait do - "hi world" - end -end - -module ParametrizedTrait - as_trait do |name| - "hi, #{name}" - end -end - -describe Modularity::AsTrait do - - describe 'as_trait' do - - it "should let modules save a proc upon loading" do - Trait.instance_variable_get("@trait_macro").call.should == "hi world" - end - - it "should let modules save a proc with parameters upon loading" do - ParametrizedTrait.instance_variable_get("@trait_macro").call("jean").should == "hi, jean" - end - - end - -end diff --git a/spec/does_spec.rb b/spec/does_spec.rb deleted file mode 100755 index a918e86..0000000 --- a/spec/does_spec.rb +++ /dev/null @@ -1,93 +0,0 @@ -require 'spec_helper' - -module SomeTrait - as_trait do - some_trait_included - end -end - -module Some - module ChildTrait - as_trait do - some_child_trait_included - end - end -end - -module CallMethodTrait - as_trait do |field| - send(field) - end -end - -module VisibilityTrait - as_trait do - def public_method_from_trait - end - protected - def protected_method_from_trait - end - private - def private_method_from_trait - end - end -end - -module DefineConstantMethodTrait - as_trait do |name, return_value| - define_method name do - return_value - end - end -end - -class Doer -end - -describe Modularity::AsTrait do - - describe 'does' do - - it "should apply the named module" do - Doer.should_receive(:some_trait_included) - Doer.class_eval do - does "some" - end - end - - it "should apply a namespaced module, using slash-notation like require" do - Doer.should_receive(:some_child_trait_included) - Doer.class_eval do - does "some/child" - end - end - - it "should class_eval the as_trait proc on the doer" do - Doer.should_receive(:foo) - Doer.class_eval do - does "call_method", :foo - end - end - - it "should allow the trait to define methods with different visibility" do - Doer.class_eval do - does "visibility" - end - instance = Doer.new - instance.public_methods.collect(&:to_s).should include("public_method_from_trait") - instance.protected_methods.collect(&:to_s).should include("protected_method_from_trait") - instance.private_methods.collect(&:to_s).should include("private_method_from_trait") - end - - it "should allow the trait to perform metaprogramming acrobatics" do - Doer.class_eval do - does "define_constant_method", "some_method", "some_return_value" - end - instance = Doer.new - instance.should respond_to(:some_method) - instance.some_method.should == "some_return_value" - end - - end - -end diff --git a/spec/modularity/as_trait_spec.rb b/spec/modularity/as_trait_spec.rb new file mode 100755 index 0000000..d0cca6a --- /dev/null +++ b/spec/modularity/as_trait_spec.rb @@ -0,0 +1,237 @@ +describe Modularity::AsTrait do + + describe '.included' do + + before :each do + @doing_class = Class.new + end + + describe 'without parameters' do + + it "applies the trait macro of the given module" do + + module DoesSome + as_trait do + some_trait_included + end + end + + @doing_class.should_receive(:some_trait_included) + + @doing_class.class_eval do + include DoesSome + end + + end + + it "applies the trait macro of the given namespaced module" do + + module Some + module DoesChild + as_trait do + some_child_trait_included + end + end + end + + @doing_class.should_receive(:some_child_trait_included) + + @doing_class.class_eval do + include Some::DoesChild + end + + end + + it "lets a trait define methods with different visibility" do + + module DoesVisibility + as_trait do + def public_method_from_trait + end + protected + def protected_method_from_trait + end + private + def private_method_from_trait + end + end + end + + @doing_class.class_eval do + include DoesVisibility + end + + instance = @doing_class.new + + instance.public_methods.collect(&:to_s).should include("public_method_from_trait") + instance.protected_methods.collect(&:to_s).should include("protected_method_from_trait") + instance.private_methods.collect(&:to_s).should include("private_method_from_trait") + + end + + it 'appends methods outside the trait macro' do + + module HybridModule + + as_trait do + define_method :trait_method do + end + end + + def vanilla_method + end + + end + + @doing_class.class_eval do + include HybridModule + end + + instance = @doing_class.new + + instance.should respond_to(:trait_method) + instance.should respond_to(:vanilla_method) + + end + + it 'applies multiple trait macros' do + + module FirstTrait + as_trait do + define_method :first do + end + end + end + + module SecondTrait + as_trait do + define_method :second do + end + end + end + + @doing_class.class_eval do + include FirstTrait + include SecondTrait + end + + instance = @doing_class.new + + instance.should respond_to(:first) + instance.should respond_to(:second) + + end + + end + + describe "with parameters" do + + it "it applies a trait macro with parameters" do + + module DoesCallMethod + as_trait do |field| + send(field) + end + end + + @doing_class.should_receive(:foo) + @doing_class.class_eval do + include DoesCallMethod[:foo] + end + + end + + it "facilitates metaprogramming acrobatics" do + + module DoesDefineConstantMethod + as_trait do |name, return_value| + define_method name do + return_value + end + end + end + + @doing_class.class_eval do + include DoesDefineConstantMethod["some_method", "some_return_value"] + end + + instance = @doing_class.new + instance.should respond_to(:some_method) + instance.some_method.should == "some_return_value" + end + + it "allies to call an unparametrized trait macro with an empty parameter list" do + + module DoesSome + as_trait do + some_trait_included + end + end + + @doing_class.should_receive(:some_trait_included) + + @doing_class.class_eval do + include DoesSome[] + end + + end + + it 'appends methods outside the trait macro' do + + module HybridModuleWithParameters + + as_trait do |name| + define_method name do + end + end + + def vanilla_method + end + + end + + @doing_class.class_eval do + include HybridModuleWithParameters[:trait_method] + end + + instance = @doing_class.new + + instance.should respond_to(:trait_method) + instance.should respond_to(:vanilla_method) + + end + + it 'passes keyword args to the block given to as_trait' do + + module ModuleWithKeywordArgs + as_trait do |hash, required_kwarg:, optional_kwarg: 'foo'| + define_method :passed_hash do + hash + end + + define_method :required_keyword do + required_kwarg + end + + define_method :optional_keyword do + optional_kwarg + end + + end + end + + @doing_class.class_eval do + include ModuleWithKeywordArgs[{ first_hash_key: 'value_one', second_hash_key: 'value_two' }, required_kwarg: 'bar'] + end + + instance = @doing_class.new + instance.passed_hash.should eq({ first_hash_key: 'value_one', second_hash_key: 'value_two' }) + instance.required_keyword.should eq('bar') + instance.optional_keyword.should eq('foo') + end + + end + + end + +end diff --git a/spec/rcov.opts b/spec/rcov.opts deleted file mode 100755 index 274ed51..0000000 --- a/spec/rcov.opts +++ /dev/null @@ -1,2 +0,0 @@ ---exclude "spec/*,gems/*" ---rails \ No newline at end of file diff --git a/spec/spec.opts b/spec/spec.opts deleted file mode 100755 index 391705b..0000000 --- a/spec/spec.opts +++ /dev/null @@ -1,4 +0,0 @@ ---colour ---format progress ---loadby mtime ---reverse diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 9669a3f..b86b309 100755 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,3 +1,6 @@ -$: << File.join(File.dirname(__FILE__), "/../lib" ) +require 'modularity' -require "#{File.dirname(__FILE__)}/../lib/modularity" +RSpec.configure do |config| + config.expect_with(:rspec) { |expects| expects.syntax = :should } + config.mock_with(:rspec) { |mocks| mocks.syntax = [:should, :receive] } +end