docile-1.4.1/0000755000004100000410000000000014705053115013005 5ustar www-datawww-datadocile-1.4.1/.gitignore0000644000004100000410000000013414705053115014773 0ustar www-datawww-data*.gem .bundle Gemfile.lock pkg .idea doc .yardoc coverage vendor .ruby-gemset .ruby-version docile-1.4.1/SECURITY.md0000644000004100000410000000106714705053115014602 0ustar www-datawww-data# 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. docile-1.4.1/docile.gemspec0000644000004100000410000000256514705053115015621 0ustar www-datawww-data# frozen_string_literal: true require_relative "lib/docile/version" Gem::Specification.new do |s| 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 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 (2.5 to support JRuby 9.2.17.0) s.required_ruby_version = ">= 2.5.0" # 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"] 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 docile-1.4.1/.github/0000755000004100000410000000000014705053115014345 5ustar www-datawww-datadocile-1.4.1/.github/dependabot.yml0000644000004100000410000000045414705053115017200 0ustar www-datawww-dataversion: 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" docile-1.4.1/.github/workflows/0000755000004100000410000000000014705053115016402 5ustar www-datawww-datadocile-1.4.1/.github/workflows/main.yml0000644000004100000410000000206314705053115020052 0ustar www-datawww-dataname: 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, 2.7, '3.0', 3.1, 3.2, 3.3, head] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3.3.0 # 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.3 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - run: bundle exec rspec - uses: codecov/codecov-action@v3.1.1 with: name: ${{ matrix.ruby }} file: ./coverage/coverage.xml - run: bundle exec rubocop if: matrix.ruby == 3.3 docile-1.4.1/lib/0000755000004100000410000000000014705053115013553 5ustar www-datawww-datadocile-1.4.1/lib/docile.rb0000644000004100000410000001062614705053115015344 0ustar www-datawww-data# 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 extend Execution # Execute a block in the context of an object whose methods represent the # commands in a DSL. # # @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("Hello, world!") do # reverse! # upcase! # end # #=> "!DLROW ,OLLEH" # # @example Use an Array as a DSL # Docile.dsl_eval([]) do # push 1 # push 2 # pop # push 3 # end # #=> [1, 3] # # @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 `dsl` context object after executing the block def dsl_eval(dsl, *args, &block) 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. # # @note Use with a *functional* DSL (commands return successor # context objects) # # Use this method to execute a *functional* DSL, which means that: # # 1. The original DSL context object is never mutated # 2. Each command returns the next DSL context object # 3. The final return value is the value returned by the last command # # @example Use a frozen String as a DSL # Docile.dsl_eval_immutable("I'm immutable!".freeze) do # reverse # upcase # end # #=> "!ELBATUMMI M'I" # # @example Use a Float as a DSL # Docile.dsl_eval_immutable(84.5) do # fdiv(2) # floor # end # #=> 42 # # @param dsl [Object] immutable context object whose methods make up the # initial 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 and successor return values # @return [Object] the return value of the final command in the block 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 docile-1.4.1/lib/docile/0000755000004100000410000000000014705053115015012 5ustar www-datawww-datadocile-1.4.1/lib/docile/fallback_context_proxy.rb0000644000004100000410000001016314705053115022104 0ustar www-datawww-data# frozen_string_literal: true require "set" module Docile # @api private # # A proxy object with a primary receiver as well as a secondary # fallback receiver. # # Will attempt to forward all method calls first to the primary receiver, # and then to the fallback receiver if the primary does not handle that # method. # # 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_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. NON_PROXIED_INSTANCE_VARIABLES = Set[:@__receiver__, :@__fallback__] # Undefine all instance methods except those in {NON_PROXIED_METHODS} instance_methods.each do |method| undef_method(method) unless NON_PROXIED_METHODS.include?(method.to_sym) end # @param [Object] receiver the primary proxy target to which all methods # initially will be forwarded # @param [Object] fallback the fallback proxy target to which any methods # not handled by `receiver` will be forwarded 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] Instance variable names, excluding # {NON_PROXIED_INSTANCE_VARIABLES} def instance_variables super.reject { |v| NON_PROXIED_INSTANCE_VARIABLES.include?(v) } end # Proxy all methods, excluding {NON_PROXIED_METHODS}, first to `receiver` # and then to `fallback` if not found. def method_missing(method, *args, &block) if @__receiver__.respond_to?(method.to_sym) @__receiver__.__send__(method.to_sym, *args, &block) else 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 docile-1.4.1/lib/docile/backtrace_filter.rb0000644000004100000410000000123114705053115020620 0ustar www-datawww-data# 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 docile-1.4.1/lib/docile/chaining_fallback_context_proxy.rb0000644000004100000410000000147514705053115023752 0ustar www-datawww-data# frozen_string_literal: true require "docile/fallback_context_proxy" module Docile # @api private # # Operates in the same manner as {FallbackContextProxy}, but replacing # the primary `receiver` object with the result of each proxied method. # # This is useful for implementing DSL evaluation for immutable context # 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 end ruby2_keywords :method_missing if respond_to?(:ruby2_keywords, true) end # rubocop:enable Style/MissingRespondToMissing end docile-1.4.1/lib/docile/execution.rb0000644000004100000410000000422014705053115017340 0ustar www-datawww-data# frozen_string_literal: true module Docile # @api private # # A namespace for functions relating to the execution of a block against a # proxy object. module Execution # Execute a block in the context of an object whose methods represent the # commands in a DSL, using a specific proxy class. # # @param dsl [Object] context object whose methods make up the # (initial) DSL # @param proxy_type [FallbackContextProxy, ChainingFallbackContextProxy] # which class to instantiate as proxy context # @param args [Array] arguments to be passed to the block # @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) # 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 docile-1.4.1/lib/docile/version.rb0000644000004100000410000000015514705053115017025 0ustar www-datawww-data# frozen_string_literal: true module Docile # The current version of this library VERSION = "1.4.1" end docile-1.4.1/.yardopts0000644000004100000410000000015514705053115014654 0ustar www-datawww-data--title 'Docile Documentation' --no-private --main=README.md --markup-provider=redcarpet --markup=markdown docile-1.4.1/.rspec0000644000004100000410000000003614705053115014121 0ustar www-datawww-data--color --format documentationdocile-1.4.1/Rakefile0000644000004100000410000000051514705053115014453 0ustar www-datawww-data# 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] # Use default rspec rake task RSpec::Core::RakeTask.new # Configure `rake clobber` to delete all generated files CLOBBER.include("pkg", "doc", "coverage") docile-1.4.1/HISTORY.md0000644000004100000410000001357014705053115014476 0ustar www-datawww-data# HISTORY ## [Unreleased changes](http://github.com/ms-ati/docile/compare/v1.4.1...main) ## [v1.4.1 (May 12, 2021)](http://github.com/ms-ati/docile/compare/v1.4.0...v1.4.1) - Special thanks to Mamoru TASAKA (@mtasaka): - Starting point for a fix on the tests to pass on Ruby 3.3 - Added support for Ruby 3.2 and 3.3 - Removed support for Rubies below 2.7 ## [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 - clarify gemspec settings as much as possible - bump rspec dependency to 3.0.x ## [v1.1.4 (Jun 11, 2014)](http://github.com/ms-ati/docile/compare/v1.1.3...v1.1.4) - Special thanks to Ken Dreyer (@ktdreyer): - make simplecov/coveralls optional for running tests \[[33834852c7](https://github.com/ms-ati/docile/commit/33834852c7849912b97e109e8c5c193579cc5e98)\] - update URL in gemspec \[[174e654a07](https://github.com/ms-ati/docile/commit/174e654a075c8350b3411b212cfb409bc605348a)\] ## [v1.1.3 (Feb 4, 2014)](http://github.com/ms-ati/docile/compare/v1.1.2...v1.1.3) - Special thanks to Alexey Vasiliev (@le0pard): - fix problem to catch NoMethodError from non receiver object - upgrade rspec format to new "expect" syntax ## [v1.1.2 (Jan 10, 2014)](http://github.com/ms-ati/docile/compare/v1.1.1...v1.1.2) - remove unnecessarily nested proxy objects (thanks @Ajedi32)! - documentation updates and corrections ## [v1.1.1 (Nov 26, 2013)](http://github.com/ms-ati/docile/compare/v1.1.0...v1.1.1) - documentation updates and corrections - fix Rubinius build in Travis CI ## [v1.1.0 (Jul 29, 2013)](http://github.com/ms-ati/docile/compare/v1.0.5...v1.1.0) - add functional-style DSL objects via `Docile#dsl_eval_immutable` ## [v1.0.5 (Jul 28, 2013)](http://github.com/ms-ati/docile/compare/v1.0.4...v1.0.5) - achieve 100% yard docs coverage - fix rendering of docs at http://rubydoc.info/gems/docile ## [v1.0.4 (Jul 25, 2013)](http://github.com/ms-ati/docile/compare/v1.0.3...v1.0.4) - simplify and clarify code - fix a minor bug where FallbackContextProxy#instance_variables would return symbols rather than strings on Ruby 1.8.x ## [v1.0.3 (Jul 6, 2013)](http://github.com/ms-ati/docile/compare/v1.0.2...v1.0.3) - instrument code coverage via SimpleCov and publish to Coveralls.io ## [v1.0.2 (Apr 1, 2013)](http://github.com/ms-ati/docile/compare/v1.0.1...v1.0.2) - allow passing parameters to DSL blocks (thanks @dslh!) ## [v1.0.1 (Nov 29, 2012)](http://github.com/ms-ati/docile/compare/v1.0.0...v1.0.1) - relaxed rspec and rake dependencies to allow newer versions - fixes to documentation ## [v1.0.0 (Oct 29, 2012)](http://github.com/ms-ati/docile/compare/1b225c8a27...v1.0.0) - Initial Feature Set docile-1.4.1/Gemfile0000644000004100000410000000115314705053115014300 0ustar www-datawww-data# frozen_string_literal: true 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 docile-1.4.1/LICENSE0000644000004100000410000000207314705053115014014 0ustar www-datawww-dataThe MIT License (MIT) Copyright (c) 2012-2024 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 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. docile-1.4.1/README.md0000644000004100000410000002571414705053115014275 0ustar www-datawww-data# Docile [![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 somewhat hairy meta-programming to get the interface just right. "Docile" means *Ready to accept control or instruction; submissive* [[1]] Instead of each Ruby project reinventing this wheel, let's make our Ruby DSL coding a bit more docile... [1]: http://www.google.com/search?q=docile+definition "Google" ## Usage ### 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? ```ruby with_array([]) do push 1 push 2 pop push 3 end #=> [1, 3] ``` No problem, just define the method `with_array` like this: ```ruby def with_array(arr=[], &block) Docile.dsl_eval(arr, &block) end ``` Easy! ### 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=>#, :instance_exec=># } ``` 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]. For example, let's say you want a DSL to specify how you want to build a Pizza: ```ruby @sauce_level = :extra pizza do cheese pepperoni sauce @sauce_level end #=> # ``` And let's say we have a PizzaBuilder, which builds a Pizza like this: ```ruby Pizza = Struct.new(:cheese, :pepperoni, :bacon, :sauce) class PizzaBuilder def cheese(v=true); @cheese = v; self; end def pepperoni(v=true); @pepperoni = v; self; end def bacon(v=true); @bacon = v; self; end def sauce(v=nil); @sauce = v; self; end def build Pizza.new(!!@cheese, !!@pepperoni, !!@bacon, @sauce) end end PizzaBuilder.new.cheese.pepperoni.sauce(:extra).build #=> # ``` Then implement your DSL like this: ```ruby def pizza(&block) Docile.dsl_eval(PizzaBuilder.new, &block).build end ``` 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' } } } #=> #, # father=#, # 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. Supposing you want to make some sort of cheap [Sinatra][3] knockoff: ```ruby @last_request = nil respond '/path' do |request| puts "Request received: #{request}" @last_request = request end def ride bike # Play with your new bike end respond '/new_bike' do |bike| ride(bike) end ``` You'd put together a dispatcher something like this: ```ruby require 'singleton' class DispatchScope def a_method_you_can_call_from_inside_the_block :useful_huh? end end class MessageDispatch include Singleton def initialize @responders = {} end def add_responder path, &block @responders[path] = block end def dispatch path, request Docile.dsl_eval(DispatchScope.new, request, &@responders[path]) end end def respond path, &handler MessageDispatch.instance.add_responder path, handler end def send_request path, request MessageDispatch.instance.dispatch path, request end ``` [3]: http://www.sinatrarb.com "Sinatra" ### 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-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-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-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? ```ruby s = "I'm immutable!".freeze with_immutable_string(s) do reverse upcase end #=> "!ELBATUMMI M'I" s #=> "I'm immutable!" ``` No problem, just define the method `with_immutable_string` like this: ```ruby def with_immutable_string(str="", &block) Docile.dsl_eval_immutable(str, &block) 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 2. Local variable lookup falls back from the DSL object to the block's context 3. Instance variables are from the block's context only 4. Nested DSL evaluation, correctly chaining method and variable handling from the inner to the outer DSL scopes 5. Alternatives for both imperative and functional styles of DSL objects ## Installation ``` bash $ gem install docile ``` ## Links * [Source](https://github.com/ms-ati/docile) * [Documentation](http://rubydoc.info/gems/docile) * [Bug Tracker](https://github.com/ms-ati/docile/issues) ## Status 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). ## Release Policy Docile releases follow [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html). ## Note on Patches/Pull Requests * Fork the project. * Setup your development environment with: `gem install bundler; bundle install` * Make your feature addition or bug fix. * Add tests for it. This is important so I don't break it in a future version unintentionally. * Commit, do not mess with rakefile, version, or history. (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) * Send me a pull request. Bonus points for topic branches. ## Releasing To make a new release of `Docile` to [RubyGems](https://rubygems.org/gems/docile), first install the release dependencies (e.g. `rake`) as follows: ```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-2024 Marc Siegel. Licensed under the [MIT License](http://choosealicense.com/licenses/mit/), see [LICENSE](LICENSE) for details. docile-1.4.1/.rubocop.yml0000644000004100000410000000011014705053115015247 0ustar www-datawww-datainherit_gem: panolint: panolint-rubocop.yml require: - rubocop-rake