yard_types 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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