contraction 0.2.6 → 0.3.0

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: e0f7aa54dcbe857b462c9f55a7b72e5462b25cec
4
+ data.tar.gz: 5b338f1f833a9434b61dfd0b423387398a6789ce
5
+ SHA512:
6
+ metadata.gz: 4e31cc128163153debc169166fca37bb472473bfb55f24da2a629dbbe577e3b2080b001820c905cec99825b9942f74e2148c6d1905a5a289291bd724d175eebf
7
+ data.tar.gz: f73b4bb2677a13d62c56d935647d1775ab3480e7b5f68f1f18343f0f02010db443d20614a3e4e9b95f3691ef0ff92835d13420e7ad8b984bed163f13c5115cce
data/lib/parser.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'string'
2
2
  require 'parser/type'
3
3
  require 'parser/lines'
4
+ require 'parser/type_parser'
4
5
  require 'contract'
5
6
 
6
7
  module Contraction
data/lib/parser/type.rb CHANGED
@@ -1,138 +1,24 @@
1
- # FIXME: Actually use the type parser in the actual parser... Duh.
2
1
  module Contraction
3
2
  module Parser
4
3
  class Type
5
- attr_reader :legal_types, :method_requirements, :length, :key_types, :value_types
6
-
4
+ attr_reader :type
7
5
  def initialize(part)
8
- @legal_types = []
9
- @method_requirements = []
10
- @length = -1
11
- @key_types = []
12
- @value_types = []
13
-
14
6
  parse(part)
15
7
  end
16
8
 
17
9
  # Checks weather or not thing is a given type.
18
- # @param [String] thing A string containing a type definition. For example:
19
- # Array<String>
20
10
  def check(thing)
21
- check_types(thing) &&
22
- check_duck_typing(thing) &&
23
- check_length(thing) &&
24
- check_hash(thing)
11
+ return true unless type
12
+ type.check thing
25
13
  end
26
14
 
27
15
  private
28
16
 
29
17
  def parse(line)
30
- parse_typed_container(line) ||
31
- parse_duck_type(line) ||
32
- parse_fixed_list(line) ||
33
- parse_hash(line) ||
34
- parse_short_hash_or_reference(line) ||
35
- parse_regular(line)
36
- end
37
-
38
- def parse_typed_container(line)
39
- return unless line.include? '<'
40
- # It's some kind of container that can only hold certain things
41
- list = line.match(/\<(?<list>[^\>]+)\>/)['list']
42
- list.split(',').each do |type|
43
- @legal_types << Type.new(type.strip)
44
- end
45
- true
46
- end
47
-
48
- def parse_duck_type(line)
49
- return unless line =~ /^#/
50
- # It's a duck-typed object of some kind
51
- methods = line.split(",").map { |p| p.strip.gsub(/^#/,'').to_sym }
52
- @method_requirements += methods
53
- true
54
- end
55
-
56
- def parse_fixed_list(line)
57
- return unless line.include?('(')
58
- # It's a fixed-length list
59
- list = line.match(/\((?<list>[^\>]+)\)/)['list']
60
- parts = list.split(',')
61
- @length = parts.length
62
- parts.each do |type|
63
- @legal_types << Type.new(type.strip)
64
- end
65
- true
66
- end
67
-
68
- def parse_hash(line)
69
- return unless line.include? 'Hash{'
70
- # It's a hash with specific key-value pair types
71
- parts = line.match(/\{(?<key_types>.+)\s*=\>\s*(?<value_types>[^\}]+)\}/)
72
- @key_types = parts['key_types'].split(',').map { |t| t.include?('#') ? t.strip.gsub(/^#/, '').to_sym : t.strip.constantize }
73
- @value_types = parts['value_types'].split(',').map { |t| t.include?('#') ? t.strip.gsub(/^#/, '').to_sym : t.strip.constantize }
74
- end
75
-
76
- def parse_short_hash_or_reference(line)
77
- return unless line.include? '{'
78
- if parts = line.match(/\{(?<key_types>.+)\s*=\>\s*(?<value_types>[^\}]+)\}/)
79
- @key_types = parts['key_types'].split(',').map { |t| t.include?('#') ? t.strip.gsub(/^#/, '').to_sym : t.strip.constantize }
80
- @value_types = parts['value_types'].split(',').map { |t| t.include?('#') ? t.strip.gsub(/^#/, '').to_sym : t.strip.constantize }
81
- else
82
- # It's a reference to another documented type defined someplace in
83
- # the codebase. We can ignore the reference, and treat it like a
84
- # normal type.
85
- @legal_types << line.gsub(/\{|\}/, '').constantize
86
- end
87
- true
88
- end
89
-
90
- def parse_regular(line)
91
- # It's a regular-ass type.
92
- @legal_types << line.constantize
93
- end
94
-
95
- def check_hash(thing)
96
- return true if @key_types.empty? or @value_types.empty?
97
- return false unless thing.is_a?(Hash)
98
- thing.keys.all? do |k|
99
- @key_types.any? { |kt| kt.is_a?(Symbol) ? k.respond_to?(kt) : k.is_a?(kt) }
100
- end &&
101
- thing.values.all? do |v|
102
- @value_types.any? { |vt| vt.is_a?(Symbol) ? v.respond_to?(vt) : v.is_a?(vt) }
103
- end
104
- end
105
-
106
- def check_length(thing)
107
- return true if @length == -1
108
- thing.length == @length
109
- end
110
-
111
- def check_duck_typing(thing)
112
- return true if @method_requirements.empty?
113
- @method_requirements.all? do |m|
114
- thing.respond_to? m
115
- end
116
- end
117
-
118
- def check_types(thing)
119
- return true if @legal_types.empty?
120
- if thing.is_a? Enumerable
121
- types = @legal_types.map { |t| t.respond_to?(:legal_types) ? t.legal_types : t }.flatten
122
- return thing.all? { |th| types.include?(th.class) }
123
- else
124
- @legal_types.any? do |t|
125
- if t.is_a?(Contraction::Parser::Type)
126
- # Given the fact that we check enumerables above, we should never be here.
127
- next false
128
- end
129
- if thing.is_a?(Enumerable)
130
- thing.all? { |th| th.is_a?(t) }
131
- else
132
- thing.is_a?(t)
133
- end
134
- end
135
- end
18
+ @type = Contraction::TypeParser.parse(line).first
19
+ rescue => e
20
+ puts e
21
+ @type = nil
136
22
  end
137
23
  end
138
24
  end
@@ -0,0 +1,374 @@
1
+ module Contraction
2
+ # The lexer scans the input, creating a stack of tokens that can be used
3
+ # to then figure out our parse tree.
4
+ class TypeLexer
5
+ TOKENS = [
6
+ /^Hash/,
7
+ /^=>/,
8
+ /^\{/, /^\}/,
9
+ /^\[/, /^\]/,
10
+ /^\(/, /^\)/,
11
+ /^</, /^>/,
12
+ /^,/,
13
+ /^#/,
14
+ /([a-z_]+[a-z0-9_]*|(H(?!ash)))?[^=\{\[\(<>\)\]\},#]+/
15
+ ]
16
+
17
+ def self.lex(text)
18
+ stack = []
19
+ while text.length > 0
20
+ changed = false
21
+
22
+ TOKENS.each do |r|
23
+ if m = text.match(r)
24
+ if m[0].strip != ''
25
+ stack << m[0].strip
26
+ end
27
+ text.sub! r, ''
28
+ changed = true
29
+ break
30
+ end
31
+ end
32
+
33
+ raise "Unknown token found at #{text}" unless changed
34
+ end
35
+
36
+ stack.reverse
37
+ end
38
+ end
39
+
40
+ class Type
41
+ attr_reader :klass
42
+ def initialize(klass)
43
+ @klass = klass
44
+ end
45
+
46
+ def works_as_a?(thing)
47
+ thing == klass || thing.is_a?(klass)
48
+ end
49
+
50
+ def check(thing)
51
+ works_as_a?(thing)
52
+ end
53
+ end
54
+
55
+ class DuckType
56
+ attr_reader :method
57
+ def initialize(method)
58
+ @method = method
59
+ end
60
+
61
+ def check(thing)
62
+ thing.respond_to? method.to_sym
63
+ end
64
+ end
65
+
66
+ class TypeList
67
+ attr_reader :types
68
+ def initialize(things)
69
+ @types = things.flatten
70
+ end
71
+
72
+ def works_as_a?(thing)
73
+ types.any? { |t| t.works_as_a? thing }
74
+ end
75
+
76
+ def check(thing)
77
+ # The only time that we need to match all instead of any is with
78
+ # duck-typing, so we just special-case it here.
79
+ if types.all? { |t| t.is_a? Contraction::DuckType }
80
+ return types.all? { |t| t.check(thing) }
81
+ else
82
+ return types.any? { |t| t.check(thing) }
83
+ end
84
+ end
85
+
86
+ def size
87
+ types.size
88
+ end
89
+ end
90
+
91
+ class HashType
92
+ attr_reader :key_type, :value_type
93
+ def initialize(key_type, value_type)
94
+ @key_type = key_type
95
+ @value_type = value_type
96
+ end
97
+
98
+ def check(thing)
99
+ thing.is_a?(Hash) &&
100
+ thing.keys.all? { |k| key_type.check(k) } &&
101
+ thing.values.all? { |v| value_type.check(v) }
102
+ end
103
+ end
104
+
105
+ class TypedContainer
106
+ attr_reader :type_list, :class_name
107
+
108
+ def initialize(class_type, type_list)
109
+ @type_list = type_list
110
+ @class_name = class_type
111
+ end
112
+
113
+ def works_as_a?(thing)
114
+ type_list.works_as_a? thing
115
+ end
116
+
117
+ def check(thing)
118
+ return false if !class_name.nil? && !class_name.check(thing)
119
+ thing.all? { |v| type_list.works_as_a? v }
120
+ end
121
+ end
122
+
123
+ class SizedContainer < TypedContainer
124
+ def check(thing)
125
+ super && thing.size == type_list.size
126
+ end
127
+ end
128
+
129
+ class ReferenceType
130
+ attr_reader :klass
131
+ def initialize(klass)
132
+ @klass = klass
133
+ end
134
+
135
+ def check(thing)
136
+ thing.is_a? klass
137
+ end
138
+ end
139
+
140
+ class TypeParser
141
+ def self.parse(string)
142
+ @stack = TypeLexer.lex(string)
143
+
144
+ # We are going to walk though this one at a time, popping off the
145
+ # end, and seeing if the list of thing we have so far matches any
146
+ # known rules, being as greedy as possible.
147
+ things = [:typed_container, :sized_container, :type_list, :reference, :hash, :duck_type]
148
+ something_happened = false
149
+ data = []
150
+ begin
151
+ something_happened = false
152
+ things.each do |t|
153
+ thing = send(t)
154
+ if thing
155
+ data << thing
156
+ something_happened = true
157
+ end
158
+ end
159
+ end while something_happened
160
+
161
+ raise "Type parse error #{@stack.reverse.join ' '}" unless @stack.compact.empty?
162
+ data.flatten
163
+ end
164
+
165
+ # A class name is anything that has a capitol first-letter
166
+ def self.class_name
167
+ thing = @stack.pop
168
+ return nil if thing.nil?
169
+ if thing[0] =~ /^[A-Z]/
170
+ return Type.new(thing.constantize)
171
+ else
172
+ @stack.push thing
173
+ return nil
174
+ end
175
+ end
176
+
177
+ # A duck-type is a thing prefaced with '#', indicating that it must have
178
+ # that method.
179
+ def self.duck_type
180
+ thing = @stack.pop
181
+ return nil if thing.nil?
182
+ if thing != '#'
183
+ @stack.push thing
184
+ return nil
185
+ end
186
+
187
+ DuckType.new @stack.pop
188
+ end
189
+
190
+ # A type is either hash, or any class-name like thing
191
+ def self.type
192
+ reference || hash || typed_container || sized_container || class_name || duck_type
193
+ end
194
+
195
+ # A type-list is a Type, optionally followed by a comma and another
196
+ # type-list
197
+ def self.type_list
198
+ things = []
199
+ things << type
200
+ return nil if things.first.nil?
201
+
202
+ things << @stack.pop
203
+ if things.last != ','
204
+ @stack.push things.pop
205
+ return TypeList.new things
206
+ end
207
+ things.pop # Remove the ',' from the list
208
+
209
+ things << type_list.types
210
+ TypeList.new(things.flatten)
211
+ end
212
+
213
+ # A hash starts with an optional "Hash", and this then followed by an
214
+ # opening {, followed by a type-list, followed by a fat arrow ("=>"),
215
+ # followed by another type-list, followed by a closing curly brace
216
+ # ("}")
217
+ def self.hash
218
+ things = []
219
+ things << @stack.pop
220
+ if things.first != 'Hash' && things.first != '{'
221
+ things.size.times { @stack.push things.pop }
222
+ return nil
223
+ end
224
+
225
+ if things.first == 'Hash'
226
+ things << @stack.pop
227
+ end
228
+
229
+ if things.last != '{'
230
+ things.size.times { @stack.push things.pop }
231
+ return nil
232
+ end
233
+
234
+ # Get the first type
235
+ key_type = type_list
236
+ if !key_type
237
+ things.size.times { @stack.push things.pop }
238
+ return nil
239
+ else
240
+ things << key_type
241
+ end
242
+
243
+ # And the arrow
244
+ things << @stack.pop
245
+ if things.last != '=>'
246
+ things.size.times { @stack.push things.pop }
247
+ return nil
248
+ end
249
+
250
+ # And the value type
251
+ value_type = type_list
252
+ if !value_type
253
+ things.size.times { @stack.push things.pop }
254
+ return nil
255
+ end
256
+
257
+ # Finally, the colosing brace
258
+ things << @stack.pop
259
+ if things.last != '}'
260
+ things.size.times { @stack.push things.pop }
261
+ return nil
262
+ end
263
+
264
+ HashType.new(key_type, value_type)
265
+ end
266
+
267
+ # A typed container is an optional class type, followed by a '<', followed
268
+ # by a type list, followed by a '>'
269
+ def self.typed_container
270
+ class_type = class_name
271
+
272
+ bracket = @stack.pop
273
+ if bracket.nil?
274
+ if class_type
275
+ @stack.push class_type.klass.to_s
276
+ end
277
+ return nil
278
+ end
279
+ if bracket != '<'
280
+ @stack.push bracket
281
+
282
+ if class_type
283
+ @stack.push class_type.klass.to_s
284
+ end
285
+ return nil
286
+ end
287
+
288
+ types = type_list
289
+ if !types
290
+ @stack.push bracket
291
+
292
+ if class_type
293
+ @stack.push class_type.klass.to_s
294
+ end
295
+ return nil
296
+ end
297
+
298
+ bracket2 = @stack.pop
299
+ if bracket2.nil? || bracket2 != '>'
300
+ raise "Expected '>', got #{bracket2}: #{@stack.inspect}"
301
+ end
302
+
303
+ TypedContainer.new(class_type, types)
304
+ end
305
+
306
+ # A reference is a "{" followed by a type, followed by a "}"
307
+ def self.reference
308
+ b = @stack.pop
309
+ return nil if b.nil?
310
+ if b != '{'
311
+ @stack.push b
312
+ return nil
313
+ end
314
+
315
+ t = class_name
316
+ if !t
317
+ @stack.push b
318
+ return nil
319
+ end
320
+
321
+ b2 = @stack.pop
322
+ if b2.nil? || b2 != '}'
323
+ @stack.push b2 if b2
324
+ @stack.push t.klass.to_s
325
+ @stack.push b
326
+ return nil
327
+ end
328
+
329
+ return ReferenceType.new t
330
+ end
331
+
332
+ # A sized container is like a typed container, except it has a type for
333
+ # every member of the set. So if there are 3 types in the type list, the
334
+ # final type must be a container with exactly three members, conforming to
335
+ # their respective types. An example would be a Vector3 class, with the
336
+ # initializer defined as either 3 floats, or a Array[Float, Float, Float]
337
+ def self.sized_container
338
+ class_type = class_name
339
+
340
+ bracket = @stack.pop
341
+ if bracket.nil?
342
+ if class_type
343
+ @stack.push class_type.klass.to_s
344
+ end
345
+ return nil
346
+ end
347
+ if bracket != '('
348
+ @stack.push bracket
349
+
350
+ if class_type
351
+ @stack.push class_type.klass.to_s
352
+ end
353
+ return nil
354
+ end
355
+
356
+ types = type_list
357
+ if !types
358
+ @stack.push bracket
359
+
360
+ if class_type
361
+ @stack.push class_type.klass.to_s
362
+ end
363
+ return nil
364
+ end
365
+
366
+ bracket2 = @stack.pop
367
+ if bracket2.nil? || bracket2 != ')'
368
+ raise "Expected ']', got #{bracket2}: #{@stack.inspect}"
369
+ end
370
+
371
+ SizedContainer.new(class_type, types)
372
+ end
373
+ end
374
+ end
metadata CHANGED
@@ -1,15 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: contraction
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.6
5
- prerelease:
4
+ version: 0.3.0
6
5
  platform: ruby
7
6
  authors:
8
7
  - Thomas Luce
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2014-07-19 00:00:00.000000000 Z
11
+ date: 2014-07-22 00:00:00.000000000 Z
13
12
  dependencies: []
14
13
  description: Using RDoc documentation as your contract definition, you get solid code,
15
14
  and good docs. Win-win!
@@ -26,32 +25,29 @@ files:
26
25
  - lib/parser.rb
27
26
  - lib/parser/lines.rb
28
27
  - lib/parser/type.rb
28
+ - lib/parser/type_parser.rb
29
29
  - lib/string.rb
30
30
  homepage: https://github.com/thomasluce/contraction
31
31
  licenses: []
32
+ metadata: {}
32
33
  post_install_message:
33
34
  rdoc_options: []
34
35
  require_paths:
35
36
  - lib
36
37
  required_ruby_version: !ruby/object:Gem::Requirement
37
- none: false
38
38
  requirements:
39
- - - ! '>='
39
+ - - '>='
40
40
  - !ruby/object:Gem::Version
41
41
  version: '0'
42
- segments:
43
- - 0
44
- hash: -2650941166078552798
45
42
  required_rubygems_version: !ruby/object:Gem::Requirement
46
- none: false
47
43
  requirements:
48
- - - ! '>='
44
+ - - '>='
49
45
  - !ruby/object:Gem::Version
50
46
  version: '0'
51
47
  requirements: []
52
48
  rubyforge_project:
53
- rubygems_version: 1.8.24
49
+ rubygems_version: 2.2.2
54
50
  signing_key:
55
- specification_version: 3
51
+ specification_version: 4
56
52
  summary: A simple desgin-by-contract library
57
53
  test_files: []