yard_types 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 722cc485964dc469732d1ef265f650bae6c029ee
4
+ data.tar.gz: 4e85d42f94b0d31a7991e076f62acdcc717c1c27
5
+ SHA512:
6
+ metadata.gz: 154fb6a437ecd17d7c331836224e6706e4edb411f4de4dd3723fc98131f20b9487b5bfb4c6d33ad0bbd53c9f23577fdbc3bd44af58cea4c35c491b96737dc387
7
+ data.tar.gz: 11800a82a3a26ddc32c4472f086fa4ba20f53d9b98f8ebb03e2098d495b2cbef1c504142d4976c48d6f92b4c8731ba6095178ccb85cacbe8a899ff78acb0f173
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - 2.1
6
+ - ruby-head
7
+ - jruby
8
+ - rbx-2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in yard_type_check.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Kyle Hargraves
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # yard_types
2
+
3
+ [![Build Status](https://travis-ci.org/pd/yard_types.svg?branch=master)](https://travis-ci.org/pd/yard_types)
4
+
5
+ Parse YARD type description strings -- eg `Array<#to_sym>` -- and use the
6
+ resulting types to check type correctness of objects at runtime.
7
+
8
+ ## Installation
9
+ Like everything else these days:
10
+
11
+ ~~~ruby
12
+ gem 'yard_types'
13
+ ~~~
14
+
15
+ Note that the `yard` gem may automatically require anything named `yard_*` or
16
+ `yard-*` on your load path, and attempt to use it as a plugin. You could see
17
+ errors along the lines of `failed to load plugin yard_types`; this is harmless,
18
+ as best I can tell.
19
+
20
+ ## Usage
21
+ Parse a type description string, and test an object against it:
22
+
23
+ ~~~ruby
24
+ type = YardTypes.parse('#quack') #=> #<YardTypes::TypeConstraint ...>
25
+
26
+ type.check(Object.new)
27
+ #=> false
28
+
29
+ obj = Object.new
30
+ def obj.quack; 'quack!'; end
31
+ type.check(obj)
32
+ #=> true
33
+ ~~~
34
+
35
+ ## Caveats
36
+ YARD does not officially specify a syntax for its type descriptions; the syntax
37
+ used by its own documentation varies between files. The syntax supported in
38
+ this gem aims to follow the rules given by the [YARD Type Parser][type-parser].
39
+
40
+ In the wild, people seem to use a wide variety of different syntaxes, many of
41
+ which are unlikely to be supported right now. If you find any such examples,
42
+ feel free to file an issue -- or better yet, write a test, implement the feature,
43
+ and send me a pull request.
44
+
45
+ ## Tests
46
+ Pretty standard. Just run `rake` or `rspec`.
47
+
48
+ ## Contributing
49
+
50
+ 1. Fork it ( http://github.com/pd/yard_types/fork )
51
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
52
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
53
+ 4. Push to the branch (`git push origin my-new-feature`)
54
+ 5. Create new Pull Request
55
+
56
+ ## Credits
57
+ The bulk of the parser was [written by lsegal](lsegal-parser); unfortunately, it
58
+ was never released as a gem, and has sat untouched for 5 years. I've only modified
59
+ the parser to better support `Hash<A, B>` syntax and to use more consistent
60
+ naming patterns.
61
+
62
+ [type-parser]: http://yardoc.org/types
63
+ [lsegel-parser]: https://github.com/lsegal/yard-types-parser
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+ task :default => :spec
@@ -0,0 +1,96 @@
1
+ require "yard_types/types"
2
+
3
+ module YardTypes
4
+
5
+ # Initial code taken from https://github.com/lsegal/yard-types-parser --
6
+ # unfortunately that was never released as a gem; and the code on master
7
+ # doesn't actually run.
8
+ #
9
+ # @api private
10
+ # @see YardTypes.parse
11
+ class Parser
12
+ TOKENS = {
13
+ collection_start: /</,
14
+ collection_end: />/,
15
+ tuple_start: /\(/,
16
+ tuple_end: /\)/,
17
+ type_name: /#\w+|((::)?\w+)+/,
18
+ type_next: /[,;]/,
19
+ whitespace: /\s+/,
20
+ hash_start: /\{/,
21
+ hash_next: /=>/,
22
+ hash_end: /\}/,
23
+ parse_end: nil
24
+ }
25
+
26
+ def self.parse(string)
27
+ TypeConstraint.new(new(string).parse)
28
+ end
29
+
30
+ def initialize(string)
31
+ @scanner = StringScanner.new(string)
32
+ end
33
+
34
+ def parse
35
+ types = []
36
+ type = nil
37
+ name = nil
38
+
39
+ loop do
40
+ found = false
41
+ TOKENS.each do |token_type, match|
42
+ if (match.nil? && @scanner.eos?) || (match && token = @scanner.scan(match))
43
+ found = true
44
+ case token_type
45
+ when :type_name
46
+ raise SyntaxError, "expecting END, got name '#{token}'" if name
47
+ name = token
48
+
49
+ when :type_next
50
+ raise SyntaxError, "expecting name, got '#{token}' at #{@scanner.pos}" if name.nil?
51
+ unless type
52
+ type = Type.for(name)
53
+ end
54
+ types << type
55
+ type = nil
56
+ name = nil
57
+
58
+ when :tuple_start, :collection_start
59
+ name ||=
60
+ token_type == :collection_start ? 'Array' : '<generic-tuple>'
61
+
62
+ type =
63
+ if name == 'Hash' && token_type == :collection_start
64
+ contents = parse
65
+ if contents.length != 2
66
+ raise SyntaxError, "expected 2 types for key/value; got #{contents.length}"
67
+ end
68
+
69
+ HashType.new(name, [contents[0]], [contents[1]])
70
+ elsif token_type == :collection_start
71
+ CollectionType.new(name, parse)
72
+ else
73
+ TupleType.new(name, parse)
74
+ end
75
+
76
+ when :hash_start
77
+ name ||= "Hash"
78
+ type = HashType.new(name, parse, parse)
79
+
80
+ when :hash_next, :hash_end, :tuple_end, :collection_end, :parse_end
81
+ raise SyntaxError, "expecting name, got '#{token}'" if name.nil?
82
+ unless type
83
+ type = Type.for(name)
84
+ end
85
+ types << type
86
+ return types
87
+ end
88
+ end
89
+
90
+ end
91
+ raise SyntaxError, "invalid character at #{@scanner.peek(1)}" unless found
92
+ end
93
+ end
94
+ end
95
+
96
+ end
@@ -0,0 +1,293 @@
1
+ module YardTypes
2
+
3
+ # A +TypeConstraint+ specifies the set of acceptable types
4
+ # which can satisfy the constraint. Parsing any YARD type
5
+ # description will return a +TypeConstraint+ instance.
6
+ #
7
+ # @see YardTypes.parse
8
+ class TypeConstraint
9
+ # @return [Array<Type>]
10
+ attr_reader :accepted_types
11
+
12
+ # @param types [Array<Type>] the list of acceptable types
13
+ def initialize(types)
14
+ @accepted_types = types
15
+ end
16
+
17
+ # @param i [Fixnum]
18
+ # @return [Type] the type at index +i+
19
+ # @todo deprecate this; remnant from original TDD'd API.
20
+ def [](i)
21
+ accepted_types[i]
22
+ end
23
+
24
+ # @return [Type] the first type
25
+ # @todo deprecate this; remnant from original TDD'd API.
26
+ def first
27
+ self[0]
28
+ end
29
+
30
+ # @param obj [Object] Any object.
31
+ # @return [Type, nil] The first type which matched +obj+,
32
+ # or +nil+ if none.
33
+ def check(obj)
34
+ accepted_types.find { |t| t.check(obj) }
35
+ end
36
+
37
+ # @return [String] A YARD type string describing this set of
38
+ # types.
39
+ def to_s
40
+ accepted_types.map(&:to_s).join(', ')
41
+ end
42
+ end
43
+
44
+ # The base class for all supported types.
45
+ class Type
46
+ # @return [String]
47
+ attr_accessor :name
48
+
49
+ # @todo This interface was just hacked into place while
50
+ # enhancing the parser to return {DuckType}, {KindType}, etc.
51
+ # @api private
52
+ def self.for(name)
53
+ case name
54
+ when /^#/
55
+ DuckType.new(name)
56
+ when *LiteralType.names
57
+ LiteralType.new(name)
58
+ else
59
+ KindType.new(name)
60
+ end
61
+ end
62
+
63
+ # @param name [String]
64
+ def initialize(name)
65
+ @name = name
66
+ end
67
+
68
+ # @return [String] a YARD type string describing this type.
69
+ def to_s
70
+ name
71
+ end
72
+
73
+ # @param obj [Object] Any object.
74
+ # @return [Boolean] whether the object is of this type.
75
+ # @raise [NotImplementedError] must be handled by the subclasses.
76
+ def check(obj)
77
+ raise NotImplementedError
78
+ end
79
+ end
80
+
81
+ # A {DuckType} constraint is specified as +#some_message+,
82
+ # and indicates that the object must respond to the method
83
+ # +some_message+.
84
+ class DuckType < Type
85
+ # @return [String] The method the object must respond to;
86
+ # this does not include the leading +#+ character.
87
+ attr_reader :message
88
+
89
+ # @param name [String] The YARD identifier, eg +#some_message+.
90
+ def initialize(name)
91
+ @name = name
92
+ @message = name[1..-1]
93
+ end
94
+
95
+ # @param (see Type#check)
96
+ # @return [Boolean] +true+ if the object responds to +message+.
97
+ def check(obj)
98
+ obj.respond_to? message
99
+ end
100
+ end
101
+
102
+ # A {KindType} constraint is specified as +SomeModule+ or
103
+ # +SomeClass+, and indicates that the object must be a kind of that
104
+ # module.
105
+ class KindType < Type
106
+ # Type checks a given object. Special consideration is given to
107
+ # the pseudo-class +Boolean+, which does not actually exist in Ruby,
108
+ # but is commonly used to mean +TrueClass, FalseClass+.
109
+ #
110
+ # @param (see Type#check)
111
+ # @return [Boolean] +true+ if +obj.kind_of?(constant)+.
112
+ def check(obj)
113
+ if name == 'Boolean'
114
+ obj == true || obj == false
115
+ else
116
+ obj.kind_of? constant
117
+ end
118
+ end
119
+
120
+ # @return [Module] the constant specified by +name+.
121
+ # @raise [TypeError] if the constant is neither a module nor a class
122
+ # @raise [NameError] if the specified constant could not be loaded.
123
+ def constant
124
+ @constant ||=
125
+ begin
126
+ const = name.split('::').reduce(Object) { |namespace, const|
127
+ namespace.const_get(const)
128
+ }
129
+
130
+ unless const.kind_of?(Module)
131
+ raise TypeError, "class or module required; #{name} is a #{const.class}"
132
+ end
133
+
134
+ const
135
+ end
136
+ end
137
+ end
138
+
139
+ # A {LiteralType} constraint is specified by the name of one of YARD's
140
+ # supported "literals": +true+, +false+, +nil+, +void+, and +self+, and
141
+ # indicates that the object must be exactly one of those values.
142
+ #
143
+ # However, +void+ and +self+ have no particular meaning: +void+ is typically
144
+ # used solely to specify that a method returns no meaningful types; and
145
+ # +self+ is used to specify that a method returns its receiver, generally
146
+ # to indicate that calls can be chained. All values type check as valid
147
+ # objects for +void+ and +self+ literals.
148
+ class LiteralType < Type
149
+ # @return [Array<String>] the list of supported literal identifiers.
150
+ def self.names
151
+ @literal_names ||= %w(true false nil void self)
152
+ end
153
+
154
+ # @param (see Type#check)
155
+ # @return [Boolean] +true+ if the object is exactly +true+, +false+, or
156
+ # +nil+ (depending on the value of +name+); for +void+ and +self+
157
+ # types, this method *always* returns +true+.
158
+ # @raise [NotImplementedError] if an unsupported literal name is to be
159
+ # tested against.
160
+ def check(obj)
161
+ case name
162
+ when 'true' then obj == true
163
+ when 'false' then obj == false
164
+ when 'nil' then obj == nil
165
+ when 'self', 'void' then true
166
+ else raise NotImplementedError, "Unsupported literal type: #{name.inspect}"
167
+ end
168
+ end
169
+ end
170
+
171
+ # A {CollectionType} is specified with the syntax +Kind<Some, #thing>+, and
172
+ # indicates that the object is a kind of +Kind+, containing only objects which
173
+ # type check against +Some+ or +#thing+.
174
+ #
175
+ # @todo The current implementation of type checking here requires that the collection
176
+ # respond to +all?+; this may not be ideal.
177
+ class CollectionType < Type
178
+ # @return [Array<Type>] the acceptable types for this collection's contents.
179
+ attr_accessor :types
180
+
181
+ # @param name [String] the name of the module the collection must be a kind of.
182
+ # @param types [Array<Type>] the acceptable types for the collection's contents.
183
+ def initialize(name, types)
184
+ @name = name
185
+ @types = types
186
+ end
187
+
188
+ # @return (see Type#to_s)
189
+ def to_s
190
+ "%s<%s>" % [name, types.map(&:to_s).join(', ')]
191
+ end
192
+
193
+ # @param (see Type#check)
194
+ # @return [Boolean] +true+ if the object is both a kind of +name+, and all of
195
+ # its contents (if any) are of the types in +types+. Any combination, order,
196
+ # and count of content types is acceptable.
197
+ def check(obj)
198
+ return false unless KindType.new(name).check(obj)
199
+
200
+ obj.all? do |el|
201
+ # TODO -- could probably just use another TypeConstraint here
202
+ types.any? { |type| type.check(el) }
203
+ end
204
+ end
205
+ end
206
+
207
+ # A {TupleType} is specified with the syntax +(Some, Types, #here)+, and indicates
208
+ # that the contents of the collection must be exactly that size, and each element
209
+ # must be of the exact type specified for that index.
210
+ #
211
+ # @todo The current implementation of type checking here requires that the collection
212
+ # respond to both +length+ and +[]+; this may not be ideal.
213
+ class TupleType < CollectionType
214
+ def initialize(name, types)
215
+ @name = name == '<generic-tuple>' ? nil : name
216
+ @types = types
217
+ end
218
+
219
+ # @return (see Type#to_s)
220
+ def to_s
221
+ "%s(%s)" % [name, types.map(&:to_s).join(', ')]
222
+ end
223
+
224
+ # @param (see Type#check)
225
+ # @return [Boolean] +true+ if the collection's +length+ is exactly the length of
226
+ # the expected +types+, and each element with the collection is of the type
227
+ # specified for that index by +types+.
228
+ def check(obj)
229
+ return false unless name.nil? || KindType.new(name).check(obj)
230
+ return false unless obj.respond_to?(:length) && obj.respond_to?(:[])
231
+ return false unless obj.length == types.length
232
+
233
+ enum = types.to_enum
234
+ enum.with_index.all? do |t, i|
235
+ t.check(obj[i])
236
+ end
237
+ end
238
+ end
239
+
240
+ # A {HashType} is specified with the syntax +{KeyType =>
241
+ # ValueType}+, and indicates that all keys in the hash must be of
242
+ # type +KeyType+, and all values must be of type +ValueType+.
243
+ #
244
+ # An alternate syntax for {HashType} is also available as +Hash<A,
245
+ # B>+, but its usage is not recommended; it is less capable than the
246
+ # +{A => B}+ syntax, as some inner type constraints can not be
247
+ # parsed reliably.
248
+ #
249
+ # A {HashType} actually only requires that the object respond to
250
+ # both +keys+ and +values+; it should be capable of type checking
251
+ # any object which conforms to that interface.
252
+ #
253
+ # @todo Enforce kind, eg +HashWithIndifferentAccess{#to_sym => Array}+,
254
+ # in case you _really_ care that it's indifferent. Maybe?
255
+ class HashType < Type
256
+ # @return [Array<Type>] the set of acceptable types for keys
257
+ attr_reader :key_types
258
+
259
+ # @return [Array<Type>] the set of acceptable types for values
260
+ attr_reader :value_types
261
+
262
+ # @param name [String] the kind of the expected object; currently unused.
263
+ # @param key_types [Array<Type>] the set of acceptable types for keys
264
+ # @param value_types [Array<Type>] the set of acceptable types for values
265
+ def initialize(name, key_types, value_types)
266
+ @name = name
267
+ @key_types = key_types
268
+ @value_types = value_types
269
+ end
270
+
271
+ # Unlike the other types, {HashType} can result from two alternate syntaxes;
272
+ # however, this method will *only* return the +{A => B}+ syntax.
273
+ #
274
+ # @return (see Type#to_s)
275
+ def to_s
276
+ "{%s => %s}" % [
277
+ key_types.map(&:to_s).join(', '),
278
+ value_types.map(&:to_s).join(', ')
279
+ ]
280
+ end
281
+
282
+ # @param (see Type#check)
283
+ # @return [Boolean] +true+ if the object responds to both +keys+ and +values+,
284
+ # and every key type checks against a type in +key_types+, and every value
285
+ # type checks against a type in +value_types+.
286
+ def check(obj)
287
+ return false unless obj.respond_to?(:keys) && obj.respond_to?(:values)
288
+ obj.keys.all? { |key| key_types.any? { |t| t.check(key) } } &&
289
+ obj.values.all? { |value| value_types.any? { |t| t.check(value) } }
290
+ end
291
+ end
292
+
293
+ end
@@ -0,0 +1,3 @@
1
+ module YardTypes
2
+ VERSION = "0.0.1"
3
+ end
data/lib/yard_types.rb ADDED
@@ -0,0 +1,55 @@
1
+ require "yard_types/version"
2
+ require "yard_types/types"
3
+ require "yard_types/parser"
4
+
5
+ module YardTypes
6
+ extend self
7
+
8
+ class Result
9
+ def initialize(pass = false)
10
+ @pass = pass
11
+ end
12
+
13
+ def success?
14
+ @pass == true
15
+ end
16
+ end
17
+
18
+ class Success < Result
19
+ def initialize
20
+ super(true)
21
+ end
22
+ end
23
+
24
+ class Failure < Result
25
+ def initialize
26
+ super(false)
27
+ end
28
+ end
29
+
30
+ # Parse a type string using the {Parser}, and return a
31
+ # {TypeConstraint} instance representing the described
32
+ # type.
33
+ #
34
+ # @param type [String, Array<String>] The YARD type description
35
+ # @return [TypeConstraint]
36
+ # @raise [SyntaxError] if the string could not be parsed
37
+ # @example
38
+ # type = YardTypes.parse('MyClass, #quacks_like_my_class')
39
+ # type.check(some_object)
40
+ def parse(type)
41
+ type = type.join(', ') if type.respond_to?(:join)
42
+ Parser.parse(type)
43
+ end
44
+
45
+ # @return [Result]
46
+ # @todo deprecate; rename it +check+ to match everything else.
47
+ def validate(type, obj)
48
+ constraint = parse(type)
49
+ if constraint.check(obj)
50
+ Success.new
51
+ else
52
+ Failure.new
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Defensive error raising' do
4
+ specify 'Type#check raises NotImplementedError' do
5
+ type = YardTypes::Type.new('Foo')
6
+ expect { type.check(nil) }.to raise_error(NotImplementedError)
7
+ end
8
+
9
+ specify 'LiteralType raises when checking for an unsupported literal' do
10
+ type = YardTypes::LiteralType.new('zero')
11
+ expect { type.check(0) }.to raise_error(NotImplementedError, /zero/)
12
+ end
13
+
14
+ specify 'KindType raises when its constant is neither module nor class' do
15
+ type = YardTypes::KindType.new('Math::PI')
16
+ expect { type.check(:anything) }.to raise_error(TypeError, 'class or module required; Math::PI is a Float')
17
+ end
18
+ end
@@ -0,0 +1,175 @@
1
+ require 'spec_helper'
2
+
3
+ describe YardTypes, 'parsing' do
4
+ def parse(type_string)
5
+ YardTypes.parse(type_string)
6
+ end
7
+
8
+ matcher :be_type_class do |type|
9
+ def type_class(type_identifier)
10
+ YardTypes.const_get("#{type_identifier.to_s.capitalize}Type")
11
+ end
12
+
13
+ match do |type_string|
14
+ result = YardTypes.parse(type_string)
15
+ result.first.instance_of? type_class(type)
16
+ end
17
+
18
+ description do |type_string|
19
+ "'#{type_string}' parses into a #{type_class(type).name} instance"
20
+ end
21
+
22
+ failure_message do |type_string|
23
+ "expected '#{type_string}' to parse into a #{type_class(type).name} instance"
24
+ end
25
+
26
+ failure_message_when_negated do |type_string|
27
+ "expected '#{type_string}' not to parse into a #{type_class(type).name} instance, but did"
28
+ end
29
+ end
30
+
31
+ matcher :have_inner_types do |*expected_inner_types|
32
+ def collection?(type)
33
+ type.respond_to?(:types)
34
+ end
35
+
36
+ def matching_count?(actual, expected)
37
+ actual.size == expected.size
38
+ end
39
+
40
+ def matching_type_classes?(actual, expected)
41
+ actual.zip(expected).all? do |type, (klass_name, type_name)|
42
+ klass = YardTypes.const_get("#{klass_name.to_s.capitalize}Type")
43
+ type.is_a?(klass) && type.name == type_name
44
+ end
45
+ end
46
+
47
+ match do |type_string|
48
+ type = YardTypes.parse(type_string).first
49
+ collection?(type) &&
50
+ matching_count?(type.types, expected_inner_types) &&
51
+ matching_type_classes?(type.types, expected_inner_types)
52
+ end
53
+ end
54
+
55
+ specify 'literals' do
56
+ expect('true').to be_type_class(:literal)
57
+ expect('false').to be_type_class(:literal)
58
+ expect('nil').to be_type_class(:literal)
59
+ expect('void').to be_type_class(:literal)
60
+ expect('self').to be_type_class(:literal)
61
+ end
62
+
63
+ specify 'duck' do
64
+ expect('#foo').to be_type_class(:duck)
65
+ end
66
+
67
+ specify 'kind' do
68
+ expect('Foo').to be_type_class(:kind)
69
+ expect('Array').to be_type_class(:kind)
70
+ expect('Hash').to be_type_class(:kind)
71
+ end
72
+
73
+ context 'parameterized array' do
74
+ specify 'not bare `Array` type' do
75
+ expect('Array').not_to be_type_class(:collection)
76
+ end
77
+
78
+ specify 'Array<...>' do
79
+ expect('Array<String>').to be_type_class(:collection)
80
+ expect('Array<#foo>').to be_type_class(:collection)
81
+ expect('Array<#a, #b>').to be_type_class(:collection)
82
+ end
83
+
84
+ specify 'inner types' do
85
+ expect('Array<String>').to have_inner_types([:kind, 'String'])
86
+
87
+ expect('Array<String, #to_date>').to have_inner_types([:kind, 'String'],
88
+ [:duck, '#to_date'])
89
+ end
90
+ end
91
+
92
+ context 'tuples' do
93
+ specify '(...)' do
94
+ expect('(String)').to be_type_class(:tuple)
95
+ expect('(String, #to_date, true)').to be_type_class(:tuple)
96
+ end
97
+
98
+ specify 'inner types' do
99
+ expect('(String)').to have_inner_types([:kind, 'String'])
100
+
101
+ expect('(String, #to_date, true)').to have_inner_types([:kind, 'String'],
102
+ [:duck, '#to_date'],
103
+ [:literal, 'true'])
104
+ end
105
+ end
106
+
107
+ context 'hashes' do
108
+ specify 'Hash<a, b>' do
109
+ expect('Hash<#a, #b>').to be_type_class(:hash)
110
+ expect('Hash<Fixnum, String>').to be_type_class(:hash)
111
+ expect('Hash<(#some, #tuple), Array<#to_date>>').to be_type_class(:hash)
112
+ end
113
+
114
+ specify 'Hash<a> | Hash<a, b, c> => SyntaxError' do
115
+ expect { parse('Hash<a>') }.to raise_error(SyntaxError)
116
+ expect { parse('Hash<a, b, c>') }.to raise_error(SyntaxError)
117
+ end
118
+
119
+ specify '{a => b}' do
120
+ expect('{A => B}').to be_type_class(:hash)
121
+ expect('{#a, #b => #to_date}').to be_type_class(:hash)
122
+ end
123
+ end
124
+ end
125
+
126
+ describe YardTypes::Type, '.parse' do
127
+ it "can accept a single YARD type string" do
128
+ constraint = YardTypes.parse('Array, Hash')
129
+ expect(constraint).to be_instance_of(YardTypes::TypeConstraint)
130
+ expect(constraint.to_s).to eq('Array, Hash')
131
+ end
132
+
133
+ it "can accept an array of individual type strings, and return a single Constraint" do
134
+ constraint = YardTypes.parse(['Array', 'Hash'])
135
+ expect(constraint).to be_instance_of(YardTypes::TypeConstraint)
136
+ expect(constraint.to_s).to eq('Array, Hash')
137
+ end
138
+ end
139
+
140
+ describe YardTypes::Type, '#to_s' do
141
+ [
142
+ # Kind
143
+ 'String', 'Boolean', 'Array', 'String, Symbol',
144
+
145
+ # Duck
146
+ '#foo', '#foo, #bar',
147
+
148
+ # Literals
149
+ 'true', 'false', 'self', 'nil', 'void', 'true, false, nil',
150
+
151
+ # Collection
152
+ 'Array<Fixnum>', 'Array<Fixnum, (#to_i, #to_f)>', 'Set<Date>',
153
+
154
+ # Tuple
155
+ '(String, Boolean)', '(A, B), (C, D)',
156
+
157
+ # Hash
158
+ '{String => Symbol}', '{#a, #b => (A, B)}', '{#foo => #bar}, {Fixnum => String}',
159
+
160
+ # Crazy
161
+ '(Array<(#foo, #bar), {String => Symbol}>, #to_sym, (nil, Boolean))'
162
+ ].each do |string|
163
+
164
+ specify string do
165
+ parsed = YardTypes.parse(string)
166
+ expect(parsed.to_s).to eq(string)
167
+ end
168
+
169
+ end
170
+
171
+ it "does not preserve Hash<> notation" do
172
+ parsed = YardTypes.parse('Hash<(#a, #b), Symbol>')
173
+ expect(parsed.to_s).to eq('{(#a, #b) => Symbol}')
174
+ end
175
+ end
@@ -0,0 +1,13 @@
1
+ require 'simplecov'
2
+ SimpleCov.start do
3
+ add_filter "/spec/"
4
+ end
5
+
6
+ require 'yard_types'
7
+ require 'pry'
8
+
9
+ RSpec.configure do |config|
10
+ config.order = :rand
11
+ config.filter_run focus: true
12
+ config.run_all_when_everything_filtered = true
13
+ end
@@ -0,0 +1,245 @@
1
+ require 'spec_helper'
2
+ require 'set'
3
+
4
+ describe YardTypes, 'type checking' do
5
+ matcher :type_check do |obj|
6
+ match do |type|
7
+ result = YardTypes.validate(type, obj)
8
+ result.success?
9
+ end
10
+
11
+ description do |type|
12
+ "type checks against #{type.inspect}"
13
+ end
14
+
15
+ failure_message do |type|
16
+ "expected `#{obj.inspect}` to type check against #{type.inspect}"
17
+ end
18
+
19
+ failure_message_when_negated do |type|
20
+ "expected `#{obj.inspect}` not to type check against #{type.inspect}"
21
+ end
22
+ end
23
+
24
+ context 'ducks' do
25
+ specify 'responds' do
26
+ expect('#to_s').to type_check(nil)
27
+ expect('#reverse').to type_check('foo')
28
+ expect('#name').to type_check(Class)
29
+ end
30
+
31
+ specify 'does not respond' do
32
+ expect('#bogus').not_to type_check(nil)
33
+ end
34
+ end
35
+
36
+ context 'kinds' do
37
+ specify 'is kind_of' do
38
+ expect('String').to type_check('')
39
+ expect('Object').to type_check([])
40
+ end
41
+
42
+ specify 'is not kind_of' do
43
+ expect('String').not_to type_check([])
44
+ end
45
+
46
+ specify 'Boolean == true || false' do
47
+ expect('Boolean').to type_check(true)
48
+ expect('Boolean').to type_check(false)
49
+ expect('Boolean').not_to type_check(nil)
50
+ end
51
+
52
+ specify 'constant resolution' do
53
+ expect('YardTypes::DuckType').to type_check(YardTypes::DuckType.new('#foo'))
54
+ end
55
+
56
+ specify 'unknown constant' do
57
+ expect {
58
+ type = YardTypes.parse('ReversedString')[0] # mind the typo
59
+ type.check('gnirts')
60
+ }.to raise_error(NameError)
61
+ end
62
+ end
63
+
64
+ context 'arrays' do
65
+ specify 'inner type' do
66
+ # Empty always passes
67
+ expect('Array<String>').to type_check([])
68
+
69
+ # Every element passes
70
+ expect('Array<#reverse>').to type_check(['foo', 'bar'])
71
+ expect('Array<#reverse>').to type_check([['a'], 'foo'])
72
+
73
+ # Every element fails
74
+ expect('Array<#reverse>').not_to type_check([1])
75
+
76
+ # Some element fails
77
+ expect('Array<#reverse>').not_to type_check(['foo', 1])
78
+ end
79
+ end
80
+
81
+ context 'alternate collection types' do
82
+ specify 'Set<Symbol>' do
83
+ array = [:foo, :bar]
84
+ set = Set.new(array)
85
+
86
+ expect('Set<Symbol>').to type_check(set)
87
+ expect('Set<Symbol>').not_to type_check(array)
88
+ end
89
+ end
90
+
91
+ context 'tuples' do
92
+ class ::MyTuple < Array
93
+ end
94
+
95
+ let(:type) { '(String, Fixnum, #reverse)' }
96
+
97
+ specify 'matches' do
98
+ expect(type).to type_check(['foo', 1, []])
99
+ end
100
+
101
+ specify 'one type is wrong' do
102
+ expect(type).not_to type_check([:nope, 1, []])
103
+ expect(type).not_to type_check(['foo', 1.0, []])
104
+ expect(type).not_to type_check(['foo', 1, nil])
105
+ end
106
+
107
+ specify 'invalid length' do
108
+ expect(type).not_to type_check([])
109
+ expect(type).not_to type_check(['foo'])
110
+ expect(type).not_to type_check(['foo', 1])
111
+ expect(type).not_to type_check(['foo', 1, [], true])
112
+ end
113
+
114
+ specify 'unspecified kind accepts any kind' do
115
+ tuple = MyTuple.new
116
+ tuple[0] = 'hi'
117
+ tuple[1] = 1
118
+ tuple[2] = []
119
+
120
+ expect(type).to type_check(tuple)
121
+ end
122
+
123
+ context 'specified kind' do
124
+ let(:type) { 'MyTuple(String, Fixnum)' }
125
+
126
+ specify 'kind + contents match' do
127
+ tuple = MyTuple.new
128
+ tuple[0] = 'hi'
129
+ tuple[1] = 1
130
+
131
+ expect(type).to type_check(tuple)
132
+ end
133
+
134
+ specify 'kind matches, contents do not' do
135
+ expect(type).not_to type_check(MyTuple.new)
136
+ end
137
+
138
+ specify 'contents match, but kind does not' do
139
+ expect(type).not_to type_check(['hi', 1])
140
+ end
141
+ end
142
+ end
143
+
144
+ context 'hash' do
145
+ context 'Hash<> syntax' do
146
+ let(:type) { 'Hash<Fixnum, String>' }
147
+
148
+ specify 'matches' do
149
+ expect(type).to type_check({ 1 => 'foo', 2 => 'bar' })
150
+ end
151
+
152
+ specify 'wrong key type' do
153
+ expect(type).not_to type_check({ 1.0 => 'foo' })
154
+ expect(type).not_to type_check({ 1 => 'foo', :wrong => 'bar' })
155
+ end
156
+
157
+ specify 'wrong value type' do
158
+ expect(type).not_to type_check({ 1 => :foo })
159
+ expect(type).not_to type_check({ 1 => 'foo', 2 => :bar })
160
+ end
161
+
162
+ specify 'quacks like a hash' do
163
+ map_type = Struct.new(:keys, :values)
164
+ hash_map = map_type.new([1, 2], ['three', 'four'])
165
+ expect(type).to type_check(hash_map)
166
+ end
167
+ end
168
+
169
+ context 'Hash{} syntax' do
170
+ let(:type) { '{Boolean => #reverse}' }
171
+
172
+ specify 'matches' do
173
+ expect(type).to type_check(false => [])
174
+ expect(type).to type_check(true => 'foo', false => [])
175
+ end
176
+
177
+ specify 'wrong key type' do
178
+ expect(type).not_to type_check(:false => [])
179
+ expect(type).not_to type_check(false => [], 'true' => 'bar')
180
+ end
181
+
182
+ specify 'wrong value type' do
183
+ expect(type).not_to type_check(true => :fail)
184
+ expect(type).not_to type_check(true => 'pass', false => :fail)
185
+ end
186
+
187
+ specify 'quacks like a hash' do
188
+ map_type = Struct.new(:keys, :values)
189
+ hash_map = map_type.new([true, false], ['three', [:four]])
190
+ expect(type).to type_check(hash_map)
191
+ end
192
+ end
193
+ end
194
+
195
+ context 'literals' do
196
+ specify 'nil' do
197
+ expect('nil').to type_check(nil)
198
+ expect('nil').not_to type_check(false)
199
+ end
200
+
201
+ specify 'true' do
202
+ expect('true').to type_check(true)
203
+ expect('true').not_to type_check(false)
204
+ expect('true').not_to type_check(nil)
205
+ end
206
+
207
+ specify 'false' do
208
+ expect('false').to type_check(false)
209
+ expect('false').not_to type_check(true)
210
+ expect('false').not_to type_check(nil)
211
+ end
212
+
213
+ specify 'void' do
214
+ expect('void').to type_check(nil)
215
+ expect('void').to type_check('')
216
+ expect('void').to type_check(['anything', :really])
217
+ end
218
+
219
+ specify 'self' do
220
+ expect('self').to type_check(nil)
221
+ expect('self').to type_check('')
222
+ expect('self').to type_check(['anything', :really])
223
+ end
224
+ end
225
+
226
+ context 'multiple acceptable types' do
227
+ specify 'String, Symbol' do
228
+ expect('String, Symbol').to type_check('foo')
229
+ expect('String, Symbol').to type_check(:foo)
230
+ expect('String, Symbol').not_to type_check([])
231
+ expect('String, Symbol').not_to type_check(['foo', :foo])
232
+ end
233
+
234
+ specify 'Array<A, B>' do
235
+ expect('Array<Fixnum, #to_i>').to type_check([])
236
+ expect('Array<Fixnum, #to_i>').to type_check([1])
237
+ expect('Array<Fixnum, #to_i>').to type_check(['1'])
238
+ expect('Array<Fixnum, #to_i>').to type_check([nil])
239
+
240
+ expect('Array<Fixnum, #to_i>').not_to type_check([:oops])
241
+ expect('Array<Fixnum, #to_i>').not_to type_check(nil)
242
+ end
243
+ end
244
+
245
+ end
data/yard_type.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'yard_types/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "yard_types"
8
+ spec.version = YardTypes::VERSION
9
+ spec.authors = ["Kyle Hargraves"]
10
+ spec.email = ["pd@krh.me"]
11
+ spec.summary = %q{Parse and validate objects against YARD type descriptions.}
12
+ spec.description = %q{Your API docs say you return Array<#to_date>, but do you really?}
13
+ spec.homepage = "https://github.com/pd/yard_types"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.5"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_development_dependency "rspec", "~> 3.0"
24
+ spec.add_development_dependency "simplecov", "~> 0.7.1"
25
+ spec.add_development_dependency "pry", "> 0"
26
+ end
metadata ADDED
@@ -0,0 +1,133 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: yard_types
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Kyle Hargraves
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-07-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.5'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.5'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: simplecov
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.7.1
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 0.7.1
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">"
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">"
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: Your API docs say you return Array<#to_date>, but do you really?
84
+ email:
85
+ - pd@krh.me
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".gitignore"
91
+ - ".travis.yml"
92
+ - Gemfile
93
+ - LICENSE.txt
94
+ - README.md
95
+ - Rakefile
96
+ - lib/yard_types.rb
97
+ - lib/yard_types/parser.rb
98
+ - lib/yard_types/types.rb
99
+ - lib/yard_types/version.rb
100
+ - spec/errors_spec.rb
101
+ - spec/parsing_spec.rb
102
+ - spec/spec_helper.rb
103
+ - spec/type_checking_spec.rb
104
+ - yard_type.gemspec
105
+ homepage: https://github.com/pd/yard_types
106
+ licenses:
107
+ - MIT
108
+ metadata: {}
109
+ post_install_message:
110
+ rdoc_options: []
111
+ require_paths:
112
+ - lib
113
+ required_ruby_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ required_rubygems_version: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ requirements: []
124
+ rubyforge_project:
125
+ rubygems_version: 2.2.0
126
+ signing_key:
127
+ specification_version: 4
128
+ summary: Parse and validate objects against YARD type descriptions.
129
+ test_files:
130
+ - spec/errors_spec.rb
131
+ - spec/parsing_spec.rb
132
+ - spec/spec_helper.rb
133
+ - spec/type_checking_spec.rb