Created
August 25, 2010 04:53
-
-
Save dkubb/548879 to your computer and use it in GitHub Desktop.
YARD based DbC system (code spike)
This file contains 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
#!/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 } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@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.