Skip to content
Browse files

Initial commit

  • Loading branch information...
0 parents commit 43d3a5a78d05279a2c433936bcea0851d2530847 @lsegal committed Jun 9, 2009
Showing with 347 additions and 0 deletions.
  1. +21 −0 README.md
  2. +133 −0 lib/yard_types_parser.rb
  3. +193 −0 spec/yard_types_parser_spec.rb
21 README.md
@@ -0,0 +1,21 @@
+YARD Types Parser
+=================
+
+Parses YARD type declarations and translates them into plain English. A quick
+example of a YARD type declaration is in the following parameter declaration:
+
+ # @param [Array<String, Symbol>] arg takes an Array of Strings or Symbols
+ def foo(arg)
+ end
+
+The parser will convert `Array<String, Symbol>` into the more readable:
+
+ an Array of (Strings or Symbols)
+
+You can quickly parse with:
+
+ Parser.new("String, Symbol, false").parse.list_join
+ #=> "a String, a Symbol or false"
+
+You can try this yourself live at [http://yard.soen.ca/types](http://yard.soen.ca/types)
+with plenty more examples.
133 lib/yard_types_parser.rb
@@ -0,0 +1,133 @@
+require 'strscan'
+
+class Array
+ def list_join
+ index = 0
+ inject("") do |acc, el|
+ acc << el.to_s
+ acc << ", " if index < size - 2
+ acc << " or " if index == size - 2
+ index += 1
+ acc
+ end
+ end
+end
+
+class Type
+ attr_accessor :name
+
+ def initialize(name)
+ @name = name
+ end
+
+ def to_s(singular = true)
+ if name[0] == "#"
+ singular ? "an object that responds to #{name}" : "objects that respond to #{name}"
+ elsif name[0] =~ /[A-Z]/
+ singular ? "a#{name[0] =~ /[aeiou]/i ? 'n' : ''} " + name : "#{name}#{name[-1] =~ /[A-Z]/ ? "'" : ''}s"
+ else
+ name
+ end
+ end
+end
+
+class CollectionType < Type
+ attr_accessor :types
+
+ def initialize(name, types)
+ @name = name
+ @types = types
+ end
+
+ def to_s(singular = true)
+ "a#{name[0] =~ /[aeiou]/i ? 'n' : ''} #{name} of (" + types.map {|t| t.to_s(false) }.list_join + ")"
+ end
+end
+
+class FixedCollectionType < CollectionType
+ def to_s(singular = true)
+ "a#{name[0] =~ /[aeiou]/i ? 'n' : ''} #{name} containing (" + types.map(&:to_s).join(" followed by ") + ")"
+ end
+end
+
+class HashCollectionType < Type
+ attr_accessor :key_types, :value_types
+
+ def initialize(name, key_types, value_types)
+ @name = name
+ @key_types = key_types
+ @value_types = value_types
+ end
+
+ def to_s(singular = true)
+ "a#{name[0] =~ /[aeiou]/i ? 'n' : ''} #{name} with keys made of (" + key_types.map {|t| t.to_s(false) }.list_join +
+ ") and values of (" + value_types.map {|t| t.to_s(false) }.list_join + ")"
+ end
+end
+
+class Parser
+ TOKENS = {
+ collection_start: /</,
+ collection_end: />/,
+ fixed_collection_start: /\(/,
+ fixed_collection_end: /\)/,
+ type_name: /#\w+|((::)?\w+)+/,
+ type_next: /[,;]/,
+ whitespace: /\s+/,
+ hash_collection_start: /\{/,
+ hash_collection_next: /=>/,
+ hash_collection_end: /\}/,
+ parse_end: nil
+ }
+
+ def self.parse(string)
+ new.parse(string)
+ end
+
+ def initialize(string)
+ @scanner = StringScanner.new(string)
+ end
+
+ def parse
+ types = []
+ type = nil
+ fixed = false
+ name = nil
+ loop do
+ found = false
+ TOKENS.each do |token_type, match|
+ if (match.nil? && @scanner.eos?) || (match && token = @scanner.scan(match))
+ found = true
+ case token_type
+ when :type_name
+ raise SyntaxError, "expecting END, got name '#{token}'" if name
+ name = token
+ when :type_next
+ raise SyntaxError, "expecting name, got '#{token}' at #{@scanner.pos}" if name.nil?
+ unless type
+ type = Type.new(name)
+ end
+ types << type
+ type = nil
+ name = nil
+ when :fixed_collection_start, :collection_start
+ name ||= "Array"
+ klass = token_type == :collection_start ? CollectionType : FixedCollectionType
+ type = klass.new(name, parse)
+ when :hash_collection_start
+ name ||= "Hash"
+ type = HashCollectionType.new(name, parse, parse)
+ when :hash_collection_next, :hash_collection_end, :fixed_collection_end, :collection_end, :parse_end
+ raise SyntaxError, "expecting name, got '#{token}'" if name.nil?
+ unless type
+ type = Type.new(name)
+ end
+ types << type
+ return types
+ end
+ end
+ end
+ raise SyntaxError, "invalid character at #{@scanner.peek(1)}" unless found
+ end
+ end
+end
193 spec/yard_types_parser_spec.rb
@@ -0,0 +1,193 @@
+require File.dirname(__FILE__) + '/../lib/yard_types_parser'
+
+def parse(types)
+ Parser.new(types).parse
+end
+
+def parse_fail(types)
+ lambda { parse(types) }.should raise_error(SyntaxError)
+end
+
+describe Type, '#to_s' do
+ before { @t = Type.new(nil) }
+
+ it "works for a class/module reference" do
+ @t.name = "ClassModuleName"
+ @t.to_s.should == "a ClassModuleName"
+ @t.to_s(false).should == "ClassModuleNames"
+
+ @t.name = "XYZ"
+ @t.to_s.should == "a XYZ"
+ @t.to_s(false).should == "XYZ's"
+
+ @t.name = "Array"
+ @t.to_s.should == "an Array"
+ @t.to_s(false).should == "Arrays"
+ end
+
+ it "works for a method (ducktype)" do
+ @t.name = "#mymethod"
+ @t.to_s.should == "an object that responds to #mymethod"
+ @t.to_s(false).should == "objects that respond to #mymethod"
+ end
+
+ it "works for a constant value" do
+ ['false', 'true', 'nil', '4'].each do |name|
+ @t.name = name
+ @t.to_s.should == name
+ @t.to_s(false).should == name
+ end
+ end
+end
+
+describe CollectionType, '#to_s' do
+ before { @t = CollectionType.new("Array", nil) }
+
+ it "can contain one item" do
+ @t.types = [Type.new("Object")]
+ @t.to_s.should == "an Array of (Objects)"
+ end
+
+ it "can contain more than one item" do
+ @t.types = [Type.new("Object"), Type.new("String"), Type.new("Symbol")]
+ @t.to_s.should == "an Array of (Objects, Strings or Symbols)"
+ end
+
+ it "can contain nested collections" do
+ @t.types = [CollectionType.new("List", [Type.new("Object")])]
+ @t.to_s.should == "an Array of (a List of (Objects))"
+ end
+end
+
+describe FixedCollectionType, '#to_s' do
+ before { @t = FixedCollectionType.new("Array", nil) }
+
+ it "can contain one item" do
+ @t.types = [Type.new("Object")]
+ @t.to_s.should == "an Array containing (an Object)"
+ end
+
+ it "can contain more than one item" do
+ @t.types = [Type.new("Object"), Type.new("String"), Type.new("Symbol")]
+ @t.to_s.should == "an Array containing (an Object followed by a String followed by a Symbol)"
+ end
+
+ it "can contain nested collections" do
+ @t.types = [FixedCollectionType.new("List", [Type.new("Object")])]
+ @t.to_s.should == "an Array containing (a List containing (an Object))"
+ end
+end
+
+describe FixedCollectionType, '#to_s' do
+ before { @t = HashCollectionType.new("Hash", nil, nil) }
+
+ it "can contain a single key type and value type" do
+ @t.key_types = [Type.new("Object")]
+ @t.value_types = [Type.new("Object")]
+ @t.to_s.should == "a Hash with keys made of (Objects) and values of (Objects)"
+ end
+
+ it "can contain multiple key types" do
+ @t.key_types = [Type.new("Key"), Type.new("String")]
+ @t.value_types = [Type.new("Object")]
+ @t.to_s.should == "a Hash with keys made of (Keys or Strings) and values of (Objects)"
+ end
+
+ it "can contain multiple value types" do
+ @t.key_types = [Type.new("String")]
+ @t.value_types = [Type.new("true"), Type.new("false")]
+ @t.to_s.should == "a Hash with keys made of (Strings) and values of (true or false)"
+ end
+end
+
+
+describe Parser, '#parse' do
+ it "should parse a regular class name" do
+ type = parse("MyClass")
+ type.size.should == 1
+ type.first.should be_a(Type)
+ type.first.name.should == "MyClass"
+ end
+
+ it "should parse a path reference name" do
+ type = parse("A::B")
+ type.size.should == 1
+ type.first.should be_a(Type)
+ type.first.name.should == "A::B"
+ end
+
+ it "should parse a list of simple names" do
+ type = parse("A, B::C, D, E")
+ type.size.should == 4
+ type[0].name.should == "A"
+ type[1].name.should == "B::C"
+ type[2].name.should == "D"
+ type[3].name.should == "E"
+ end
+
+ it "should parse a collection type" do
+ type = parse("MyList<String>")
+ type.first.should be_a(CollectionType)
+ type.first.types.size.should == 1
+ type.first.name.should == "MyList"
+ type.first.types.first.name.should == "String"
+ end
+
+ it "should allow a collection type without a name" do
+ type = parse("<String>")
+ type.first.name.should == "Array"
+ end
+
+ it "should allow a fixed collection type without a name" do
+ type = parse("(String)")
+ type.first.name.should == "Array"
+ end
+
+ it "should allow a hash collection type without a name" do
+ type = parse("{K=>V}")
+ type.first.name.should == "Hash"
+ end
+
+ it "should not accept two commas in a row" do
+ parse_fail "A,,B"
+ end
+
+ it "should not accept two types not separated by a comma" do
+ parse_fail "A B"
+ end
+
+ it "should not allow a comma without a following type" do
+ parse_fail "A, "
+ end
+
+ it "should fail on any unrecognized character" do
+ parse_fail "$"
+ end
+end
+
+describe Parser, " // Integration" do
+ it "should parse an arbitrarily nested collection type" do
+ type = parse("Array<String, Array<Symbol, List(String, {K=>V})>>")
+ result = "an Array of (Strings or an Array of (Symbols or a List containing
+ (a String followed by a Hash with keys made of (K's) and values of (V's))))"
+ type.join.should == result.gsub(/\n/, '').squeeze(' ')
+ end
+
+ it "should parse various examples" do
+ expect = {
+ "Fixnum, Foo, Object, true" => "a Fixnum; a Foo; an Object; true",
+ "#read" => "an object that responds to #read",
+ "Array<String, Symbol, #read>" => "an Array of (Strings, Symbols or objects that respond to #read)",
+ "Set<Number>" => "a Set of (Numbers)",
+ "Array(String, Symbol)" => "an Array containing (a String followed by a Symbol)",
+ "Hash{String => Symbol, Number}" => "a Hash with keys made of (Strings) and values of (Symbols or Numbers)",
+ "Array<Foo, Bar>, List(String, Symbol, #to_s), {Foo, Bar => Symbol, Number}" => "an Array of (Foos or Bars);
+ a List containing (a String followed by a Symbol followed by an object that responds to #to_s);
+ a Hash with keys made of (Foos or Bars) and values of (Symbols or Numbers)"
+ }
+ expect.each do |input, expected|
+ types = parse(input)
+ types.join("; ").should == expected.gsub(/\n/, '').squeeze(' ')
+ end
+ end
+end

0 comments on commit 43d3a5a

Please sign in to comment.
Something went wrong with that request. Please try again.