speculation 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 19dd9abfde4552ea62d19dda2714f9dd0e6f33a8
4
- data.tar.gz: 112bed767778c508470b5d3303937580158bd649
3
+ metadata.gz: 11bb7e3c5871a34e1419aa9b02bfce56bdc334a2
4
+ data.tar.gz: b4d3470f9bb704a94b61f333a632f193a8baa21d
5
5
  SHA512:
6
- metadata.gz: 1e0d5b36cb7c3e868004e4e62b408bc1a80c144759791c2ae138fefcef90a345106fde0780c7ac8b1fee36c0049c3e8f922af8a72e9ceeed9c133337d494514f
7
- data.tar.gz: fb3eb18c1a8a4e1be72a0223f30c11dd05170a1b9225bfd73bebb6a2f27bda4255d51ae952597bf5bf3dd8301ce6990cac1fbe481d3897caf9f54e5b531cc7ef
6
+ metadata.gz: 1db14032f7e8755385fcd56346c8d04e712b3faf496caa9ecd77784f2ae7442edd0addea2d757d19e418970cbf7a408e82cef6291436f20dc18420312c893092
7
+ data.tar.gz: '096a7f073629269c834a691a2ad370b9e8722ffdc97fd8885a60b2ec299c29cfb3ce90be6d43b8414a5669afe31468cd6f3a8b3fa2f588a2991a7272cbc7fe22'
data/.gitignore CHANGED
@@ -7,3 +7,4 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ /bin/
data/Gemfile CHANGED
@@ -1,7 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  source "https://rubygems.org"
3
3
 
4
- gem "test-queue", :github => "bboe/test-queue", :ref => "578e1ebe47b171b9d488c61ba112742e32ee0cfc"
5
-
6
- # Specify your gem's dependencies in speculation.gemspec
7
4
  gemspec
data/README.md CHANGED
@@ -2,34 +2,19 @@
2
2
 
3
3
  A Ruby port of Clojure's `clojure.spec`. See [clojure.spec - Rationale and Overview](https://clojure.org/about/spec). All advantages/disadvantages for clojure.spec should apply to Speculation too. This library is largely a copy-and-paste from clojure.spec so all credit goes to the clojure.spec authors.
4
4
 
5
- ## Installation
6
-
7
- Add this line to your application's Gemfile:
8
-
9
- ```ruby
10
- gem 'speculation'
11
- ```
12
-
13
- And then execute:
14
-
15
- $ bundle
16
-
17
- Or install it yourself as:
18
-
19
- $ gem install speculation
20
-
21
5
  ## Project Goals
22
6
 
23
- The goal of this project is to match clojure.spec as closely as possible, from design to features to API. This decision comes with the trade-off that the library may not necessarily be idiomatic Ruby, however there's nothing stopping other libraries from being built atop Speculation to bring a more Ruby-like feel. This library won't introduce features that do not exist in clojure.spec.
7
+ The goal of this project is to match clojure.spec as closely as possible, from design to features to API. This decision comes with the trade-off that the library may not necessarily be idiomatic Ruby, however there's nothing stopping other libraries from being built on top of Speculation to bring a more Ruby-like feel. This library won't introduce features that do not exist in clojure.spec.
24
8
 
25
9
  ## Examples
26
10
 
11
+ - [sinatra-web-app](examples/sinatra-web-app): A small sinatra web application demonstrating model validation and API error message generation.
27
12
  - [spec_guide.rb](examples/spec_guide.rb): Speculation port of Clojure's [spec guide](https://clojure.org/guides/spec)
28
13
  - [codebreaker.rb](examples/codebreaker.rb): Speculation port of the 'codebreaker' game described in [Interactive development with clojure.spec](http://blog.cognitect.com/blog/2016/10/5/interactive-development-with-clojurespec)
29
14
 
30
15
  ## Usage
31
16
 
32
- The API is more-or-less the same as `clojure.spec`. If you're already familiar with then you should be write at home with Speculation. Clojure and Ruby and quite different languages, so naturally there are some differences:
17
+ The API is more-or-less the same as `clojure.spec`. If you're already familiar clojure.spec with then you should feel at home with Speculation. Clojure and Ruby and quite different languages, so naturally there are some differences:
33
18
 
34
19
  ### Built in predicates
35
20
 
@@ -46,18 +31,20 @@ S.valid?(Set[:foo, :bar, :baz], :foo)
46
31
 
47
32
  ### Namespaced keywords/symbols
48
33
 
49
- Namespaced keywords are at the core of `clojure.spec`. Since spec utilises a global spec registry, namespaced keywords allow libraries to register specs with the same names but under different namespaces, thus removing accidental collisions. Ruby's equivalent to Clojure's keywords are Symbols. Ruby Symbol's don't have namespaces.
34
+ Namespaced keywords are at the core of `clojure.spec`. Since clojure.spec utilises a global spec registry, namespaced keywords allow libraries to register specs with the same names but under different namespaces, thus removing accidental collisions. Ruby's equivalent to Clojure's keywords are Symbols. Ruby Symbol's don't have namespaces.
50
35
 
51
- In order keep the global spec registry architecture in Speculation, we utilise a helper method `ns` achieve similar behaviour:
36
+ In order keep the global spec registry architecture in Speculation, we utilise a helper method `ns` to achieve similar behaviour:
52
37
 
53
38
  ```rb
54
- extend Speculation::NamespacedSymbols
39
+ module MyModule
40
+ extend Speculation::NamespacedSymbols
55
41
 
56
- p ns(:foo)
57
- # => :"MyModule/foo"
42
+ p ns(:foo)
43
+ # => :"MyModule/foo"
58
44
 
59
- p ns(AnotherModule, :foo)
60
- # => :"AnotherModule/foo"
45
+ p ns(AnotherModule, :foo)
46
+ # => :"AnotherModule/foo"
47
+ end
61
48
  ```
62
49
 
63
50
  ### FSpecs
@@ -90,9 +77,9 @@ S.fdef(method(:hello), :args => S.cat(:name => String),
90
77
 
91
78
  #### Generators and quick check
92
79
 
93
- Speculation uses [`Rantly`](https://github.com/abargnesi/rantly) for random data generation. Generator functions in Speculation are Procs that take one argument (Rantly instance) and return random value. While clojure's test.check generators generate values that start small and continue to grow and get more complex as a property holds true, Rantly always generates random values.
80
+ Speculation uses [`Rantly`](https://github.com/abargnesi/rantly) for random data generation. Generator functions in Speculation are Procs that take one argument (Rantly instance) and return a random value. While clojure's test.check generators generate values that start small and continue to grow and get more complex as a property holds true, Rantly always generates random values.
94
81
 
95
- Rantly gives Speculation the ability to shrink a failing test case down to a its smallest failing case, however in Speculation we limit this to Integers and Strings. This is an area where Speculation may currently be significantly weaker than clojure.spec.
82
+ Rantly gives Speculation the ability to shrink a failing test case down to its smallest failing case, however in Speculation we limit this to Integers and Strings. This is an area where Speculation may currently be significantly weaker than clojure.spec.
96
83
 
97
84
  ## Development
98
85
 
data/bin/console CHANGED
@@ -23,9 +23,9 @@ def reload!
23
23
  load "./lib/speculation/utils.rb"
24
24
  load "./lib/speculation/error.rb"
25
25
 
26
- load "./lib/speculation/spec_impl.rb"
26
+ load "./lib/speculation/spec.rb"
27
27
 
28
- Dir["lib/speculation/spec_impl/*.rb"].each do |f|
28
+ Dir["lib/speculation/spec/*.rb"].each do |f|
29
29
  load f
30
30
  end
31
31
 
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+ gem 'sinatra'
3
+ gem 'sequel'
4
+ gem 'bcrypt'
5
+ gem 'sqlite3'
6
+ gem 'speculation', :git => "https://github.com/english/speculation.git", :ref => "ae3ee09"
7
+ gem 'pry'
@@ -0,0 +1,46 @@
1
+ GIT
2
+ remote: https://github.com/english/speculation.git
3
+ revision: ae3ee095765816a8d5bfa5b29c808439f04633b0
4
+ ref: ae3ee09
5
+ specs:
6
+ speculation (0.2.0)
7
+ concurrent-ruby (~> 1.0)
8
+ rantly (~> 1.0)
9
+
10
+ GEM
11
+ remote: https://rubygems.org/
12
+ specs:
13
+ bcrypt (3.1.11)
14
+ coderay (1.1.1)
15
+ concurrent-ruby (1.0.5)
16
+ method_source (0.8.2)
17
+ pry (0.10.4)
18
+ coderay (~> 1.1.0)
19
+ method_source (~> 0.8.1)
20
+ slop (~> 3.4)
21
+ rack (1.6.5)
22
+ rack-protection (1.5.3)
23
+ rack
24
+ rantly (1.0.0)
25
+ sequel (4.44.0)
26
+ sinatra (1.4.8)
27
+ rack (~> 1.5)
28
+ rack-protection (~> 1.4)
29
+ tilt (>= 1.3, < 3)
30
+ slop (3.6.0)
31
+ sqlite3 (1.3.13)
32
+ tilt (2.0.6)
33
+
34
+ PLATFORMS
35
+ ruby
36
+
37
+ DEPENDENCIES
38
+ bcrypt
39
+ pry
40
+ sequel
41
+ sinatra
42
+ speculation!
43
+ sqlite3
44
+
45
+ BUNDLED WITH
46
+ 1.14.6
@@ -0,0 +1,153 @@
1
+ require 'bundler/setup'
2
+ require 'sinatra'
3
+ require 'sequel'
4
+ require 'bcrypt'
5
+ require 'sqlite3'
6
+ require 'speculation'
7
+ require 'speculation/gen'
8
+ require 'json'
9
+
10
+ S = Speculation
11
+ Gen = S::Gen
12
+ DB = Sequel.sqlite
13
+
14
+ DB.create_table :users do
15
+ primary_key :id
16
+ String :username
17
+ String :email
18
+ String :hashed_password
19
+ end
20
+
21
+ post '/users' do
22
+ user_attrs = symbolize_keys(params["user"])
23
+
24
+ if User.valid?(user_attrs)
25
+ User.create!(DB, user_attrs)
26
+ else
27
+ User.serialize_validation_errors(user_attrs).to_json
28
+ end
29
+ end
30
+
31
+ get '/users' do
32
+ users = User.all(DB)
33
+ users.to_json
34
+ end
35
+
36
+ get '/fake-user' do
37
+ User.fake
38
+ end
39
+
40
+ def symbolize_keys(hash)
41
+ hash.map { |k, v| [k.to_sym, v] }.to_h
42
+ end
43
+
44
+ module User
45
+ extend Speculation::NamespacedSymbols
46
+
47
+ def self.valid?(user)
48
+ S.valid?(ns(:user), user)
49
+ end
50
+
51
+ def self.create!(db, user)
52
+ hashed_password = BCrypt::Password.create(user[:password])
53
+
54
+ db[:users].insert(:email => user[:email],
55
+ :username => user[:username],
56
+ :hashed_password => hashed_password)
57
+
58
+ "success!"
59
+ end
60
+
61
+ def self.all(db)
62
+ db[:users].all
63
+ end
64
+
65
+ def self.serialize_validation_errors(user)
66
+ data = S.explain_data(ns(:user), user)
67
+ data[ns(S, :problems)].map { |problem| Validation.serialize_problem(problem) }
68
+ end
69
+
70
+ def self.fake
71
+ Gen.generate(S.gen(ns(User, :user))).to_json
72
+ end
73
+
74
+ module Generators
75
+ def self.email(rantly)
76
+ local_part = rantly.sized(rantly.range(1, 64)) { string(:alnum) }
77
+ subdomain = rantly.sized(rantly.range(1, 10)) { string(:alnum) }
78
+ tld = rantly.sized(3) { string(:alpha).downcase }
79
+
80
+ "#{local_part}@#{subdomain}.#{tld}"
81
+ end
82
+
83
+ def self.username(rantly)
84
+ rantly.sized(rantly.range(5, 20)) { rantly.string }
85
+ end
86
+
87
+ def self.password(rantly)
88
+ [
89
+ rantly.sized(rantly.range(2, 5)) { rantly.string(:upper) },
90
+ rantly.sized(rantly.range(2, 5)) { rantly.string(:lower) },
91
+ rantly.sized(rantly.range(2, 5)) { rantly.string(:digit) },
92
+ rantly.sized(rantly.range(2, 5)) { rantly.string(:punct) }
93
+ ].join.split("").shuffle.join
94
+ end
95
+ end
96
+
97
+ module Validation
98
+ def self.serialize_problem(problem)
99
+ path = problem[:path]
100
+ predicate, args = problem[:pred]
101
+
102
+ message = USER_ERROR_MESSAGE_MAP.fetch(path).fetch(predicate)
103
+ message = message.call(args) if message.respond_to?(:call)
104
+
105
+ { :error => message }
106
+ end
107
+
108
+ def self.validate_username_length(username)
109
+ username.length.between?(5, 20)
110
+ end
111
+
112
+ def self.validate_password_length(password)
113
+ password.length.between?(8, 50)
114
+ end
115
+
116
+ def self.validate_password_complexity(password)
117
+ [/[A-Z]/, /[a-z]/, /\d/, /\W/].all? { |re| password.match(re) }
118
+ end
119
+
120
+ EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$/
121
+
122
+ USER_ERROR_MESSAGE_MAP = {
123
+ [] => {
124
+ S::Utils.method(:key?) => ->(args) {
125
+ key = args.first
126
+ if key == S.or_keys(:email, :username)
127
+ "email or username is required"
128
+ else
129
+ "#{key} is required"
130
+ end
131
+ }
132
+ },
133
+ [:username] => {
134
+ String => "username must be a string",
135
+ method(:validate_username_length) => "username must be between 5 and 20 characters",
136
+ },
137
+ [:email] => {
138
+ String => "email must be a string",
139
+ EMAIL_REGEX => "email must be a valid email address"
140
+ },
141
+ [:password] => {
142
+ String => "password must be a string",
143
+ method(:validate_password_length) => "password must be between 8 and 50 characters",
144
+ method(:validate_password_complexity) => "password must contain at least one of each: upper case, lower case, numeric and special characters"
145
+ }
146
+ }
147
+ end
148
+
149
+ S.def ns(:email), S.with_gen(S.and(String, Validation::EMAIL_REGEX), Generators.method(:email))
150
+ S.def ns(:username), S.with_gen(S.and(String, Validation.method(:validate_username_length)), Generators.method(:username))
151
+ S.def ns(:password), S.with_gen(S.and(String, Validation.method(:validate_password_length), Validation.method(:validate_password_complexity)), Generators.method(:password))
152
+ S.def ns(:user), S.keys(:req_un => [S.or_keys(ns(:email), ns(:username)), ns(:password)])
153
+ end
@@ -0,0 +1,16 @@
1
+ set -o xtrace
2
+
3
+ curl -s -X POST -d 'user[email]=someone@example.com&user[psasword]=abc123!@ASD' http://localhost:4567/users | jq -c .
4
+ # => [{"error":"password is required"}]
5
+
6
+ curl -s -X POST -d 'user[email]=someone@example.com&user[password]=abc123' http://localhost:4567/users | jq -c .
7
+ # => [{"error":"password must be between 8 and 50 characters"}]
8
+
9
+ curl -s -X POST -d 'user[username]=anonymous&user[password]=test123456' http://localhost:4567/users | jq -c .
10
+ # => [{"error":"password must contain at least one of each: upper case, lower case, numeric and special characters"}]
11
+
12
+ curl -s -X POST -d 'user[username]=anonymous&user[password]=abc123!_ASD' http://localhost:4567/users
13
+ # => success!
14
+
15
+ curl -s http://localhost:4567/users | jq -c .
16
+ # => [{"id":1,"username":"anonymous","email":null,"hashed_password":"$2a$10$TMLSJNNj4K4YtpMmx.4hTOxh0lg0WsIQbFkY6v8ssuMBtMoJ2oCG6"}]
data/lib/speculation.rb CHANGED
@@ -7,7 +7,7 @@ require "speculation/version"
7
7
  require "speculation/namespaced_symbols"
8
8
  require "speculation/identifier"
9
9
  require "speculation/utils"
10
- require "speculation/spec_impl"
10
+ require "speculation/spec"
11
11
  require "speculation/error"
12
12
 
13
13
  module Speculation
@@ -42,6 +42,8 @@ module Speculation
42
42
 
43
43
  @registry_ref = Concurrent::Atom.new({})
44
44
 
45
+ INVALID = ns(:invalid)
46
+
45
47
  # Can be enabled or disabled at runtime:
46
48
  # - enabled/disabled by setting `check_asserts`.
47
49
  # - enabled by setting environment variable SPECULATION_CHECK_ASSERTS to the
@@ -108,7 +110,7 @@ module Speculation
108
110
  # @param x [Spec, Object]
109
111
  # @return [Spec, false] x if x is a spec, else false
110
112
  def self.spec?(x)
111
- x if x.is_a?(SpecImpl)
113
+ x if x.is_a?(Spec)
112
114
  end
113
115
 
114
116
  # @param x [Hash, Object]
@@ -120,7 +122,7 @@ module Speculation
120
122
  # @param value return value of a `conform` call
121
123
  # @return [Boolean] true if value is the result of an unsuccessful conform
122
124
  def self.invalid?(value)
123
- value.equal?(ns(:invalid))
125
+ value.equal?(INVALID)
124
126
  end
125
127
 
126
128
  # @param spec [Spec]
@@ -661,11 +663,11 @@ module Speculation
661
663
  if spec
662
664
  conform(spec, x)
663
665
  elsif pred.is_a?(Module) || pred.is_a?(::Regexp)
664
- pred === x ? x : ns(:invalid)
666
+ pred === x ? x : INVALID
665
667
  elsif pred.is_a?(Set)
666
- pred.include?(x) ? x : ns(:invalid)
668
+ pred.include?(x) ? x : INVALID
667
669
  elsif pred.respond_to?(:call)
668
- pred.call(x) ? x : ns(:invalid)
670
+ pred.call(x) ? x : INVALID
669
671
  else
670
672
  raise "#{pred} is not a class, proc, set or regexp"
671
673
  end
@@ -700,7 +702,7 @@ module Speculation
700
702
  elsif Utils.ident?(pred)
701
703
  the_spec(pred)
702
704
  else
703
- Spec.new(pred, should_conform)
705
+ PredicateSpec.new(pred, should_conform)
704
706
  end
705
707
  end
706
708
 
@@ -810,7 +812,7 @@ module Speculation
810
812
  x, *xs = data
811
813
 
812
814
  if data.empty?
813
- return ns(:invalid) unless accept_nil?(regex)
815
+ return INVALID unless accept_nil?(regex)
814
816
 
815
817
  return_value = preturn(regex)
816
818
 
@@ -825,7 +827,7 @@ module Speculation
825
827
  if dp
826
828
  re_conform(dp, xs)
827
829
  else
828
- ns(:invalid)
830
+ INVALID
829
831
  end
830
832
  end
831
833
  end
@@ -953,7 +955,7 @@ module Speculation
953
955
  x = dt(pred, x)
954
956
 
955
957
  if invalid?(x)
956
- ns(:invalid)
958
+ INVALID
957
959
  elsif preds.empty?
958
960
  x
959
961
  else
@@ -1286,22 +1288,15 @@ module Speculation
1286
1288
  raise "Unexpected #{ns(:op)} #{p[ns(:op)]}"
1287
1289
  end
1288
1290
  end
1289
-
1290
- # Resets the spec registry to only builtin specs
1291
- def reset_registry!
1292
- builtins = {
1293
- ns(:any) => with_gen(Utils.constantly(true), ->(r) { r.branch(*Gen::GEN_BUILTINS.values) }),
1294
- ns(:boolean) => Set[true, false],
1295
- ns(:positive_integer) => with_gen(self.and(Integer, ->(x) { x > 0 }), ->(r) { r.range(1) }),
1296
- # Rantly#positive_integer is actually a natural integer
1297
- ns(:natural_integer) => with_gen(self.and(Integer, ->(x) { x >= 0 }), :positive_integer.to_proc),
1298
- ns(:negative_integer) => with_gen(self.and(Integer, ->(x) { x < 0 }), ->(r) { r.range(nil, -1) }),
1299
- ns(:empty) => with_gen(Utils.method(:empty?), Utils.constantly([]))
1300
- }.freeze
1301
-
1302
- @registry_ref.reset(builtins)
1303
- end
1304
1291
  end
1305
1292
 
1306
- reset_registry!
1293
+ @registry_ref.reset(
1294
+ ns(:any) => with_gen(Utils.constantly(true), ->(r) { r.branch(*Gen::GEN_BUILTINS.values) }),
1295
+ ns(:boolean) => Set[true, false],
1296
+ ns(:positive_integer) => with_gen(self.and(Integer, ->(x) { x > 0 }), ->(r) { r.range(1) }),
1297
+ # Rantly#positive_integer is actually a natural integer
1298
+ ns(:natural_integer) => with_gen(self.and(Integer, ->(x) { x >= 0 }), :positive_integer.to_proc),
1299
+ ns(:negative_integer) => with_gen(self.and(Integer, ->(x) { x < 0 }), ->(r) { r.range(nil, -1) }),
1300
+ ns(:empty) => with_gen(Utils.method(:empty?), Utils.constantly([]))
1301
+ )
1307
1302
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Speculation
4
4
  # @private
5
- class SpecImpl
5
+ class Spec
6
6
  attr_accessor :name, :gen
7
7
  attr_reader :id
8
8
 
@@ -24,13 +24,13 @@ module Speculation
24
24
  end
25
25
  end
26
26
 
27
- require_relative "spec_impl/hash_spec"
28
- require_relative "spec_impl/spec"
29
- require_relative "spec_impl/tuple_spec"
30
- require_relative "spec_impl/or_spec"
31
- require_relative "spec_impl/and_spec"
32
- require_relative "spec_impl/merge_spec"
33
- require_relative "spec_impl/every_spec"
34
- require_relative "spec_impl/regex_spec"
35
- require_relative "spec_impl/f_spec"
36
- require_relative "spec_impl/nilable_spec"
27
+ require_relative "spec/hash_spec"
28
+ require_relative "spec/predicate_spec"
29
+ require_relative "spec/tuple_spec"
30
+ require_relative "spec/or_spec"
31
+ require_relative "spec/and_spec"
32
+ require_relative "spec/merge_spec"
33
+ require_relative "spec/every_spec"
34
+ require_relative "spec/regex_spec"
35
+ require_relative "spec/f_spec"
36
+ require_relative "spec/nilable_spec"
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Speculation
4
4
  # @private
5
- class AndSpec < SpecImpl
5
+ class AndSpec < Spec
6
6
  include NamespacedSymbols
7
7
  S = Speculation
8
8
 
@@ -17,7 +17,7 @@ module Speculation
17
17
  @specs.value!.each do |spec|
18
18
  value = spec.conform(value)
19
19
 
20
- return ns(S, :invalid) if S.invalid?(value)
20
+ return S::INVALID if S.invalid?(value)
21
21
  end
22
22
 
23
23
  value
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  module Speculation
3
3
  # @private
4
- class EverySpec < SpecImpl
4
+ class EverySpec < Spec
5
5
  include NamespacedSymbols
6
6
  S = Speculation
7
7
 
@@ -55,7 +55,7 @@ module Speculation
55
55
  end
56
56
 
57
57
  def conform(value)
58
- return ns(S, :invalid) unless @collection_predicate.call(value)
58
+ return S::INVALID unless @collection_predicate.call(value)
59
59
 
60
60
  spec = @delayed_spec.value!
61
61
 
@@ -68,7 +68,7 @@ module Speculation
68
68
  conformed_value = spec.conform(val)
69
69
 
70
70
  if S.invalid?(conformed_value)
71
- return ns(S, :invalid)
71
+ return S::INVALID
72
72
  else
73
73
  return_value = add.call(return_value, index, val, conformed_value)
74
74
  end
@@ -81,7 +81,7 @@ module Speculation
81
81
 
82
82
  value.each_with_index do |item, index|
83
83
  return value if index == limit
84
- return ns(S, :invalid) unless S.valid?(spec, item)
84
+ return S::INVALID unless S.valid?(spec, item)
85
85
  end
86
86
 
87
87
  value
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Speculation
4
4
  # @private
5
- class FSpec < SpecImpl
5
+ class FSpec < Spec
6
6
  include NamespacedSymbols
7
7
  S = Speculation
8
8
 
@@ -18,14 +18,14 @@ module Speculation
18
18
  def conform(f)
19
19
  raise "Can't conform fspec without args spec: #{inspect}" unless @args
20
20
 
21
- return ns(S, :invalid) unless f.is_a?(Proc) || f.is_a?(Method)
21
+ return S::INVALID unless f.is_a?(Proc) || f.is_a?(Method)
22
22
 
23
23
  specs = { :args => @args, :ret => @ret, :fn => @fn, :block => @block }
24
24
 
25
25
  if f.equal?(FSpec.validate_fn(f, specs, S.fspec_iterations))
26
26
  f
27
27
  else
28
- ns(S, :invalid)
28
+ S::INVALID
29
29
  end
30
30
  end
31
31
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  module Speculation
3
3
  # @private
4
- class HashSpec < SpecImpl
4
+ class HashSpec < Spec
5
5
  include NamespacedSymbols
6
6
  S = Speculation
7
7
 
@@ -34,11 +34,11 @@ module Speculation
34
34
  @opt_keys = opt + opt_un.map(&method(:unqualify_key))
35
35
  @opt_specs = opt + opt_un
36
36
  @keys_pred = ->(v) { pred_exprs.all? { |p| p.call(v) } }
37
- @key_to_spec_map = Hash[req_keys.concat(@opt_keys).zip(req_specs.concat(@opt_specs))]
37
+ @key_to_spec_map = Hash[Utils.conj(req_keys, @opt_keys).zip(Utils.conj(req_specs, @opt_specs))]
38
38
  end
39
39
 
40
40
  def conform(value)
41
- return ns(S, :invalid) unless @keys_pred.call(value)
41
+ return S::INVALID unless @keys_pred.call(value)
42
42
 
43
43
  reg = S.registry
44
44
  ret = value
@@ -52,7 +52,7 @@ module Speculation
52
52
  conformed_value = S.conform(spec, v)
53
53
 
54
54
  if S.invalid?(conformed_value)
55
- return ns(S, :invalid)
55
+ return S::INVALID
56
56
  else
57
57
  unless conformed_value.equal?(v)
58
58
  ret = ret.merge(key => conformed_value)
@@ -74,8 +74,7 @@ module Speculation
74
74
  failures = parse_req(@req, value, Utils.method(:itself))
75
75
 
76
76
  failures.each do |failure_sexp|
77
- # eww
78
- pred = [Utils.method(:key?), [sexp_to_rb(failure_sexp)]]
77
+ pred = [Utils.method(:key?), [failure_sexp]]
79
78
  problems << { :path => path, :pred => pred, :val => value, :via => via, :in => inn }
80
79
  end
81
80
  end
@@ -84,7 +83,7 @@ module Speculation
84
83
  failures = parse_req(@req_un, value, method(:unqualify_key))
85
84
 
86
85
  failures.each do |failure_sexp|
87
- pred = [Utils.method(:key?), [sexp_to_rb(failure_sexp)]]
86
+ pred = [Utils.method(:key?), [failure_sexp]]
88
87
  problems << { :path => path, :pred => pred, :val => value, :via => via, :in => inn }
89
88
  end
90
89
  end
@@ -135,29 +134,6 @@ module Speculation
135
134
 
136
135
  private
137
136
 
138
- def sexp_to_rb(sexp, level = 0)
139
- if sexp.is_a?(Array)
140
- op, *keys = sexp
141
- rb_string = String.new
142
-
143
- rb_string << "(" unless level.zero?
144
-
145
- keys.each_with_index do |key, i|
146
- unless i.zero?
147
- rb_string << " #{NamespacedSymbols.name(op)} "
148
- end
149
-
150
- rb_string << sexp_to_rb(key, level + 1).to_s
151
- end
152
-
153
- rb_string << ")" unless level.zero?
154
-
155
- rb_string
156
- else
157
- sexp
158
- end
159
- end
160
-
161
137
  def extract_keys(symbol_or_arr)
162
138
  if symbol_or_arr.is_a?(Array)
163
139
  symbol_or_arr[1..-1].flat_map(&method(:extract_keys))
@@ -177,16 +153,16 @@ module Speculation
177
153
  op, *kks = key
178
154
  case op
179
155
  when ns(S, :or)
180
- if kks.one? { |k| parse_req([k], v, f).empty? }
156
+ if kks.any? { |k| parse_req([k], v, f).empty? }
181
157
  []
182
158
  else
183
- [key]
159
+ transform_keys([key], f)
184
160
  end
185
161
  when ns(S, :and)
186
162
  if kks.all? { |k| parse_req([k], v, f).empty? }
187
163
  []
188
164
  else
189
- [key]
165
+ transform_keys([key], f)
190
166
  end
191
167
  else
192
168
  raise "Expected or, and, got #{op}"
@@ -194,7 +170,7 @@ module Speculation
194
170
  elsif v.key?(f.call(key))
195
171
  []
196
172
  else
197
- [key]
173
+ [f.call(key)]
198
174
  end
199
175
 
200
176
  if ks.any?
@@ -203,5 +179,15 @@ module Speculation
203
179
  ret
204
180
  end
205
181
  end
182
+
183
+ def transform_keys(keys, f)
184
+ keys.map { |key|
185
+ case key
186
+ when Array then transform_keys(key, f)
187
+ when ns(S, :and), ns(S, :or) then key
188
+ else f.call(key)
189
+ end
190
+ }
191
+ end
206
192
  end
207
193
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  module Speculation
3
3
  # @private
4
- class MergeSpec < SpecImpl
4
+ class MergeSpec < Spec
5
5
  include NamespacedSymbols
6
6
  S = Speculation
7
7
 
@@ -13,7 +13,7 @@ module Speculation
13
13
  ms = @preds.map { |pred| S.dt(pred, x) }
14
14
 
15
15
  if ms.any?(&S.method(:invalid?))
16
- ns(S, :invalid)
16
+ S::INVALID
17
17
  else
18
18
  ms.reduce(&:merge)
19
19
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  module Speculation
3
3
  # @private
4
- class NilableSpec < SpecImpl
4
+ class NilableSpec < Spec
5
5
  include NamespacedSymbols
6
6
  S = Speculation
7
7
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  module Speculation
3
3
  # @private
4
- class OrSpec < SpecImpl
4
+ class OrSpec < Spec
5
5
  include NamespacedSymbols
6
6
  S = Speculation
7
7
 
@@ -27,7 +27,7 @@ module Speculation
27
27
  end
28
28
  end
29
29
 
30
- ns(S, :invalid)
30
+ S::INVALID
31
31
  end
32
32
 
33
33
  def explain(path, via, inn, value)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  module Speculation
3
3
  # @private
4
- class Spec < SpecImpl
4
+ class PredicateSpec < Spec
5
5
  include NamespacedSymbols
6
6
  S = Speculation
7
7
 
@@ -20,7 +20,7 @@ module Speculation
20
20
  if @should_conform
21
21
  ret
22
22
  else
23
- ret ? value : ns(S, :invalid)
23
+ ret ? value : S::INVALID
24
24
  end
25
25
  end
26
26
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  module Speculation
3
3
  # @private
4
- class RegexSpec < SpecImpl
4
+ class RegexSpec < Spec
5
5
  include NamespacedSymbols
6
6
  S = Speculation
7
7
 
@@ -13,7 +13,7 @@ module Speculation
13
13
  if value.nil? || Utils.collection?(value)
14
14
  S.re_conform(@regex, value)
15
15
  else
16
- ns(S, :invalid)
16
+ S::INVALID
17
17
  end
18
18
  end
19
19
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  module Speculation
3
3
  # @private
4
- class TupleSpec < SpecImpl
4
+ class TupleSpec < Spec
5
5
  include NamespacedSymbols
6
6
  S = Speculation
7
7
 
@@ -17,7 +17,7 @@ module Speculation
17
17
  specs = @delayed_specs.value!
18
18
 
19
19
  unless Utils.array?(collection) && collection.count == specs.count
20
- return ns(S, :invalid)
20
+ return S::INVALID
21
21
  end
22
22
 
23
23
  return_value = collection.class.new
@@ -26,7 +26,7 @@ module Speculation
26
26
  conformed_value = spec.conform(value)
27
27
 
28
28
  if S.invalid?(conformed_value)
29
- return ns(S, :invalid)
29
+ return S::INVALID
30
30
  else
31
31
  return_value += [conformed_value]
32
32
  end
@@ -218,7 +218,7 @@ module Speculation
218
218
  conformed_args = S.conform(fspec.args, args)
219
219
  conformed_block = S.conform(fspec.block, block) if fspec.block
220
220
 
221
- if conformed_args == ns(S, :invalid)
221
+ if conformed_args == S::INVALID
222
222
  backtrace = backtrace_relevant_to_instrument(caller)
223
223
 
224
224
  ed = S.
@@ -230,7 +230,7 @@ module Speculation
230
230
  msg = io.string
231
231
 
232
232
  raise Speculation::Error.new("Call to '#{ident}' did not conform to spec:\n #{msg}", ed)
233
- elsif conformed_block == ns(S, :invalid)
233
+ elsif conformed_block == S::INVALID
234
234
  backtrace = backtrace_relevant_to_instrument(caller)
235
235
 
236
236
  ed = S.
@@ -354,13 +354,13 @@ module Speculation
354
354
  def check_call(method, spec, args, block)
355
355
  conformed_args = S.conform(spec.args, args) if spec.args
356
356
 
357
- if conformed_args == ns(S, :invalid)
357
+ if conformed_args == S::INVALID
358
358
  return explain_check(args, spec.args, args, :args)
359
359
  end
360
360
 
361
361
  conformed_block = S.conform(spec.block, block) if spec.block
362
362
 
363
- if conformed_block == ns(S, :invalid)
363
+ if conformed_block == S::INVALID
364
364
  return explain_check(block, spec.block, block, :block)
365
365
  end
366
366
 
@@ -368,7 +368,7 @@ module Speculation
368
368
 
369
369
  conformed_ret = S.conform(spec.ret, ret) if spec.ret
370
370
 
371
- if conformed_ret == ns(S, :invalid)
371
+ if conformed_ret == S::INVALID
372
372
  return explain_check(args, spec.ret, ret, :ret)
373
373
  end
374
374
 
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Speculation
3
- VERSION = "0.2.0"
3
+ VERSION = "0.3.0"
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: speculation
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jamie English
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-03-05 00:00:00.000000000 Z
11
+ date: 2017-03-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -165,24 +165,13 @@ files:
165
165
  - LICENSE.txt
166
166
  - README.md
167
167
  - Rakefile
168
- - bin/bundler
169
- - bin/byebug
170
- - bin/coderay
171
168
  - bin/console
172
- - bin/cucumber-queue
173
- - bin/minitest-queue
174
- - bin/pry
175
- - bin/rake
176
- - bin/rspec-queue
177
- - bin/rubocop
178
- - bin/ruby-parse
179
- - bin/ruby-rewrite
180
169
  - bin/setup
181
- - bin/testunit-queue
182
- - bin/yard
183
- - bin/yardoc
184
- - bin/yri
185
170
  - examples/codebreaker.rb
171
+ - examples/sinatra-web-app/Gemfile
172
+ - examples/sinatra-web-app/Gemfile.lock
173
+ - examples/sinatra-web-app/app.rb
174
+ - examples/sinatra-web-app/test
186
175
  - examples/spec_guide.rb
187
176
  - lib/speculation.rb
188
177
  - lib/speculation/error.rb
@@ -190,17 +179,17 @@ files:
190
179
  - lib/speculation/identifier.rb
191
180
  - lib/speculation/namespaced_symbols.rb
192
181
  - lib/speculation/pmap.rb
193
- - lib/speculation/spec_impl.rb
194
- - lib/speculation/spec_impl/and_spec.rb
195
- - lib/speculation/spec_impl/every_spec.rb
196
- - lib/speculation/spec_impl/f_spec.rb
197
- - lib/speculation/spec_impl/hash_spec.rb
198
- - lib/speculation/spec_impl/merge_spec.rb
199
- - lib/speculation/spec_impl/nilable_spec.rb
200
- - lib/speculation/spec_impl/or_spec.rb
201
- - lib/speculation/spec_impl/regex_spec.rb
202
- - lib/speculation/spec_impl/spec.rb
203
- - lib/speculation/spec_impl/tuple_spec.rb
182
+ - lib/speculation/spec.rb
183
+ - lib/speculation/spec/and_spec.rb
184
+ - lib/speculation/spec/every_spec.rb
185
+ - lib/speculation/spec/f_spec.rb
186
+ - lib/speculation/spec/hash_spec.rb
187
+ - lib/speculation/spec/merge_spec.rb
188
+ - lib/speculation/spec/nilable_spec.rb
189
+ - lib/speculation/spec/or_spec.rb
190
+ - lib/speculation/spec/predicate_spec.rb
191
+ - lib/speculation/spec/regex_spec.rb
192
+ - lib/speculation/spec/tuple_spec.rb
204
193
  - lib/speculation/test.rb
205
194
  - lib/speculation/utils.rb
206
195
  - lib/speculation/version.rb
data/bin/bundler DELETED
@@ -1,17 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
- #
4
- # This file was generated by Bundler.
5
- #
6
- # The application 'bundler' is installed as part of a gem, and
7
- # this file is here to facilitate running it.
8
- #
9
-
10
- require "pathname"
11
- ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
12
- Pathname.new(__FILE__).realpath)
13
-
14
- require "rubygems"
15
- require "bundler/setup"
16
-
17
- load Gem.bin_path("bundler", "bundler")
data/bin/byebug DELETED
@@ -1,17 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
- #
4
- # This file was generated by Bundler.
5
- #
6
- # The application 'byebug' is installed as part of a gem, and
7
- # this file is here to facilitate running it.
8
- #
9
-
10
- require "pathname"
11
- ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
12
- Pathname.new(__FILE__).realpath)
13
-
14
- require "rubygems"
15
- require "bundler/setup"
16
-
17
- load Gem.bin_path("byebug", "byebug")
data/bin/coderay DELETED
@@ -1,17 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
- #
4
- # This file was generated by Bundler.
5
- #
6
- # The application 'coderay' is installed as part of a gem, and
7
- # this file is here to facilitate running it.
8
- #
9
-
10
- require "pathname"
11
- ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
12
- Pathname.new(__FILE__).realpath)
13
-
14
- require "rubygems"
15
- require "bundler/setup"
16
-
17
- load Gem.bin_path("coderay", "coderay")
data/bin/cucumber-queue DELETED
@@ -1,17 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
- #
4
- # This file was generated by Bundler.
5
- #
6
- # The application 'cucumber-queue' is installed as part of a gem, and
7
- # this file is here to facilitate running it.
8
- #
9
-
10
- require "pathname"
11
- ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
12
- Pathname.new(__FILE__).realpath)
13
-
14
- require "rubygems"
15
- require "bundler/setup"
16
-
17
- load Gem.bin_path("test-queue", "cucumber-queue")
data/bin/minitest-queue DELETED
@@ -1,17 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
- #
4
- # This file was generated by Bundler.
5
- #
6
- # The application 'minitest-queue' is installed as part of a gem, and
7
- # this file is here to facilitate running it.
8
- #
9
-
10
- require "pathname"
11
- ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
12
- Pathname.new(__FILE__).realpath)
13
-
14
- require "rubygems"
15
- require "bundler/setup"
16
-
17
- load Gem.bin_path("test-queue", "minitest-queue")
data/bin/pry DELETED
@@ -1,17 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
- #
4
- # This file was generated by Bundler.
5
- #
6
- # The application 'pry' is installed as part of a gem, and
7
- # this file is here to facilitate running it.
8
- #
9
-
10
- require "pathname"
11
- ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
12
- Pathname.new(__FILE__).realpath)
13
-
14
- require "rubygems"
15
- require "bundler/setup"
16
-
17
- load Gem.bin_path("pry", "pry")
data/bin/rake DELETED
@@ -1,17 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
- #
4
- # This file was generated by Bundler.
5
- #
6
- # The application 'rake' is installed as part of a gem, and
7
- # this file is here to facilitate running it.
8
- #
9
-
10
- require "pathname"
11
- ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
12
- Pathname.new(__FILE__).realpath)
13
-
14
- require "rubygems"
15
- require "bundler/setup"
16
-
17
- load Gem.bin_path("rake", "rake")
data/bin/rspec-queue DELETED
@@ -1,17 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
- #
4
- # This file was generated by Bundler.
5
- #
6
- # The application 'rspec-queue' is installed as part of a gem, and
7
- # this file is here to facilitate running it.
8
- #
9
-
10
- require "pathname"
11
- ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
12
- Pathname.new(__FILE__).realpath)
13
-
14
- require "rubygems"
15
- require "bundler/setup"
16
-
17
- load Gem.bin_path("test-queue", "rspec-queue")
data/bin/rubocop DELETED
@@ -1,17 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
- #
4
- # This file was generated by Bundler.
5
- #
6
- # The application 'rubocop' is installed as part of a gem, and
7
- # this file is here to facilitate running it.
8
- #
9
-
10
- require "pathname"
11
- ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
12
- Pathname.new(__FILE__).realpath)
13
-
14
- require "rubygems"
15
- require "bundler/setup"
16
-
17
- load Gem.bin_path("rubocop", "rubocop")
data/bin/ruby-parse DELETED
@@ -1,17 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
- #
4
- # This file was generated by Bundler.
5
- #
6
- # The application 'ruby-parse' is installed as part of a gem, and
7
- # this file is here to facilitate running it.
8
- #
9
-
10
- require "pathname"
11
- ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
12
- Pathname.new(__FILE__).realpath)
13
-
14
- require "rubygems"
15
- require "bundler/setup"
16
-
17
- load Gem.bin_path("parser", "ruby-parse")
data/bin/ruby-rewrite DELETED
@@ -1,17 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
- #
4
- # This file was generated by Bundler.
5
- #
6
- # The application 'ruby-rewrite' is installed as part of a gem, and
7
- # this file is here to facilitate running it.
8
- #
9
-
10
- require "pathname"
11
- ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
12
- Pathname.new(__FILE__).realpath)
13
-
14
- require "rubygems"
15
- require "bundler/setup"
16
-
17
- load Gem.bin_path("parser", "ruby-rewrite")
data/bin/testunit-queue DELETED
@@ -1,17 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
- #
4
- # This file was generated by Bundler.
5
- #
6
- # The application 'testunit-queue' is installed as part of a gem, and
7
- # this file is here to facilitate running it.
8
- #
9
-
10
- require "pathname"
11
- ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
12
- Pathname.new(__FILE__).realpath)
13
-
14
- require "rubygems"
15
- require "bundler/setup"
16
-
17
- load Gem.bin_path("test-queue", "testunit-queue")
data/bin/yard DELETED
@@ -1,17 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
- #
4
- # This file was generated by Bundler.
5
- #
6
- # The application 'yard' is installed as part of a gem, and
7
- # this file is here to facilitate running it.
8
- #
9
-
10
- require "pathname"
11
- ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
12
- Pathname.new(__FILE__).realpath)
13
-
14
- require "rubygems"
15
- require "bundler/setup"
16
-
17
- load Gem.bin_path("yard", "yard")
data/bin/yardoc DELETED
@@ -1,17 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
- #
4
- # This file was generated by Bundler.
5
- #
6
- # The application 'yardoc' is installed as part of a gem, and
7
- # this file is here to facilitate running it.
8
- #
9
-
10
- require "pathname"
11
- ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
12
- Pathname.new(__FILE__).realpath)
13
-
14
- require "rubygems"
15
- require "bundler/setup"
16
-
17
- load Gem.bin_path("yard", "yardoc")
data/bin/yri DELETED
@@ -1,17 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
- #
4
- # This file was generated by Bundler.
5
- #
6
- # The application 'yri' is installed as part of a gem, and
7
- # this file is here to facilitate running it.
8
- #
9
-
10
- require "pathname"
11
- ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
12
- Pathname.new(__FILE__).realpath)
13
-
14
- require "rubygems"
15
- require "bundler/setup"
16
-
17
- load Gem.bin_path("yard", "yri")