contraction 0.2.6 → 0.3.0

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: 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: []