Created
October 11, 2012 13:16
-
-
Save stevecj/3872200 to your computer and use it in GitHub Desktop.
Interfaces for Ruby
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# A module for inclusion in a class of objects that delegate to and | |
# provide restricted interface definitions for underlying "occurrence" | |
# objects. | |
# | |
# This is useful for purposes such as to enforce the same API for a | |
# unit under test as for mocks and stubs of the same unit used for | |
# testing other units. | |
# | |
# This is a proof of concept demonstration and is not well-tested, | |
# production-ready code. | |
module DefinesInterface | |
def self.included(other) | |
other.extend self::ClassBehavior | |
end | |
# Creates a new instance of an interface class that decorates the | |
# given occurrence object. | |
def initialize(occurrence) | |
@__occurrence__ = occurrence | |
end | |
# The underlying object that the interface class delegates to. | |
def __occurrence__ ; @__occurrence__ ; end | |
module ClassBehavior | |
# Defines a delegator to a method of an occurrence. Each argument | |
# specification may be a string or symbol such as 'arg1', :arg2, | |
# 'arg3?', :arg4?, '*args', or '&block'. A "?" suffix designates | |
# an optional argument and will be omitted from the actual argument | |
# name. | |
# Delegation of block arguments is supported only when a block | |
# argumment specification has been given. | |
def api_def(message, *arg_specs) | |
arg_specs.map!{|a| "#{a}" } | |
initial_reqd_args = arg_specs.take_while{|a| a !~ /^\*|\?$/ } | |
arg_specs = arg_specs[initial_reqd_args.length..-1] | |
optional_args = arg_specs.take_while{|a| a =~ /\?$/ }.map{|a| a[0..-2] } | |
arg_specs = arg_specs[optional_args.length..-1] | |
splat_arg = arg_specs.first =~ /^\*/ ? arg_specs.unshift : nil | |
final_args = arg_specs | |
block_arg = arg_specs.last =~ /^&/ ? arg_specs.pop : nil | |
def_args_expr = ( | |
initial_reqd_args + | |
optional_args.map{|a| "#{a} = #{OmittedArgExpr}" } + | |
[splat_arg].compact + | |
final_args + | |
[block_arg].compact | |
) * ', ' | |
has_value_args = !def_args_expr.empty? | |
build_call_args_expr = [ | |
initial_reqd_args.empty? ? nil : "[#{initial_reqd_args * ', '}]" , | |
optional_args.empty? ? nil : "[#{optional_args * ', '}].take_while{|a| #{OmittedArgExpr} != a }" , | |
splat_arg ? "#{splat_arg}" : nil , | |
final_args.empty? ? nil : "[#{final_args * ', '}]" | |
].compact * ' + ' | |
module_eval <<-CODE, __FILE__, __LINE__ + 1 | |
def #{message}(#{def_args_expr}) | |
#{has_value_args ? "args = #{build_call_args_expr}" : ''} | |
__occurrence__.#{message}(#{has_value_args ? '*args' : ''}#{block_arg ? ", #{block_arg}" : ''}) | |
end | |
CODE | |
end | |
# The magic default value for interface class optional arguments. | |
OmittedArg = Object.new | |
# A Ruby expression that evaluates to OmittedArg. | |
OmittedArgExpr = "#{self}::OmittedArg" | |
end | |
end | |
# ==== A simple usage example ==== | |
class StickyNotePlacerApi | |
include DefinesInterface | |
api_def :call, :user, :message, :color? | |
end | |
class ApplicationApi | |
include DefinesInterface | |
api_def :sticky_note_placer | |
end | |
module Application | |
def self.new | |
ApplicationApi.new(Base.new) | |
end | |
class Base | |
def sticky_note_placer | |
StickyNotePlacerApi.new(StickyNotePlacer.new) | |
end | |
end | |
class StickyNotePlacer | |
def call(user, message, color = :yellow) | |
puts "#{self} called with #{[user, message, color]}" | |
end | |
end | |
end | |
app = Application.new | |
placer = app.sticky_note_placer | |
placer.call 'Pat', "Thanks Riley. Looks good.", :green | |
placer.call 'Jo', "Remember the cookies." | |
# These invocations would fail at the interface level and would not | |
# reach the underlying occurrences. | |
# | |
# placer = app.sticky_note_poster | |
# placer = app.sticky_note_placer(:something) | |
# placer.call 'Jo' | |
# placer.call 'Pat', "Thanks Riley. Looks good.", :green, :bright |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
That's... kind of a wall o' code you've got there in the .api_def method, sir.