Skip to content

Instantly share code, notes, and snippets.

@dkubb
Created August 25, 2010 04:53
Show Gist options
  • Save dkubb/548879 to your computer and use it in GitHub Desktop.
Save dkubb/548879 to your computer and use it in GitHub Desktop.
YARD based DbC system (code spike)
#!/usr/bin/ruby -Ku
# encoding: utf-8
# The idea with this code spike is to parse some code that has YARD
# documentation, and then wrap the methods at runtime to ensure that
# they match the behavior specified in the docs.
#
# This code will test the input, output and exceptions for every
# documented method. It will also test the provided block's input
# and output too. Currently it just raises an exception but I
# suppose it could also warn.
#
# I plan to run this against a few more in-the-wild documented projects
# get some of the kinks worked out, and then code this TDD style and
# release it as a gem.
$LOAD_PATH.unshift File.expand_path('~/Programming/ruby/open-source/yard-types-parser/lib')
require 'yard'
require 'yard_types_parser'
require 'pathname'
require 'extlib'
class Item
# @param [#to_str] name
def initialize(name)
@name = name.to_str
end
# @yieldparam [Integer] name
#
# @yieldreturn [String]
#
# @return [String]
def frobnicate
"Frobnicated #{yield(@name)}!"
end
end
module YARD
class InvalidConstraintError < StandardError; end
class Wrapper
attr_reader :name, :input, :output, :exception
def initialize(name)
@name = name.to_s
@input = []
@output = Constraints.new('output', self)
@exception = Constraints.new('exception', self)
end
def proc
@proc ||= Proc.new("#{name} proc")
end
def to_proc
wrapper = self
proc_wrapper = self.proc
lambda do |*input, &block|
wrapper.assert_valid_input(input)
block = proc_wrapper.wrap(block) if block
output = begin
wrapper.call(self, input, block)
rescue InvalidConstraintError => exception
raise exception
rescue Exception => exception
wrapper.assert_valid_exception(exception)
raise exception
end
# skip #initialize
return output if wrapper.name[/#initialize\z/]
wrapper.assert_valid_output(output)
output
end
end
def assert_valid_input(input)
input.zip(self.input) do |arg, constraints|
constraints.assert_valid(arg) if constraints
end
end
def assert_valid_exception(exception)
self.exception.assert_valid(exception)
end
def assert_valid_output(output)
self.output.assert_valid(output)
end
def call
raise NotImplementedError, "#{self.class}#call is not implemented"
end
def wrap(object)
raise NotImplementedError, "#{self.class}#wrap is not implemented"
end
class Proc < Wrapper
def call(object, input, block)
@proc.call(*input, &block)
end
def wrap(proc)
@proc = proc
to_proc
end
end
class Method < Wrapper
def initialize(name)
super
namespace, separator, @method_name = name.split(/([#.])/, 3)
@mod = Object.full_const_get(namespace)
@mod = @mod.meta_class if separator == '.'
@unbound_method = @mod.instance_method(@method_name)
end
def call(object, input, block)
@unbound_method.bind(object).call(*input, &block)
end
def wrap(name = @method_name)
@mod.send(:define_method, name, &self)
end
end
end
class Constraints
include Enumerable
attr_reader :type, :wrapper
def initialize(type, wrapper, entries = Set[])
@type = type.to_str
@wrapper = wrapper
@entries = entries.to_set
end
def concat(constraints)
constraints.each { |constraint| self << constraint }
self
end
def <<(constraint)
@entries << constraint
self
end
def valid?(value)
empty? || any? { |constraint| constraint.valid?(value) }
end
def assert_valid(value)
unless valid?(value)
# TODO: improve this error message
raise InvalidConstraintError, "The #{type} value for #{wrapper.name} is invalid (#{value.inspect})"
end
end
def each
@entries.each { |constraint| yield constraint }
self
end
def empty?
@entries.empty?
end
end
class Constraint
MOD_PATTERN = /\A[A-Z]\w*(?:::[A-Z]\w*)*\z/.freeze
METHOD_PATTERN = /\A#(\w+)\z/.freeze
def self.parse(tag)
::Parser.new(tag.types.join(',')).parse.map do |type|
name = type.name
case type
when HashCollectionType then Hash.new(Extlib::Inflection.constantize(name))
when FixedCollectionType then FixedCollection.new(Extlib::Inflection.constantize(name))
when CollectionType then Collection.new(Extlib::Inflection.constantize(name))
when Type
case name
when 'undefined' then Undefined.new
when 'Boolean' then Boolean.new
when MOD_PATTERN then KindOf.new(Extlib::Inflection.constantize(name))
when METHOD_PATTERN then RespondTo.new($1)
else Constant.new(name)
end
end
end
end
def valid?(value)
raise "#{self.class}#valid? is not implemented"
end
class KindOf < Constraint
def initialize(klass)
@klass = klass
end
def valid?(value)
value.kind_of?(@klass)
end
end
class RespondTo < Constraint
def initialize(method)
@method = method.to_sym
end
def valid?(value)
value.respond_to?(@method)
end
end
class Undefined < Constraint
def valid?(value)
true
end
end
class Boolean < Constraint
def valid?(value)
value == true || value == false
end
end
class Constant < Constraint
def initialize(constant)
@constant = eval(constant)
end
def valid?(value)
value == @constant
end
end
class Hash < KindOf
def initialize(klass = Hash)
super(klass)
end
def valid?(value)
# TODO: validate keys and values
super
end
end
class Collection < Constraint
def initialize(klass = Array)
super(klass)
end
def valid?(value)
# TODO: validate the entries are at least one of the constraints
super
end
end
class FixedCollection < Collection
def valid?(value)
# TODO: validate the entries match the constraints exactly
super
end
end
end
end
YARD.parse($0)
# TODO: move this into a helper method
YARD::Registry.all(:method).each do |method_object|
# XXX: skip the YARD::Wrapper methods
next unless method_object.namespace.to_s == 'Item'
wrapper = YARD::Wrapper::Method.new(method_object.to_s)
method_object.tags(:param).each do |tag|
wrapper.input << YARD::Constraints.new(
"input #{tag.name}",
wrapper,
YARD::Constraint.parse(tag)
)
end
method_object.tags(:raise).each do |tag|
wrapper.exception.concat(YARD::Constraint.parse(tag))
end
method_object.tags(:return).each do |tag|
wrapper.output.concat(YARD::Constraint.parse(tag))
end
method_object.tags(:yieldparam).each do |tag|
wrapper.proc.input << YARD::Constraints.new(
"proc input #{tag.name}",
wrapper,
YARD::Constraint.parse(tag)
)
end
method_object.tags(:yieldreturn).each do |tag|
wrapper.proc.output.concat(YARD::Constraint.parse(tag))
end
wrapper.wrap(method_object.name)
end
puts Item.new('Widget').frobnicate { |name| name }
@dkubb
Copy link
Author

dkubb commented Aug 1, 2011

@JackDanger this is that thing with YARD I was talking about on the bus yesterday.

It's pretty raw, but the basic idea is that it takes a reference to a method, and then replaces the method with something that wraps it and asserts the input/output/exceptions match the specifications in the YARD docs.

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