Skip to content

Instantly share code, notes, and snippets.

@stevecj
Created October 11, 2012 13:16
Show Gist options
  • Save stevecj/3872200 to your computer and use it in GitHub Desktop.
Save stevecj/3872200 to your computer and use it in GitHub Desktop.
Interfaces for Ruby
# 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
@geeksam
Copy link

geeksam commented Oct 12, 2012

That's... kind of a wall o' code you've got there in the .api_def method, sir.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment