New Upstream Snapshot - ruby-docile
Ready changes
Summary
Merged new upstream version: 1.4.0+git20220423.1.08cf67d (was: 1.1.5).
Resulting package
Built on 2022-10-21T09:34 (took 3m17s)
The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:
apt install -t fresh-snapshots ruby-docile
Diff
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..2fd1771
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,15 @@
+version: 2
+
+updates:
+
+ # Maintain dependencies for GitHub Actions
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "daily"
+
+ # Maintain dependencies for Ruby's Bundler
+ - package-ecosystem: "bundler"
+ directory: "/"
+ schedule:
+ interval: "daily"
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..982b44e
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,42 @@
+name: Main
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches:
+ - main
+jobs:
+ test:
+ name: 'CI Tests'
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest]
+ # Due to https://github.com/actions/runner/issues/849, we have to use quotes for '3.0'
+ ruby: [jruby, truffleruby, 2.5, 2.6, 2.7, '3.0', 3.1, head]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - uses: actions/checkout@v3.0.2
+
+ # Conditionally configure bundler via environment variables as advised
+ # * https://github.com/ruby/setup-ruby#bundle-config
+ - name: Set bundler environment variables
+ run: |
+ echo "BUNDLE_WITHOUT=checks" >> $GITHUB_ENV
+ if: matrix.ruby != 3.1
+
+ - uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: ${{ matrix.ruby }}
+ bundler-cache: true
+
+ - run: bundle exec rspec
+
+ - uses: codecov/codecov-action@v3.1.0
+ with:
+ name: ${{ matrix.ruby }}
+ file: ./coverage/coverage.xml
+
+ - run: bundle exec rubocop
+ if: matrix.ruby == 3.1
diff --git a/.gitignore b/.gitignore
index 3604ac0..b1261ee 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,4 +6,6 @@ pkg
doc
.yardoc
coverage
-vendor
\ No newline at end of file
+vendor
+.ruby-gemset
+.ruby-version
diff --git a/.rubocop.yml b/.rubocop.yml
new file mode 100644
index 0000000..c3fc127
--- /dev/null
+++ b/.rubocop.yml
@@ -0,0 +1,2 @@
+inherit_gem:
+ panolint: panolint-rubocop.yml
diff --git a/.ruby-gemset b/.ruby-gemset
deleted file mode 100644
index 960d9cf..0000000
--- a/.ruby-gemset
+++ /dev/null
@@ -1 +0,0 @@
-docile
diff --git a/.ruby-version b/.ruby-version
deleted file mode 100644
index 9304515..0000000
--- a/.ruby-version
+++ /dev/null
@@ -1 +0,0 @@
-ruby-2.1.0
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 85909c2..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,21 +0,0 @@
-language: ruby
-cache: bundler
-rvm:
- - ruby-head
- - 2.1.2
- - 2.1.1
- - 2.1.0
- - 2.0.0
- - 1.9.3
- - 1.9.2
- - 1.8.7
- - ree
- - jruby-head
- - jruby-19mode
- - jruby-18mode
- - rbx-2
-matrix:
- allow_failures:
- - rvm: ruby-head
- - rvm: jruby-head
- fast_finish: true
diff --git a/Gemfile b/Gemfile
index aabeeca..43b9e22 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,4 +1,26 @@
-source 'https://rubygems.org'
+# frozen_string_literal: true
-# Specify gem's dependencies in docile.gemspec
+source "https://rubygems.org"
+
+# Specify gem's runtime dependencies in docile.gemspec
gemspec
+
+group :test do
+ gem "rspec", "~> 3.10"
+ gem "simplecov", require: false
+
+ # CI-only test dependencies go here
+ if ENV.fetch("CI", nil) == "true"
+ gem "simplecov-cobertura", require: false, group: "test"
+ end
+end
+
+# Excluded from CI except on latest MRI Ruby, to reduce compatibility burden
+group :checks do
+ gem "panolint", github: "panorama-ed/panolint", branch: "main"
+end
+
+# Optional, only used locally to release to rubygems.org
+group :release, optional: true do
+ gem "rake"
+end
diff --git a/HISTORY.md b/HISTORY.md
index 22ac340..3484013 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -1,5 +1,71 @@
# HISTORY
+## [Unreleased changes](http://github.com/ms-ati/docile/compare/v1.4.0...main)
+
+## [v1.4.0 (May 12, 2021)](http://github.com/ms-ati/docile/compare/v1.3.5...v1.4.0)
+
+ - Special thanks to Matt Schreiber (@tomeon):
+ - Short-circuit to calling #instance_exec directly on the DSL object (prior to
+ constructing a proxy object) when the DSL object and block context object are
+ identical (*Sorry it took over a year to review and merge this!*)
+ - Renamed default branch from master to main, see: https://github.com/github/renaming
+ - Temporarily removed YARD doc configuration, to replace after
+ migration to Github Actions
+ - Removed support for all EOL Rubies < 2.5
+ - Migrated CI from Travis to Github Actions
+ - Special thanks (again!) to Taichi Ishitani (@taichi-ishitani):
+ - Use more reliable codecov github action (via simplecov-cobertura)
+ rather than less reliable codecov gem
+ - Enable bundle caching in github action setup-ruby
+ - Added Rubocop, and configured it to run in CI
+ - Added Dependabot, and configured it to run daily
+ - Added SECURITY.md for vulnerability reporting policy
+
+## [v1.3.5 (Jan 13, 2021)](http://github.com/ms-ati/docile/compare/v1.3.4...v1.3.5)
+
+ - Special thanks to Jochen Seeber (@jochenseeber):
+ - Fix remaining delegation on Ruby 2.7 (PR #62)
+ - Remove support for Ruby 1.8.7 and REE, because they
+ [are no longer runnable on Travis CI](https://travis-ci.community/t/ruby-1-8-7-and-ree-builds-broken-by-ssl-certificate-failure/10866)
+ - Announce that continued support for any EOL Ruby versions (that is, versions
+ prior to Ruby 2.5 as of Jan 13 2021) will be decided on **Feb 1, 2021**
+ based on comments to [issue #58](https://github.com/ms-ati/docile/issues/58)
+
+## [v1.3.4 (Dec 22, 2020)](http://github.com/ms-ati/docile/compare/v1.3.3...v1.3.4)
+
+ - Special thanks to Benoit Daloze (@eregon):
+ - Fix delegation on Ruby 2.7 (issues #45 and #44, PR #52)
+
+## [v1.3.3 (Dec 18, 2020)](http://github.com/ms-ati/docile/compare/v1.3.2...v1.3.3)
+
+ - Special thanks (again!) to Taichi Ishitani (@taichi-ishitani):
+ - Fix keyword arg warnings on Ruby 2.7 (issue #44, PR #45)
+ - Filter Docile's source files from backtrace (issue #35, PR #36)
+
+## [v1.3.2 (Jun 12, 2019)](http://github.com/ms-ati/docile/compare/v1.3.1...v1.3.2)
+
+ - Special thanks (again!) to Taichi Ishitani (@taichi-ishitani):
+ - Fix for DSL object is replaced when #dsl_eval is nested (#33, PR #34)
+
+## [v1.3.1 (May 24, 2018)](http://github.com/ms-ati/docile/compare/v1.3.0...v1.3.1)
+
+ - Special thanks to Taichi Ishitani (@taichi-ishitani):
+ - Fix for when DSL object is also the block's context (#30)
+
+## [v1.3.0 (Feb 7, 2018)](http://github.com/ms-ati/docile/compare/v1.2.0...v1.3.0)
+
+ - Allow helper methods in block's context to call DSL methods
+ - Add SemVer release policy explicitly
+ - Standardize on double-quoted string literals
+ - Workaround some more Travis CI shenanigans
+
+## [v1.2.0 (Jan 11, 2018)](http://github.com/ms-ati/docile/compare/v1.1.5...v1.2.0)
+
+ - Special thanks to Christina Koller (@cmkoller)
+ - add DSL evaluation returning *return value of the block* (see `.dsl_eval_with_block_return`)
+ - add an example to README
+ - keep travis builds passing on old ruby versions
+
## [v1.1.5 (Jun 15, 2014)](http://github.com/ms-ati/docile/compare/v1.1.4...v1.1.5)
- as much as possible, loosen version restrictions on development dependencies
diff --git a/LICENSE b/LICENSE
index 26593ee..e62ec46 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
The MIT License (MIT)
-Copyright (c) 2012-2014 Marc Siegel
+Copyright (c) 2012-2022 Marc Siegel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 098e0a4..48439c7 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,14 @@
# Docile
-[![Gem Version](https://badge.fury.io/rb/docile.png)](http://badge.fury.io/rb/docile)
-[![Build Status](https://travis-ci.org/ms-ati/docile.png)](https://travis-ci.org/ms-ati/docile)
-[![Dependency Status](https://gemnasium.com/ms-ati/docile.png)](https://gemnasium.com/ms-ati/docile)
-[![Code Climate](https://codeclimate.com/github/ms-ati/docile.png)](https://codeclimate.com/github/ms-ati/docile)
-[![Coverage Status](https://coveralls.io/repos/ms-ati/docile/badge.png)](https://coveralls.io/r/ms-ati/docile)
-[![Inline docs](http://inch-ci.org/github/ms-ati/docile.png)](http://inch-ci.org/github/ms-ati/docile)
-[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/ms-ati/docile/trend.png)](https://bitdeli.com/free "Bitdeli Badge")
+
+[![Gem Version](https://img.shields.io/gem/v/docile.svg)](https://rubygems.org/gems/docile)
+[![Gem Downloads](https://img.shields.io/gem/dt/docile.svg)](https://rubygems.org/gems/docile)
+
+[![Join the chat at https://gitter.im/ms-ati/docile](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/ms-ati/docile?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+[![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://rubydoc.info/github/ms-ati/docile)
+
+[![Build Status](https://github.com/ms-ati/docile/actions/workflows/main.yml/badge.svg)](https://github.com/ms-ati/docile/actions/workflows/main.yml)
+[![Code Coverage](https://img.shields.io/codecov/c/github/ms-ati/docile.svg)](https://codecov.io/github/ms-ati/docile)
+[![Maintainability](https://api.codeclimate.com/v1/badges/79ca631bc123f7b83b34/maintainability)](https://codeclimate.com/github/ms-ati/docile/maintainability)
Ruby makes it possible to create very expressive **Domain Specific
Languages**, or **DSL**'s for short. However, it requires some deep knowledge and
@@ -20,7 +23,7 @@ coding a bit more docile...
## Usage
-### Basic
+### Basic: Ruby [Array](http://ruby-doc.org/core-3.0.0/Array.html) as DSL
Let's say that we want to make a DSL for modifying Array objects.
Wouldn't it be great if we could just treat the methods of Array as a DSL?
@@ -45,7 +48,80 @@ end
Easy!
-### Advanced
+### Next step: Allow helper methods to call DSL methods
+
+What if, in our use of the methods of Array as a DSL, we want to extract
+helper methods which in turn call DSL methods?
+
+```ruby
+def pop_sum_and_push(n)
+ sum = 0
+ n.times { sum += pop }
+ push sum
+end
+
+Docile.dsl_eval([]) do
+ push 5
+ push 6
+ pop_sum_and_push(2)
+end
+#=> [11]
+```
+
+Without Docile, you may find this sort of code extraction to be more
+challenging.
+
+### Wait! Can't I do that with just `instance_eval` or `instance_exec`?
+
+Good question!
+
+In short: **No**.
+
+Not if you want the code in the block to be able to refer to anything
+the block would normally have access to from the surrounding context.
+
+Let's be very specific. Docile internally uses `instance_exec` (see [execution.rb#26](lib/docile/execution.rb#L26)), adding a small layer to support referencing *local variables*, *instance variables*, and *methods* from the _block's context_ **or** the target _object's context_, interchangeably. This is "**the hard part**", where most folks making a DSL in Ruby throw up their hands.
+
+For example:
+
+```ruby
+class ContextOfBlock
+ def example_of_contexts
+ @block_instance_var = 1
+ block_local_var = 2
+
+ with_array do
+ push @block_instance_var
+ push block_local_var
+ pop
+ push block_sees_this_method
+ end
+ end
+
+ def block_sees_this_method
+ 3
+ end
+
+ def with_array(&block)
+ {
+ docile: Docile.dsl_eval([], &block),
+ instance_eval: ([].instance_eval(&block) rescue $!),
+ instance_exec: ([].instance_exec(&block) rescue $!)
+ }
+ end
+end
+
+ContextOfBlock.new.example_of_contexts
+#=> {
+ :docile=>[1, 3],
+ :instance_eval=>#<NameError: undefined local variable or method `block_sees_this_method' for [nil]:Array>,
+ :instance_exec=>#<NameError: undefined local variable or method `block_sees_this_method' for [nil]:Array>
+ }
+```
+
+As you can see, it won't be possible to call methods or access instance variables defined in the block's context using just the raw `instance_eval` or `instance_exec` methods. And in fact, Docile goes further, making it easy to maintain this support even in multi-layered DSLs.
+
+### Build a Pizza
Mutating (changing) an Array instance is fine, but what usually makes a good DSL is a [Builder Pattern][2].
@@ -83,7 +159,7 @@ PizzaBuilder.new.cheese.pepperoni.sauce(:extra).build
Then implement your DSL like this:
-``` ruby
+```ruby
def pizza(&block)
Docile.dsl_eval(PizzaBuilder.new, &block).build
end
@@ -93,6 +169,38 @@ It's just that easy!
[2]: http://stackoverflow.com/questions/328496/when-would-you-use-the-builder-pattern "Builder Pattern"
+### Multi-level and Recursive DSLs
+
+Docile is a very easy way to write a multi-level DSL in Ruby, even for
+a [recursive data structure such as a tree][4]:
+
+```ruby
+Person = Struct.new(:name, :mother, :father)
+
+person {
+ name 'John Smith'
+ mother {
+ name 'Mary Smith'
+ }
+ father {
+ name 'Tom Smith'
+ mother {
+ name 'Jane Smith'
+ }
+ }
+}
+
+#=> #<struct Person name="John Smith",
+# mother=#<struct Person name="Mary Smith", mother=nil, father=nil>,
+# father=#<struct Person name="Tom Smith",
+# mother=#<struct Person name="Jane Smith", mother=nil, father=nil>,
+# father=nil>>
+```
+
+See the full [person tree example][4] for details.
+
+[4]: https://gist.github.com/ms-ati/2bb17bdf10a430faba98
+
### Block parameters
Parameters can be passed to the DSL block.
@@ -153,18 +261,18 @@ end
[3]: http://www.sinatrarb.com "Sinatra"
-### Functional-Style DSL Objects
+### Functional-Style Immutable DSL Objects
Sometimes, you want to use an object as a DSL, but it doesn't quite fit the
[imperative](http://en.wikipedia.org/wiki/Imperative_programming) pattern shown
above.
Instead of methods like
-[Array#push](http://www.ruby-doc.org/core-2.0/Array.html#method-i-push), which
+[Array#push](http://www.ruby-doc.org/core-3.0.0/Array.html#method-i-push), which
modifies the object at hand, it has methods like
-[String#reverse](http://www.ruby-doc.org/core-2.0/String.html#method-i-reverse),
+[String#reverse](http://www.ruby-doc.org/core-3.0.0/String.html#method-i-reverse),
which returns a new object without touching the original. Perhaps it's even
-[frozen](http://www.ruby-doc.org/core-2.0/Object.html#method-i-freeze) in
+[frozen](http://www.ruby-doc.org/core-3.0.0/Object.html#method-i-freeze) in
order to enforce [immutability](http://en.wikipedia.org/wiki/Immutable_object).
Wouldn't it be great if we could just treat these methods as a DSL as well?
@@ -192,6 +300,33 @@ end
All set!
+### Accessing the block's return value
+
+Sometimes you might want to access the return value of your provided block,
+as opposed to the DSL object itself. In these cases, use
+`dsl_eval_with_block_return`. It behaves exactly like `dsl_eval`, but returns
+the output from executing the block, rather than the DSL object.
+
+```ruby
+arr = []
+with_array(arr) do
+ push "a"
+ push "b"
+ push "c"
+ length
+end
+#=> 3
+
+arr
+#=> ["a", "b", "c"]
+```
+
+```ruby
+def with_array(arr=[], &block)
+ Docile.dsl_eval_with_block_return(arr, &block)
+end
+```
+
## Features
1. Method lookup falls back from the DSL object to the block's context
@@ -215,9 +350,19 @@ $ gem install docile
## Status
-Works on [all ruby versions since 1.8.7](https://github.com/ms-ati/docile/blob/master/.travis.yml), or so Travis CI [tells us](https://travis-ci.org/ms-ati/docile).
+Works on [all currently supported ruby versions](https://github.com/ms-ati/docile/blob/master/.github/workflows/main.yml),
+or so [Github Actions](https://github.com/ms-ati/docile/actions)
+tells us.
-Used by some pretty cool gems to implement their DSLs, notably including [SimpleCov](https://github.com/colszowka/simplecov). Keep an eye out for new gems using Docile at the [Ruby Toolbox](https://www.ruby-toolbox.com/projects/docile).
+Used by some pretty cool gems to implement their DSLs, notably including
+[SimpleCov](https://github.com/colszowka/simplecov). Keep an eye out for new
+gems using Docile at the
+[Ruby Toolbox](https://www.ruby-toolbox.com/projects/docile).
+
+## Release Policy
+
+Docile releases follow
+[Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html).
## Note on Patches/Pull Requests
@@ -232,10 +377,33 @@ Used by some pretty cool gems to implement their DSLs, notably including [Simple
commit by itself I can ignore when I pull)
* Send me a pull request. Bonus points for topic branches.
-## Copyright & License
+## Releasing
-Copyright (c) 2012-2014 Marc Siegel.
+To make a new release of `Docile` to
+[RubyGems](https://rubygems.org/gems/docile), first install the release
+dependencies (e.g. `rake`) as follows:
-Licensed under the [MIT License](http://choosealicense.com/licenses/mit/), see [LICENSE](LICENSE) for details.
+```shell
+bundle config set --local with 'release'
+bundle install
+```
+
+Then carry out these steps:
+
+1. Update `HISTORY.md`:
+ - Add an entry for the upcoming version _x.y.z_
+ - Move content from _Unreleased_ to the upcoming version _x.y.z_
+ - Commit with title `Update HISTORY.md for x.y.z`
+
+2. Update `lib/docile/version.rb`
+ - Replace with upcoming version _x.y.z_
+ - Commit with title `Bump version to x.y.z`
+
+3. `bundle exec rake release`
+
+## Copyright & License
+Copyright (c) 2012-2022 Marc Siegel.
+Licensed under the [MIT License](http://choosealicense.com/licenses/mit/),
+see [LICENSE](LICENSE) for details.
diff --git a/Rakefile b/Rakefile
index 5528825..83e2b67 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,28 +1,14 @@
-require 'rake/clean'
-require 'bundler/gem_tasks'
-require 'rspec/core/rake_task'
-require File.expand_path('on_what', File.dirname(__FILE__))
+# frozen_string_literal: true
+
+require "rake/clean"
+require "bundler/gem_tasks"
+require "rspec/core/rake_task"
# Default task for `rake` is to run rspec
-task :default => [:spec]
+task default: [:spec]
# Use default rspec rake task
RSpec::Core::RakeTask.new
# Configure `rake clobber` to delete all generated files
-CLOBBER.include('pkg', 'doc', 'coverage')
-
-# To limit needed compatibility with versions of dependencies, only configure
-# yard doc generation when *not* on Travis, JRuby, or 1.8
-if !on_travis? && !on_jruby? && !on_1_8?
- require 'github/markup'
- require 'redcarpet'
- require 'yard'
- require 'yard/rake/yardoc_task'
-
- YARD::Rake::YardocTask.new do |t|
- OTHER_PATHS = %w()
- t.files = ['lib/**/*.rb', OTHER_PATHS]
- t.options = %w(--markup-provider=redcarpet --markup=markdown --main=README.md)
- end
-end
+CLOBBER.include("pkg", "doc", "coverage")
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..947561c
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,19 @@
+# Security Policy
+
+## Supported Versions
+
+Use this section to tell people about which versions of your project are
+currently being supported with security updates.
+
+| Version | Supported |
+| ------- | ------------------ |
+| 1.4.x | :white_check_mark: |
+| 1.3.x | :white_check_mark: |
+| < 1.3 | :x: |
+
+## Reporting a Vulnerability
+
+At this time, security issues and vulnerabilities in Docile should
+be reported like any other issue. Please create an issue in the
+[public issue tracker](https://github.com/ms-ati/docile/issues) on
+Github.
diff --git a/checksums.yaml.gz b/checksums.yaml.gz
deleted file mode 100644
index bf864f2..0000000
Binary files a/checksums.yaml.gz and /dev/null differ
diff --git a/debian/changelog b/debian/changelog
index 5f42443..ae644a6 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,4 +1,4 @@
-ruby-docile (1.1.5-3) UNRELEASED; urgency=medium
+ruby-docile (1.4.0+git20220423.1.08cf67d-1) UNRELEASED; urgency=medium
[ Utkarsh Gupta ]
* Add salsa-ci.yml
@@ -12,8 +12,9 @@ ruby-docile (1.1.5-3) UNRELEASED; urgency=medium
* Update Vcs-* headers from URL redirect.
* Use canonical URL in Vcs-Git.
* Bump debhelper from old 12 to 13.
+ * New upstream snapshot.
- -- Utkarsh Gupta <guptautkarsh2102@gmail.com> Tue, 13 Aug 2019 04:09:40 +0530
+ -- Utkarsh Gupta <guptautkarsh2102@gmail.com> Fri, 21 Oct 2022 09:32:50 -0000
ruby-docile (1.1.5-2) unstable; urgency=medium
diff --git a/docile.gemspec b/docile.gemspec
index 4ebc077..01444e6 100644
--- a/docile.gemspec
+++ b/docile.gemspec
@@ -1,43 +1,36 @@
-require File.expand_path('on_what', File.dirname(__FILE__))
-$:.push File.expand_path('../lib', __FILE__)
-require 'docile/version'
+# frozen_string_literal: true
+
+require_relative "lib/docile/version"
Gem::Specification.new do |s|
- s.name = 'docile'
+ s.name = "docile"
s.version = Docile::VERSION
- s.author = 'Marc Siegel'
- s.email = 'marc@usainnov.com'
- s.homepage = 'https://ms-ati.github.io/docile/'
- s.summary = 'Docile keeps your Ruby DSLs tame and well-behaved'
- s.description = 'Docile turns any Ruby object into a DSL. Especially useful with the Builder pattern.'
- s.license = 'MIT'
-
- # Files included in the gem
- 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 = %w(lib)
+ s.author = "Marc Siegel"
+ s.email = "marc@usainnov.com"
+ s.homepage = "https://ms-ati.github.io/docile/"
+ s.summary = "Docile keeps your Ruby DSLs tame and well-behaved."
+ s.description = "Docile treats the methods of a given ruby object as a DSL " \
+ "(domain specific language) within a given block. \n\n" \
+ "Killer feature: you can also reference methods, instance " \
+ "variables, and local variables from the original (non-DSL) "\
+ "context within the block. \n\n" \
+ "Docile releases follow Semantic Versioning as defined at " \
+ "semver.org."
+ s.license = "MIT"
- # Specify oldest supported Ruby version
- s.required_ruby_version = '>= 1.8.7'
+ # Specify oldest supported Ruby version (2.5 to support JRuby 9.2.17.0)
+ s.required_ruby_version = ">= 2.5.0"
- # Run rspec tests from rake
- s.add_development_dependency 'rake'
- s.add_development_dependency 'rspec', '~> 3.0.0'
-
- # NOTE: needed for Travis builds on 1.8, but can't yet reproduce failure locally
- s.add_development_dependency 'mime-types', '~> 1.25.1' if on_1_8?
-
- # To limit needed compatibility with versions of dependencies, only configure
- # yard doc generation when *not* on Travis, JRuby, or 1.8
- if !on_travis? && !on_jruby? && !on_1_8?
- # Github flavored markdown in YARD documentation
- # http://blog.nikosd.com/2011/11/github-flavored-markdown-in-yard.html
- s.add_development_dependency 'yard'
- s.add_development_dependency 'redcarpet'
- s.add_development_dependency 'github-markup'
+ # Files included in the gem
+ s.files = `git ls-files -z`.split("\x0").reject do |f|
+ f.match(%r{^(test|spec|features)/})
end
+ s.require_paths = ["lib"]
- # Coveralls test coverage tool, basically hosted SimpleCov
- s.add_development_dependency 'coveralls'
+ s.metadata = {
+ "homepage_uri" => "https://ms-ati.github.io/docile/",
+ "changelog_uri" => "https://github.com/ms-ati/docile/blob/main/HISTORY.md",
+ "source_code_uri" => "https://github.com/ms-ati/docile",
+ "rubygems_mfa_required" => "true",
+ }
end
diff --git a/lib/docile.rb b/lib/docile.rb
index dc51096..90e6e84 100644
--- a/lib/docile.rb
+++ b/lib/docile.rb
@@ -1,7 +1,10 @@
-require 'docile/version'
-require 'docile/execution'
-require 'docile/fallback_context_proxy'
-require 'docile/chaining_fallback_context_proxy'
+# frozen_string_literal: true
+
+require "docile/version"
+require "docile/execution"
+require "docile/fallback_context_proxy"
+require "docile/chaining_fallback_context_proxy"
+require "docile/backtrace_filter"
# Docile keeps your Ruby DSLs tame and well-behaved.
module Docile
@@ -43,8 +46,53 @@ module Docile
exec_in_proxy_context(dsl, FallbackContextProxy, *args, &block)
dsl
end
+
+ ruby2_keywords :dsl_eval if respond_to?(:ruby2_keywords, true)
module_function :dsl_eval
+ # Execute a block in the context of an object whose methods represent the
+ # commands in a DSL, and return *the block's return value*.
+ #
+ # @note Use with an *imperative* DSL (commands modify the context object)
+ #
+ # Use this method to execute an *imperative* DSL, which means that:
+ #
+ # 1. Each command mutates the state of the DSL context object
+ # 2. The return value of each command is ignored
+ # 3. The final return value is the original context object
+ #
+ # @example Use a String as a DSL
+ # Docile.dsl_eval_with_block_return("Hello, world!") do
+ # reverse!
+ # upcase!
+ # first
+ # end
+ # #=> "!"
+ #
+ # @example Use an Array as a DSL
+ # Docile.dsl_eval_with_block_return([]) do
+ # push "a"
+ # push "b"
+ # pop
+ # push "c"
+ # length
+ # end
+ # #=> 2
+ #
+ # @param dsl [Object] context object whose methods make up the DSL
+ # @param args [Array] arguments to be passed to the block
+ # @param block [Proc] the block of DSL commands to be executed against the
+ # `dsl` context object
+ # @return [Object] the return value from executing the block
+ def dsl_eval_with_block_return(dsl, *args, &block)
+ exec_in_proxy_context(dsl, FallbackContextProxy, *args, &block)
+ end
+
+ if respond_to?(:ruby2_keywords, true)
+ ruby2_keywords :dsl_eval_with_block_return
+ end
+ module_function :dsl_eval_with_block_return
+
# Execute a block in the context of an immutable object whose methods,
# and the methods of their return values, represent the commands in a DSL.
#
@@ -80,5 +128,7 @@ module Docile
def dsl_eval_immutable(dsl, *args, &block)
exec_in_proxy_context(dsl, ChainingFallbackContextProxy, *args, &block)
end
+
+ ruby2_keywords :dsl_eval_immutable if respond_to?(:ruby2_keywords, true)
module_function :dsl_eval_immutable
end
diff --git a/lib/docile/backtrace_filter.rb b/lib/docile/backtrace_filter.rb
new file mode 100644
index 0000000..06a6605
--- /dev/null
+++ b/lib/docile/backtrace_filter.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Docile
+ # @api private
+ #
+ # This is used to remove entries pointing to Docile's source files
+ # from {Exception#backtrace} and {Exception#backtrace_locations}.
+ #
+ # If {NoMethodError} is caught then the exception object will be extended
+ # by this module to add filter functionalities.
+ module BacktraceFilter
+ FILTER_PATTERN = %r{/lib/docile/}.freeze
+
+ def backtrace
+ super.grep_v(FILTER_PATTERN)
+ end
+
+ if ::Exception.public_method_defined?(:backtrace_locations)
+ def backtrace_locations
+ super.reject { |location| location.absolute_path =~ FILTER_PATTERN }
+ end
+ end
+ end
+end
diff --git a/lib/docile/chaining_fallback_context_proxy.rb b/lib/docile/chaining_fallback_context_proxy.rb
index 20d0b63..4fea047 100644
--- a/lib/docile/chaining_fallback_context_proxy.rb
+++ b/lib/docile/chaining_fallback_context_proxy.rb
@@ -1,4 +1,6 @@
-require 'docile/fallback_context_proxy'
+# frozen_string_literal: true
+
+require "docile/fallback_context_proxy"
module Docile
# @api private
@@ -10,11 +12,16 @@ module Docile
# objects.
#
# @see Docile.dsl_eval_immutable
+ #
+ # rubocop:disable Style/MissingRespondToMissing
class ChainingFallbackContextProxy < FallbackContextProxy
# Proxy methods as in {FallbackContextProxy#method_missing}, replacing
# `receiver` with the returned value.
def method_missing(method, *args, &block)
@__receiver__ = super(method, *args, &block)
end
+
+ ruby2_keywords :method_missing if respond_to?(:ruby2_keywords, true)
end
-end
\ No newline at end of file
+ # rubocop:enable Style/MissingRespondToMissing
+end
diff --git a/lib/docile/execution.rb b/lib/docile/execution.rb
index f5696b9..e8a6408 100644
--- a/lib/docile/execution.rb
+++ b/lib/docile/execution.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Docile
# @api private
#
@@ -15,21 +17,37 @@ module Docile
# @param block [Proc] the block of DSL commands to be executed
# @return [Object] the return value of the block
def exec_in_proxy_context(dsl, proxy_type, *args, &block)
- block_context = eval('self', block.binding)
+ block_context = eval("self", block.binding) # rubocop:disable Style/EvalWithLocation
+
+ # Use #equal? to test strict object identity (assuming that this dictum
+ # from the Ruby docs holds: "[u]nlike ==, the equal? method should never
+ # be overridden by subclasses as it is used to determine object
+ # identity")
+ return dsl.instance_exec(*args, &block) if dsl.equal?(block_context)
+
proxy_context = proxy_type.new(dsl, block_context)
begin
block_context.instance_variables.each do |ivar|
value_from_block = block_context.instance_variable_get(ivar)
proxy_context.instance_variable_set(ivar, value_from_block)
end
+
proxy_context.instance_exec(*args, &block)
ensure
+ if block_context.respond_to?(:__docile_undo_fallback__)
+ block_context.send(:__docile_undo_fallback__)
+ end
+
block_context.instance_variables.each do |ivar|
+ next unless proxy_context.instance_variables.include?(ivar)
+
value_from_dsl_proxy = proxy_context.instance_variable_get(ivar)
block_context.instance_variable_set(ivar, value_from_dsl_proxy)
end
end
end
+
+ ruby2_keywords :exec_in_proxy_context if respond_to?(:ruby2_keywords, true)
module_function :exec_in_proxy_context
end
-end
\ No newline at end of file
+end
diff --git a/lib/docile/fallback_context_proxy.rb b/lib/docile/fallback_context_proxy.rb
index a865d93..18a2e0e 100644
--- a/lib/docile/fallback_context_proxy.rb
+++ b/lib/docile/fallback_context_proxy.rb
@@ -1,4 +1,6 @@
-require 'set'
+# frozen_string_literal: true
+
+require "set"
module Docile
# @api private
@@ -13,14 +15,20 @@ module Docile
# This is useful for implementing DSL evaluation in the context of an object.
#
# @see Docile.dsl_eval
+ #
+ # rubocop:disable Style/MissingRespondToMissing
class FallbackContextProxy
# The set of methods which will **not** be proxied, but instead answered
# by this object directly.
NON_PROXIED_METHODS = Set[:__send__, :object_id, :__id__, :==, :equal?,
- :'!', :'!=', :instance_exec, :instance_variables,
+ :!, :!=, :instance_exec, :instance_variables,
:instance_variable_get, :instance_variable_set,
:remove_instance_variable]
+ # The set of methods which will **not** fallback from the block's context
+ # to the dsl object.
+ NON_FALLBACK_METHODS = Set[:class, :self, :respond_to?, :instance_of?]
+
# The set of instance variables which are local to this object and hidden.
# All other instance variables will be copied in and out of this object
# from the scope in which this proxy was created.
@@ -38,16 +46,44 @@ module Docile
def initialize(receiver, fallback)
@__receiver__ = receiver
@__fallback__ = fallback
+
+ # Enables calling DSL methods from helper methods in the block's context
+ unless fallback.respond_to?(:method_missing)
+ # NOTE: We could switch to {#define_singleton_method} on current Rubies
+ singleton_class = (class << fallback; self; end)
+
+ # instrument {#method_missing} on the block's context to fallback to
+ # the DSL object. This allows helper methods in the block's context to
+ # contain calls to methods on the DSL object.
+ singleton_class.
+ send(:define_method, :method_missing) do |method, *args, &block|
+ m = method.to_sym
+ if !NON_FALLBACK_METHODS.member?(m) &&
+ !fallback.respond_to?(m) &&
+ receiver.respond_to?(m)
+ receiver.__send__(method.to_sym, *args, &block)
+ else
+ super(method, *args, &block)
+ end
+ end
+
+ if singleton_class.respond_to?(:ruby2_keywords, true)
+ singleton_class.send(:ruby2_keywords, :method_missing)
+ end
+
+ # instrument a helper method to remove the above instrumentation
+ singleton_class.
+ send(:define_method, :__docile_undo_fallback__) do
+ singleton_class.send(:remove_method, :method_missing)
+ singleton_class.send(:remove_method, :__docile_undo_fallback__)
+ end
+ end
end
# @return [Array<Symbol>] Instance variable names, excluding
# {NON_PROXIED_INSTANCE_VARIABLES}
- #
- # @note on Ruby 1.8.x, the instance variable names are actually of
- # type `String`.
def instance_variables
- # Ruby 1.8.x returns string names, convert to symbols for compatibility
- super.select { |v| !NON_PROXIED_INSTANCE_VARIABLES.include?(v.to_sym) }
+ super.reject { |v| NON_PROXIED_INSTANCE_VARIABLES.include?(v) }
end
# Proxy all methods, excluding {NON_PROXIED_METHODS}, first to `receiver`
@@ -56,8 +92,16 @@ module Docile
if @__receiver__.respond_to?(method.to_sym)
@__receiver__.__send__(method.to_sym, *args, &block)
else
- @__fallback__.__send__(method.to_sym, *args, &block)
+ begin
+ @__fallback__.__send__(method.to_sym, *args, &block)
+ rescue NoMethodError => e
+ e.extend(BacktraceFilter)
+ raise e
+ end
end
end
+
+ ruby2_keywords :method_missing if respond_to?(:ruby2_keywords, true)
end
+ # rubocop:enable Style/MissingRespondToMissing
end
diff --git a/lib/docile/version.rb b/lib/docile/version.rb
index 9614b34..02b3354 100644
--- a/lib/docile/version.rb
+++ b/lib/docile/version.rb
@@ -1,4 +1,6 @@
+# frozen_string_literal: true
+
module Docile
# The current version of this library
- VERSION = '1.1.5'
+ VERSION = "1.4.0"
end
diff --git a/metadata.yml b/metadata.yml
deleted file mode 100644
index 4196e9e..0000000
--- a/metadata.yml
+++ /dev/null
@@ -1,151 +0,0 @@
---- !ruby/object:Gem::Specification
-name: docile
-version: !ruby/object:Gem::Version
- version: 1.1.5
-platform: ruby
-authors:
-- Marc Siegel
-autorequire:
-bindir: bin
-cert_chain: []
-date: 2014-06-15 00:00:00.000000000 Z
-dependencies:
-- !ruby/object:Gem::Dependency
- name: rake
- requirement: !ruby/object:Gem::Requirement
- requirements:
- - - ">="
- - !ruby/object:Gem::Version
- version: '0'
- type: :development
- prerelease: false
- version_requirements: !ruby/object:Gem::Requirement
- requirements:
- - - ">="
- - !ruby/object:Gem::Version
- version: '0'
-- !ruby/object:Gem::Dependency
- name: rspec
- requirement: !ruby/object:Gem::Requirement
- requirements:
- - - "~>"
- - !ruby/object:Gem::Version
- version: 3.0.0
- type: :development
- prerelease: false
- version_requirements: !ruby/object:Gem::Requirement
- requirements:
- - - "~>"
- - !ruby/object:Gem::Version
- version: 3.0.0
-- !ruby/object:Gem::Dependency
- name: yard
- requirement: !ruby/object:Gem::Requirement
- requirements:
- - - ">="
- - !ruby/object:Gem::Version
- version: '0'
- type: :development
- prerelease: false
- version_requirements: !ruby/object:Gem::Requirement
- requirements:
- - - ">="
- - !ruby/object:Gem::Version
- version: '0'
-- !ruby/object:Gem::Dependency
- name: redcarpet
- requirement: !ruby/object:Gem::Requirement
- requirements:
- - - ">="
- - !ruby/object:Gem::Version
- version: '0'
- type: :development
- prerelease: false
- version_requirements: !ruby/object:Gem::Requirement
- requirements:
- - - ">="
- - !ruby/object:Gem::Version
- version: '0'
-- !ruby/object:Gem::Dependency
- name: github-markup
- requirement: !ruby/object:Gem::Requirement
- requirements:
- - - ">="
- - !ruby/object:Gem::Version
- version: '0'
- type: :development
- prerelease: false
- version_requirements: !ruby/object:Gem::Requirement
- requirements:
- - - ">="
- - !ruby/object:Gem::Version
- version: '0'
-- !ruby/object:Gem::Dependency
- name: coveralls
- requirement: !ruby/object:Gem::Requirement
- requirements:
- - - ">="
- - !ruby/object:Gem::Version
- version: '0'
- type: :development
- prerelease: false
- version_requirements: !ruby/object:Gem::Requirement
- requirements:
- - - ">="
- - !ruby/object:Gem::Version
- version: '0'
-description: Docile turns any Ruby object into a DSL. Especially useful with the Builder
- pattern.
-email: marc@usainnov.com
-executables: []
-extensions: []
-extra_rdoc_files: []
-files:
-- ".gitignore"
-- ".rspec"
-- ".ruby-gemset"
-- ".ruby-version"
-- ".travis.yml"
-- ".yardopts"
-- Gemfile
-- HISTORY.md
-- LICENSE
-- README.md
-- Rakefile
-- docile.gemspec
-- lib/docile.rb
-- lib/docile/chaining_fallback_context_proxy.rb
-- lib/docile/execution.rb
-- lib/docile/fallback_context_proxy.rb
-- lib/docile/version.rb
-- on_what.rb
-- spec/docile_spec.rb
-- spec/spec_helper.rb
-homepage: https://ms-ati.github.io/docile/
-licenses:
-- MIT
-metadata: {}
-post_install_message:
-rdoc_options: []
-require_paths:
-- lib
-required_ruby_version: !ruby/object:Gem::Requirement
- requirements:
- - - ">="
- - !ruby/object:Gem::Version
- version: 1.8.7
-required_rubygems_version: !ruby/object:Gem::Requirement
- requirements:
- - - ">="
- - !ruby/object:Gem::Version
- version: '0'
-requirements: []
-rubyforge_project:
-rubygems_version: 2.2.2
-signing_key:
-specification_version: 4
-summary: Docile keeps your Ruby DSLs tame and well-behaved
-test_files:
-- spec/docile_spec.rb
-- spec/spec_helper.rb
-has_rdoc:
diff --git a/on_what.rb b/on_what.rb
deleted file mode 100644
index b1ffa28..0000000
--- a/on_what.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# NOTE: Very simple tests for what system we are on, extracted for sharing
-# between Rakefile, gemspec, and spec_helper. Not for use in actual library.
-
-def on_travis?
- ENV['CI'] == 'true'
-end
-
-def on_jruby?
- (defined?(RUBY_ENGINE) && 'jruby' == RUBY_ENGINE)
-end
-
-def on_1_8?
- RUBY_VERSION.start_with? '1.8'
-end
\ No newline at end of file
diff --git a/spec/docile_spec.rb b/spec/docile_spec.rb
index 33bc896..e912f9b 100644
--- a/spec/docile_spec.rb
+++ b/spec/docile_spec.rb
@@ -1,11 +1,66 @@
-require 'spec_helper'
-require 'singleton'
+# frozen_string_literal: true
+require "spec_helper"
+
+# TODO: Factor single spec file into multiple specs
+# rubocop:disable RSpec/MultipleDescribes
describe Docile do
+ describe ".dsl_eval" do
+ before :each do
+ stub_const("Pizza",
+ Struct.new(:cheese, :pepperoni, :bacon, :sauce))
+
+ stub_const("PizzaBuilder", Class.new do
+ # rubocop:disable all
+ def cheese(v=true); @cheese = v; end
+ def pepperoni(v=true); @pepperoni = v; end
+ def bacon(v=true); @bacon = v; end
+ def sauce(v=nil); @sauce = v; end
+ # rubocop:enable all
+
+ def build
+ Pizza.new(!!@cheese, !!@pepperoni, !!@bacon, @sauce)
+ end
+ end)
+
+ stub_const("InnerDSL", Class.new do
+ def initialize
+ @b = "b"
+ end
- describe '.dsl_eval' do
+ attr_accessor :b
+
+ def c
+ "inner c"
+ end
+ end)
+
+ stub_const("OuterDSL", Class.new do
+ def initialize
+ @a = "a"
+ end
- context 'when DSL context object is an Array' do
+ attr_accessor :a
+
+ def c
+ "outer c"
+ end
+
+ def inner(&block)
+ Docile.dsl_eval(InnerDSL.new, &block)
+ end
+
+ def inner_with_params(param, &block)
+ Docile.dsl_eval(InnerDSL.new, param, :foo, &block)
+ end
+ end)
+ end
+
+ def outer(&block)
+ Docile.dsl_eval(OuterDSL.new, &block)
+ end
+
+ context "when DSL context object is an Array" do
let(:array) { [] }
let!(:result) { execute_dsl_against_array }
@@ -18,36 +73,28 @@ describe Docile do
end
end
- it 'executes the block against the DSL context object' do
+ it "executes the block against the DSL context object" do
expect(array).to eq([1, 3])
end
- it 'returns the DSL object after executing block against it' do
+ it "returns the DSL object after executing block against it" do
expect(result).to eq(array)
end
it "doesn't proxy #__id__" do
- Docile.dsl_eval(array) { expect(__id__).not_to eq(array.__id__) }
- end
-
- it "raises NoMethodError if the DSL object doesn't implement the method" do
- expect { Docile.dsl_eval(array) { no_such_method } }.to raise_error(NoMethodError)
+ described_class.dsl_eval(array) do
+ expect(__id__).not_to eq(array.__id__)
+ end
end
- end
- Pizza = Struct.new(:cheese, :pepperoni, :bacon, :sauce)
-
- class PizzaBuilder
- def cheese(v=true); @cheese = v; end
- def pepperoni(v=true); @pepperoni = v; end
- def bacon(v=true); @bacon = v; end
- def sauce(v=nil); @sauce = v; end
- def build
- Pizza.new(!!@cheese, !!@pepperoni, !!@bacon, @sauce)
+ it "raises NoMethodError if DSL object doesn't implement the method" do
+ expect do
+ described_class.dsl_eval(array) { no_such_method }
+ end.to raise_error(NoMethodError)
end
end
- context 'when DSL context object is a Builder pattern' do
+ context "when DSL context object is a Builder pattern" do
let(:builder) { PizzaBuilder.new }
let(:result) { execute_dsl_against_builder_and_call_build }
@@ -56,57 +103,35 @@ describe Docile do
Docile.dsl_eval(builder) do
bacon
cheese
- sauce @sauce
+ sauce @sauce # rubocop:disable RSpec/InstanceVariable
end.build
end
- it 'returns correctly built object' do
+ it "returns correctly built object" do
expect(result).to eq(Pizza.new(true, false, true, :extra))
end
end
- class InnerDSL
- def initialize; @b = 'b'; end
- attr_accessor :b
- end
-
- class OuterDSL
- def initialize; @a = 'a'; end
- attr_accessor :a
-
- def inner(&block)
- Docile.dsl_eval(InnerDSL.new, &block)
- end
-
- def inner_with_params(param, &block)
- Docile.dsl_eval(InnerDSL.new, param, :foo, &block)
- end
- end
-
- def outer(&block)
- Docile.dsl_eval(OuterDSL.new, &block)
- end
-
- context 'when given parameters for the DSL block' do
+ context "when given parameters for the DSL block" do
def parameterized(*args, &block)
Docile.dsl_eval(OuterDSL.new, *args, &block)
end
- it 'passes parameters to the block' do
- parameterized(1,2,3) do |x,y,z|
+ it "passes parameters to the block" do
+ parameterized(1, 2, 3) do |x, y, z|
expect(x).to eq(1)
expect(y).to eq(2)
expect(z).to eq(3)
end
end
- it 'finds parameters before methods' do
+ it "finds parameters before methods" do
parameterized(1) { |a| expect(a).to eq(1) }
end
- it 'find outer dsl parameters in inner dsl scope' do
- parameterized(1,2,3) do |a,b,c|
- inner_with_params(c) do |d,e|
+ it "find outer dsl parameters in inner dsl scope" do
+ parameterized(1, 2, 3) do |a, b, c|
+ inner_with_params(c) do |d, e|
expect(a).to eq(1)
expect(b).to eq(2)
expect(c).to eq(3)
@@ -117,156 +142,448 @@ describe Docile do
end
end
- class DSLWithNoMethod
- def initialize(b); @b = b; end
- attr_accessor :b
- def push_element
- @b.push 1
+ context "when block's context has helper methods which call DSL methods" do
+ subject { context.method(:factorial_as_dsl_against_array) }
+
+ let(:context) do
+ class_block_context_with_helper_methods.new(array_as_dsl)
end
- end
- context 'when DSL have NoMethod error inside' do
- it 'raise error from nil' do
- Docile.dsl_eval(DSLWithNoMethod.new(nil)) do
- expect { push_element }.to raise_error(NoMethodError, /undefined method `push' (for|on) nil:NilClass/)
+ let(:array_as_dsl) { [1, 1] }
+
+ let(:class_block_context_with_helper_methods) do
+ Class.new do
+ def initialize(array_as_dsl)
+ @array_as_dsl = array_as_dsl
+ end
+
+ # Classic dynamic programming factorial, using the methods of {Array}
+ # as a DSL to implement it, via helper methods {#calculate_factorials}
+ # and {#save_factorials} which are defined in this class, so therefore
+ # outside the block.
+ def factorial_as_dsl_against_array(num)
+ Docile.dsl_eval(@array_as_dsl) { calculate_factorials(num) }.last
+ end
+
+ # Uses the helper method {#save_factorials} below.
+ def calculate_factorials(num)
+ (2..num).each { |i| save_factorial(i) }
+ end
+
+ # Uses the methods {Array#push} and {Array#at} as a DSL from a helper
+ # method defined in the block's context. Successfully calling this
+ # proves that we can find helper methods from outside the block, and
+ # then find DSL methods from inside those helper methods.
+ def save_factorial(num)
+ push(num * at(num - 1))
+ end
+ end
+ end
+
+ it "finds DSL methods within helper method defined in block's context" do
+ # see https://en.wikipedia.org/wiki/Factorial
+ # rubocop:disable Layout/ExtraSpacing
+ [
+ [0, 1],
+ [1, 1],
+ [2, 2],
+ [3, 6],
+ [4, 24],
+ [5, 120],
+ [6, 720],
+ [7, 5_040],
+ [8, 40_320],
+ [9, 362_880],
+ [10, 3_628_800],
+ [11, 39_916_800],
+ [12, 479_001_600],
+ [13, 6_227_020_800],
+ [14, 87_178_291_200],
+ [15, 1_307_674_368_000]
+ ].each do |n, expected_factorial|
+ array_as_dsl.replace([1, 1])
+ expect(subject.call(n)).to eq expected_factorial
+ end
+ # rubocop:enable Layout/ExtraSpacing
+ end
+
+ it "removes fallback instrumentation from the DSL object after block" do
+ expect { subject.call(5) }.
+ not_to change { context.respond_to?(:method_missing) }.
+ from(false)
+ end
+
+ it "removes method to remove fallbacl from the DSL object after block" do
+ expect { subject.call(5) }.
+ not_to change { context.respond_to?(:__docile_undo_fallback__) }.
+ from(false)
+ end
+
+ context "when helper methods call methods that are undefined" do
+ let(:array_as_dsl) { "not an array" }
+
+ it "raises NoMethodError" do
+ expect { subject.call(5) }.
+ to raise_error(NoMethodError, /method `at' /)
+ end
+
+ it "removes fallback instrumentation from the DSL object after block" do
+ expect { subject.call(5) rescue nil }. # rubocop:disable Style/RescueModifier
+ not_to change { context.respond_to?(:method_missing) }.
+ from(false)
end
end
end
- context 'when DSL blocks are nested' do
+ context "when DSL have NoMethod error inside" do
+ let(:class_dsl_with_no_method) do
+ Class.new do
+ def push_element
+ nil.push(1)
+ end
+ end
+ end
+
+ it "raise NoMethodError error from nil" do
+ described_class.dsl_eval(class_dsl_with_no_method.new) do
+ expect { push_element }.
+ to raise_error(
+ NoMethodError,
+ /undefined method `push' (for|on) nil:NilClass/
+ )
+ end
+ end
+ end
- context 'method lookup' do
- it 'finds method of outer dsl in outer dsl scope' do
- outer { expect(a).to eq('a') }
+ context "when DSL blocks are nested" do
+ describe "method lookup" do
+ # rubocop:disable Style/SingleLineMethods
+ it "finds method of outer dsl in outer dsl scope" do
+ outer { expect(a).to eq("a") }
+ outer { inner {}; expect(c).to eq("outer c") } # rubocop:disable Style/Semicolon
end
- it 'finds method of inner dsl in inner dsl scope' do
- outer { inner { expect(b).to eq('b') } }
+ it "finds method of inner dsl in inner dsl scope" do
+ outer { inner { expect(b).to eq("b") } }
+ outer { inner { expect(c).to eq("inner c") } }
end
- it 'finds method of outer dsl in inner dsl scope' do
- outer { inner { expect(a).to eq('a') } }
+ it "finds method of outer dsl in inner dsl scope" do
+ outer { inner { expect(a).to eq("a") } }
end
it "finds method of block's context in outer dsl scope" do
- def c; 'c'; end
- outer { expect(c).to eq('c') }
+ def d; "d"; end
+ outer { expect(d).to eq("d") }
end
it "finds method of block's context in inner dsl scope" do
- def c; 'c'; end
- outer { inner { expect(c).to eq('c') } }
+ def d; "d"; end
+ outer { inner { expect(d).to eq("d") } }
end
- it 'finds method of outer dsl in preference to block context' do
- def a; 'not a'; end
- outer { expect(a).to eq('a') }
- outer { inner { expect(a).to eq('a') } }
+ it "finds method of outer dsl in preference to block context" do
+ def a; "not a"; end
+ outer { expect(a).to eq("a") }
+ outer { inner { expect(a).to eq("a") } }
end
+ # rubocop:enable Style/SingleLineMethods
end
- context 'local variable lookup' do
- it 'finds local variable from block context in outer dsl scope' do
- foo = 'foo'
- outer { expect(foo).to eq('foo') }
+ describe "local variable lookup" do
+ it "finds local variable from block context in outer dsl scope" do
+ foo = "foo"
+ outer { expect(foo).to eq("foo") }
end
- it 'finds local variable from block definition in inner dsl scope' do
- bar = 'bar'
- outer { inner { expect(bar).to eq('bar') } }
+ it "finds local variable from block definition in inner dsl scope" do
+ bar = "bar"
+ outer { inner { expect(bar).to eq("bar") } }
end
end
- context 'instance variable lookup' do
- it 'finds instance variable from block definition in outer dsl scope' do
- @iv1 = 'iv1'; outer { expect(@iv1).to eq('iv1') }
+ describe "instance variable lookup" do
+ # rubocop:disable RSpec/InstanceVariable
+ it "finds instance variable from block definition in outer dsl scope" do
+ @iv1 = "iv1"
+ outer { expect(@iv1).to eq("iv1") }
end
- it "proxies instance variable assignments in block in outer dsl scope back into block's context" do
- @iv1 = 'foo'; outer { @iv1 = 'bar' }; expect(@iv1).to eq('bar')
+ it "proxies instance variable assignments in block in outer dsl scope "\
+ "back into block's context" do
+ @iv1 = "foo"
+ outer { @iv1 = "bar" }
+ expect(@iv1).to eq("bar")
end
- it 'finds instance variable from block definition in inner dsl scope' do
- @iv2 = 'iv2'; outer { inner { expect(@iv2).to eq('iv2') } }
+ it "finds instance variable from block definition in inner dsl scope" do
+ @iv2 = "iv2"
+ outer { inner { expect(@iv2).to eq("iv2") } }
end
- it "proxies instance variable assignments in block in inner dsl scope back into block's context" do
- @iv2 = 'foo'; outer { inner { @iv2 = 'bar' } }; expect(@iv2).to eq('bar')
+ it "proxies instance variable assignments in block in inner dsl scope "\
+ "back into block's context" do
+ @iv2 = "foo"
+ outer { inner { @iv2 = "bar" } }
+ expect(@iv2).to eq("bar")
end
+ # rubocop:enable RSpec/InstanceVariable
end
- end
+ describe "identity of 'self' inside nested dsl blocks" do
+ # see https://github.com/ms-ati/docile/issues/31
+ subject do
+ identified_selves = {}
+
+ outer do
+ identified_selves[:a] = self
+
+ inner do
+ identified_selves[:b] = self
+ end
+
+ identified_selves[:c] = self
+ end
- context 'when DSL context object is a Dispatch pattern' do
- class DispatchScope
- def params
- { :a => 1, :b => 2, :c => 3 }
+ identified_selves
end
- end
- class MessageDispatch
- include Singleton
+ it "identifies self inside outer dsl block" do
+ expect(subject[:a]).to be_instance_of(OuterDSL)
+ end
- def initialize
- @responders = {}
+ it "replaces self inside inner dsl block" do
+ expect(subject[:b]).to be_instance_of(InnerDSL)
end
- def add_responder path, &block
- @responders[path] = block
+ it "restores self to the outer dsl object after the inner dsl block" do
+ expect(subject[:c]).to be_instance_of(OuterDSL)
+ expect(subject[:c]).to be(subject[:a])
end
+ end
+ end
- def dispatch path, request
- Docile.dsl_eval(DispatchScope.new, request, &@responders[path])
+ context "when DSL context object is a Dispatch pattern" do
+ let(:class_message_dispatcher) do
+ Class.new do
+ def initialize
+ @responders = {}
+ end
+
+ def add_responder(path, &block)
+ @responders[path] = block
+ end
+
+ def dispatch(path, request)
+ Docile.
+ dsl_eval(class_dispatch_scope.new, request, &@responders[path])
+ end
+
+ def class_dispatch_scope
+ Class.new do
+ def params
+ { a: 1, b: 2, c: 3 }
+ end
+ end
+ end
end
end
+ let(:message_dispatcher_instance) do
+ class_message_dispatcher.new
+ end
+
def respond(path, &block)
- MessageDispatch.instance.add_responder(path, &block)
+ message_dispatcher_instance.add_responder(path, &block)
end
def send_request(path, request)
- MessageDispatch.instance.dispatch(path, request)
+ message_dispatcher_instance.dispatch(path, request)
end
- it 'dispatches correctly' do
+ # rubocop:disable RSpec/InstanceVariable
+ it "dispatches correctly" do
@first = @second = nil
- respond '/path' do |request|
+ respond "/path" do |request|
@first = request
end
- respond '/new_bike' do |bike|
+ respond "/new_bike" do |bike|
@second = "Got a new #{bike}"
end
- def x(y) ; "Got a #{y}"; end
- respond '/third' do |third|
- expect(x(third)).to eq('Got a third thing')
+ def third(val)
+ "Got a #{val}"
+ end
+
+ respond "/third" do |arg|
+ expect(third(arg)).to eq("Got a third thing")
end
fourth = nil
- respond '/params' do |arg|
+
+ respond "/params" do |arg|
fourth = params[arg]
end
- send_request '/path', 1
- send_request '/new_bike', 'ten speed'
- send_request '/third', 'third thing'
- send_request '/params', :b
+ send_request "/path", 1
+ send_request "/new_bike", "ten speed"
+ send_request "/third", "third thing"
+ send_request "/params", :b
expect(@first).to eq(1)
- expect(@second).to eq('Got a new ten speed')
+ expect(@second).to eq("Got a new ten speed")
expect(fourth).to eq(2)
end
+ # rubocop:enable RSpec/InstanceVariable
+ end
+ context "when DSL context object is same as the block's context object" do
+ let(:class_context_same_as_block_context) do
+ Class.new do
+ def foo(val = nil)
+ @foo = val if val
+ @foo
+ end
+
+ def bar(val = nil)
+ @bar = val if val
+ @bar
+ end
+
+ def dsl_eval(block)
+ Docile.dsl_eval(self, &block)
+ end
+
+ def dsl_eval_string(string)
+ block = binding.eval("proc { #{string} }") # rubocop:disable all
+ dsl_eval(block)
+ end
+ end
+ end
+
+ let(:dsl) { class_context_same_as_block_context.new }
+
+ it "calls DSL methods and sets state on the DSL context object" do
+ dsl.dsl_eval_string("foo 0; bar 1")
+ expect(dsl.foo).to eq(0)
+ expect(dsl.bar).to eq(1)
+ end
+
+ context "when the DSL object is frozen" do
+ it "can call non-mutative code without raising an exception" do
+ expect { dsl.freeze.dsl_eval_string("1 + 2") }.not_to raise_error
+ end
+ end
end
+ context "when NoMethodError is raised" do
+ specify "#backtrace doesn't include path to Docile's sources" do
+ described_class.dsl_eval(Object.new) { foo }
+ rescue NoMethodError => e
+ expect(e.backtrace).not_to include(match(%r{/lib/docile/}))
+ end
+
+ specify "#backtrace_locations doesn't include path to Docile's sources" do
+ described_class.dsl_eval(Object.new) { foo }
+ rescue NoMethodError => e
+ expect(e.backtrace_locations.map(&:absolute_path)).
+ not_to include(match(%r{/lib/docile/}))
+ end
+ end
+
+ context "when a DSL method has a keyword argument" do
+ let(:class_with_method_with_keyword_arg) do
+ Class.new do
+ attr_reader :v0, :v1, :v2
+
+ def set(arg, kw1:, kw2:)
+ @v0 = arg
+ @v1 = kw1
+ @v2 = kw2
+ end
+ end
+ end
+
+ let(:dsl) { class_with_method_with_keyword_arg.new }
+
+ it "calls such DSL methods with no stderr output" do
+ # This is to check warnings related to keyword argument is not output.
+ # See: https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/
+ expect do
+ described_class.dsl_eval(dsl) { set(0, kw2: 2, kw1: 1) }
+ end.
+ not_to output.to_stderr
+
+ expect(dsl.v0).to eq 0
+ expect(dsl.v1).to eq 1
+ expect(dsl.v2).to eq 2
+ end
+ end
+
+ context "when a DSL method has a double splat" do
+ let(:class_with_method_with_double_splat) do
+ Class.new do
+ attr_reader :arguments, :options
+
+ def configure(*arguments, **options)
+ @arguments = arguments.dup
+ @options = options.dup
+ end
+ end
+ end
+
+ let(:dsl) { class_with_method_with_double_splat.new }
+
+ it "correctly passes keyword arguments" do
+ described_class.dsl_eval(dsl) { configure(1, a: 1) }
+
+ expect(dsl.arguments).to eq [1]
+ expect(dsl.options).to eq({ a: 1 })
+ end
+
+ it "correctly passes hash arguments" do
+ described_class.dsl_eval(dsl) { configure(1, { a: 1 }) }
+
+ # TruffleRuby 22.0.0.2 has RUBY_VERSION of 3.0.2, but behaves as 2.x
+ if RUBY_VERSION >= "3.0.0" && RUBY_ENGINE != "truffleruby"
+ expect(dsl.arguments).to eq [1, { a: 1 }]
+ expect(dsl.options).to eq({})
+ else
+ expect(dsl.arguments).to eq [1]
+ expect(dsl.options).to eq({ a: 1 })
+ end
+ end
+ end
end
- describe '.dsl_eval_immutable' do
+ describe ".dsl_eval_with_block_return" do
+ let(:array) { [] }
+ let!(:result) { execute_dsl_against_array }
+
+ def execute_dsl_against_array
+ Docile.dsl_eval_with_block_return(array) do
+ push 1
+ push 2
+ pop
+ push 3
+ "Return me!"
+ end
+ end
+
+ it "executes the block against the DSL context object" do
+ expect(array).to eq([1, 3])
+ end
- context 'when DSL context object is a frozen String' do
- let(:original) { "I'm immutable!".freeze }
+ it "returns the block's return value" do
+ expect(result).to eq("Return me!")
+ end
+ end
+
+ describe ".dsl_eval_immutable" do
+ context "when DSL context object is a frozen String" do
+ let(:original) { "I'm immutable!".freeze } # rubocop:disable Style/RedundantFreeze
let!(:result) { execute_non_mutating_dsl_against_string }
def execute_non_mutating_dsl_against_string
@@ -280,12 +597,12 @@ describe Docile do
expect(original).to eq("I'm immutable!")
end
- it 'chains the commands in the block against the DSL context object' do
+ it "chains the commands in the block against the DSL context object" do
expect(result).to eq("!ELBATUMMI M'I")
end
end
- context 'when DSL context object is a number' do
+ context "when DSL context object is a number" do
let(:original) { 84.5 }
let!(:result) { execute_non_mutating_dsl_against_number }
@@ -296,25 +613,26 @@ describe Docile do
end
end
- it 'chains the commands in the block against the DSL context object' do
+ it "chains the commands in the block against the DSL context object" do
expect(result).to eq(42)
end
end
end
-
end
describe Docile::FallbackContextProxy do
-
- describe '#instance_variables' do
+ describe "#instance_variables" do
subject { create_fcp_and_set_one_instance_variable.instance_variables }
+
let(:expected_type_of_names) { type_of_ivar_names_on_this_ruby }
let(:actual_type_of_names) { subject.first.class }
- let(:excluded) { Docile::FallbackContextProxy::NON_PROXIED_INSTANCE_VARIABLES }
+ let(:excluded) do
+ Docile::FallbackContextProxy::NON_PROXIED_INSTANCE_VARIABLES
+ end
def create_fcp_and_set_one_instance_variable
fcp = Docile::FallbackContextProxy.new(nil, nil)
- fcp.instance_variable_set(:@foo, 'foo')
+ fcp.instance_variable_set(:@foo, "foo")
fcp
end
@@ -323,7 +641,7 @@ describe Docile::FallbackContextProxy do
instance_variables.first.class
end
- it 'returns proxied instance variables' do
+ it "returns proxied instance variables" do
expect(subject.map(&:to_sym)).to include(:@foo)
end
@@ -331,9 +649,8 @@ describe Docile::FallbackContextProxy do
expect(subject.map(&:to_sym)).not_to include(*excluded)
end
- it 'preserves the type (String or Symbol) of names on this ruby version' do
+ it "preserves the type (String or Symbol) of names on this ruby version" do
expect(actual_type_of_names).to eq(expected_type_of_names)
end
end
-
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 756491a..b235409 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -1,30 +1,30 @@
-require File.expand_path('on_what', File.dirname(File.dirname(__FILE__)))
+# frozen_string_literal: true
-begin
- require 'simplecov'
- require 'coveralls'
+# Code coverage (via SimpleCov)
+require "simplecov"
- # On Ruby 1.9+ use SimpleCov and publish to Coveralls.io
- if !on_1_8?
- SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
- SimpleCov::Formatter::HTMLFormatter,
- Coveralls::SimpleCov::Formatter
- ]
- SimpleCov.start do
- add_filter '/spec/' # exclude test code
- add_filter '/vendor/' # exclude gems which are vendored on Travis CI
- end
+SimpleCov.start do
+ add_filter "/spec/" # exclude test code
+ add_filter "/vendor/" # exclude gems which are cached in CI
+end
- # Remove Docile, which was required by SimpleCov, to require again later
- Object.send(:remove_const, :Docile)
- $LOADED_FEATURES.reject! { |f| f =~ /\/docile\// }
+# On CI we publish coverage to codecov.io
+# To use the codecov action, we need to generate XML based coverage report
+if ENV.fetch("CI", nil) == "true"
+ begin
+ require "simplecov-cobertura"
+ SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter
+ rescue LoadError
+ warn "simplecov-cobertura gem not found - not generating XML for codecov.io"
end
-rescue LoadError
- warn 'warning: simplecov/coveralls gems not found; skipping coverage'
end
-lib_dir = File.join(File.dirname(File.dirname(__FILE__)), 'lib')
-$LOAD_PATH.unshift lib_dir unless $LOAD_PATH.include? lib_dir
+# Due to circular dependency (SimpleCov depends on Docile), remove docile and
+# then require the docile gem again below.
+Object.send(:remove_const, :Docile)
+$LOADED_FEATURES.reject! { |f| f.include?("/lib/docile") }
-# Require Docile again, now with coverage enabled on 1.9+
-require 'docile'
+# Require Docile again, now with coverage enabled
+lib_dir = File.join(File.dirname(File.dirname(__FILE__)), "lib")
+$LOAD_PATH.unshift lib_dir unless $LOAD_PATH.include? lib_dir
+require "docile"