json-spec 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (114) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/.gitmodules +3 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +960 -0
  7. data/Rakefile +10 -0
  8. data/api_documentation.md +611 -0
  9. data/cachivache/.gitignore +1 -0
  10. data/cachivache/Gemfile +4 -0
  11. data/cachivache/README.md +247 -0
  12. data/cachivache/Rakefile +19 -0
  13. data/cachivache/Vagrantfile +70 -0
  14. data/cachivache/cachivache.rb +59 -0
  15. data/cachivache/lib/let-behaviour.rb +27 -0
  16. data/cachivache/lib/rake-helper.rb +131 -0
  17. data/cachivache/lib/sh-file-context.rb +39 -0
  18. data/cachivache/lib/sh-if-context.rb +31 -0
  19. data/cachivache/stuff/.gitkeep +0 -0
  20. data/cachivache/stuff/ruby-json-spec.rb +22 -0
  21. data/examples/example-1-simple.rb +66 -0
  22. data/examples/example-2-default-expectations.rb +63 -0
  23. data/examples/example-3-each-field.rb +104 -0
  24. data/examples/example-4-to-be-as-defined-in.rb +117 -0
  25. data/examples/example-5-custom-expectations.rb +153 -0
  26. data/examples/example-6-custom-messages.rb +36 -0
  27. data/examples/example-7-full-example.rb +231 -0
  28. data/examples/fixtures.rb +77 -0
  29. data/examples/validation-printer.rb +47 -0
  30. data/json-spec.gemspec +29 -0
  31. data/lib/cabeza-de-termo/json-spec/errors/error.rb +6 -0
  32. data/lib/cabeza-de-termo/json-spec/errors/expectation-not-found-error.rb +8 -0
  33. data/lib/cabeza-de-termo/json-spec/errors/modifier-not-found-error.rb +8 -0
  34. data/lib/cabeza-de-termo/json-spec/errors/unkown-json-type-error.rb +8 -0
  35. data/lib/cabeza-de-termo/json-spec/errors/validation-error.rb +8 -0
  36. data/lib/cabeza-de-termo/json-spec/expectations-library/default-expectations/default-expectation-builder.rb +29 -0
  37. data/lib/cabeza-de-termo/json-spec/expectations-library/default-expectations/default-expectations-mapping.rb +54 -0
  38. data/lib/cabeza-de-termo/json-spec/expectations-library/definition-builders/expectation-builders/block-expectation-definition.rb +16 -0
  39. data/lib/cabeza-de-termo/json-spec/expectations-library/definition-builders/expectation-builders/class-expectation-definition.rb +15 -0
  40. data/lib/cabeza-de-termo/json-spec/expectations-library/definition-builders/expectation-builders/expectation-definition.rb +19 -0
  41. data/lib/cabeza-de-termo/json-spec/expectations-library/definition-builders/expectation-builders/expectations-definition-builder.rb +136 -0
  42. data/lib/cabeza-de-termo/json-spec/expectations-library/definition-builders/expectation-builders/expecting-all-of-expectation-definition.rb +23 -0
  43. data/lib/cabeza-de-termo/json-spec/expectations-library/definition-builders/expectation-builders/expecting-any-of-expectation-definition.rb +23 -0
  44. data/lib/cabeza-de-termo/json-spec/expectations-library/definition-builders/expectation-builders/expecting-expectation-definition.rb +17 -0
  45. data/lib/cabeza-de-termo/json-spec/expectations-library/definition-builders/expectation-builders/negating-expectation-definition.rb +16 -0
  46. data/lib/cabeza-de-termo/json-spec/expectations-library/definition-builders/expectation-library-definition-builder.rb +35 -0
  47. data/lib/cabeza-de-termo/json-spec/expectations-library/definition-builders/modifier-builders/class-modifier-definition.rb +15 -0
  48. data/lib/cabeza-de-termo/json-spec/expectations-library/definition-builders/modifier-builders/composing-modifiers-definition.rb +28 -0
  49. data/lib/cabeza-de-termo/json-spec/expectations-library/definition-builders/modifier-builders/modifier-definition.rb +13 -0
  50. data/lib/cabeza-de-termo/json-spec/expectations-library/definition-builders/modifier-builders/modifiers-definition-builder.rb +73 -0
  51. data/lib/cabeza-de-termo/json-spec/expectations-library/expectations-library.rb +150 -0
  52. data/lib/cabeza-de-termo/json-spec/expectations-library/initializers/default-library-initializer.rb +265 -0
  53. data/lib/cabeza-de-termo/json-spec/expectations-library/initializers/library-initializer.rb +9 -0
  54. data/lib/cabeza-de-termo/json-spec/expectations-library/messages/expectation-messages-mapping.rb +27 -0
  55. data/lib/cabeza-de-termo/json-spec/expectations/abstract-expectation.rb +39 -0
  56. data/lib/cabeza-de-termo/json-spec/expectations/all-expectations-composite.rb +33 -0
  57. data/lib/cabeza-de-termo/json-spec/expectations/any-expectation-composite.rb +33 -0
  58. data/lib/cabeza-de-termo/json-spec/expectations/block-expectation.rb +28 -0
  59. data/lib/cabeza-de-termo/json-spec/expectations/expectation.rb +51 -0
  60. data/lib/cabeza-de-termo/json-spec/expectations/is-email-expectation.rb +16 -0
  61. data/lib/cabeza-de-termo/json-spec/expectations/is-scalar-expectation.rb +17 -0
  62. data/lib/cabeza-de-termo/json-spec/expectations/is-url-expectation.rb +21 -0
  63. data/lib/cabeza-de-termo/json-spec/expectations/negated-expectation.rb +31 -0
  64. data/lib/cabeza-de-termo/json-spec/expectations/runner/abstract-expectations-runner.rb +27 -0
  65. data/lib/cabeza-de-termo/json-spec/expectations/runner/can-be-absent-expectations-runner.rb +50 -0
  66. data/lib/cabeza-de-termo/json-spec/expectations/runner/can-be-null-expectations-runner.rb +48 -0
  67. data/lib/cabeza-de-termo/json-spec/expectations/runner/expectations-runner.rb +43 -0
  68. data/lib/cabeza-de-termo/json-spec/expressions/json-any-of.rb +62 -0
  69. data/lib/cabeza-de-termo/json-spec/expressions/json-anything.rb +16 -0
  70. data/lib/cabeza-de-termo/json-spec/expressions/json-each-field.rb +58 -0
  71. data/lib/cabeza-de-termo/json-spec/expressions/json-each.rb +58 -0
  72. data/lib/cabeza-de-termo/json-spec/expressions/json-expression.rb +314 -0
  73. data/lib/cabeza-de-termo/json-spec/expressions/json-field-name.rb +16 -0
  74. data/lib/cabeza-de-termo/json-spec/expressions/json-field.rb +76 -0
  75. data/lib/cabeza-de-termo/json-spec/expressions/json-list.rb +40 -0
  76. data/lib/cabeza-de-termo/json-spec/expressions/json-object.rb +82 -0
  77. data/lib/cabeza-de-termo/json-spec/expressions/json-scalar.rb +20 -0
  78. data/lib/cabeza-de-termo/json-spec/expressions/json-spec.rb +174 -0
  79. data/lib/cabeza-de-termo/json-spec/instantiators/abstract-instantiator.rb +9 -0
  80. data/lib/cabeza-de-termo/json-spec/instantiators/all-expectations-composite-instantiator.rb +12 -0
  81. data/lib/cabeza-de-termo/json-spec/instantiators/any-expectation-composite-instantiator.rb +12 -0
  82. data/lib/cabeza-de-termo/json-spec/instantiators/block-expectation-instantiator.rb +16 -0
  83. data/lib/cabeza-de-termo/json-spec/instantiators/composite-instantiator.rb +45 -0
  84. data/lib/cabeza-de-termo/json-spec/instantiators/modifier-composite-instantiator.rb +12 -0
  85. data/lib/cabeza-de-termo/json-spec/instantiators/negated-expectation-instantiator.rb +20 -0
  86. data/lib/cabeza-de-termo/json-spec/instantiators/patial-application-instantiator.rb +26 -0
  87. data/lib/cabeza-de-termo/json-spec/json-spec.rb +2 -0
  88. data/lib/cabeza-de-termo/json-spec/message-formatters/block-message-formatter.rb +15 -0
  89. data/lib/cabeza-de-termo/json-spec/message-formatters/erb-message-formatter.rb +60 -0
  90. data/lib/cabeza-de-termo/json-spec/message-formatters/message-formatter.rb +9 -0
  91. data/lib/cabeza-de-termo/json-spec/metaprogramming/message-send.rb +37 -0
  92. data/lib/cabeza-de-termo/json-spec/metaprogramming/message.rb +37 -0
  93. data/lib/cabeza-de-termo/json-spec/metaprogramming/object-method.rb +14 -0
  94. data/lib/cabeza-de-termo/json-spec/modifiers/can-be-absent-modifier.rb +13 -0
  95. data/lib/cabeza-de-termo/json-spec/modifiers/can-be-null-modifier.rb +13 -0
  96. data/lib/cabeza-de-termo/json-spec/modifiers/expression-modifier.rb +9 -0
  97. data/lib/cabeza-de-termo/json-spec/modifiers/modifier-composite.rb +27 -0
  98. data/lib/cabeza-de-termo/json-spec/signals/signal.rb +6 -0
  99. data/lib/cabeza-de-termo/json-spec/signals/skip-branch-signal.rb +8 -0
  100. data/lib/cabeza-de-termo/json-spec/utilities/bind.rb +20 -0
  101. data/lib/cabeza-de-termo/json-spec/utilities/range.rb +70 -0
  102. data/lib/cabeza-de-termo/json-spec/value-holders/accessors-chain.rb +27 -0
  103. data/lib/cabeza-de-termo/json-spec/value-holders/missing-value.rb +21 -0
  104. data/lib/cabeza-de-termo/json-spec/value-holders/value-holder.rb +135 -0
  105. data/lib/cabeza-de-termo/json-spec/version.rb +5 -0
  106. data/lib/cabeza-de-termo/json-spec/walkers/expression-walker.rb +66 -0
  107. data/lib/cabeza-de-termo/json-spec/walkers/json-expectations-runner.rb +214 -0
  108. data/lib/cabeza-de-termo/json-spec/walkers/json-expression-explainer.rb +183 -0
  109. data/lib/cabeza-de-termo/json-spec/walkers/reporter/expectation-report.rb +63 -0
  110. data/lib/cabeza-de-termo/json-spec/walkers/reporter/json-expectations-reporter.rb +111 -0
  111. data/lib/cabeza-de-termo/json-spec/walkers/validator/json-validator-error.rb +29 -0
  112. data/lib/cabeza-de-termo/json-spec/walkers/validator/json-validator.rb +133 -0
  113. data/lib/cabeza-de-termo/json-spec/walkers/value-holders-stack-behaviour.rb +57 -0
  114. metadata +242 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: dd9a98802b2e9d9d82dff4d9fd8b6b174812f195
4
+ data.tar.gz: 071fdf0b74f43cae45e079b089a14fd0fc2af5fc
5
+ SHA512:
6
+ metadata.gz: 797adfabb1a8111259da2fdc097d47f68914414a07a8fdc760cd5aaad744e91bdece18dbf735a2e78124361c57779b3d3b127ba5deeaeb51fae5c0e8038248d2
7
+ data.tar.gz: 59e894989cc21acb4abdc0feb9bcee8cfd4ef892111e6d39195ed0891ae543fbb564b499e57c73cbc1e995c8a4d60e95f31b10d5d1b153e2c5e47650eb6307e9
@@ -0,0 +1,3 @@
1
+ Gemfile.lock
2
+
3
+ coverage
@@ -0,0 +1,3 @@
1
+ [submodule "cachivache/stuff-library"]
2
+ path = cachivache/stuff-library
3
+ url = git@github.com:cabeza-de-termo/stuff-library.git
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in json-spec.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Martin Rubi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,960 @@
1
+ # CabezaDeTermo::JsonSpec
2
+
3
+ A framework to declare expectations and verify that a json object satisfies those expectations. You can use this expectations to validate jsons you send or receive in your application, or to test your API with unit tests.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'json-spec'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install json-spec
20
+
21
+ ## Usage
22
+
23
+ If you want to read the supported expressions and expectations by `json-spec`, see the [API documentation](api_documentation.md).
24
+
25
+ If you want to jump to a running example, see the [full example](examples/example-7-full-example.rb).
26
+
27
+ Otherwise walk through this tutorial where we will be writting a spec to validate a partial specification of the `composer.json` used by a [dyslexic cousin](https://github.com/cabeza-de-termo/php-json-spec) of this project:
28
+
29
+ ```json
30
+ {
31
+ "name": "cabeza-de-termo/json-spec",
32
+ "type": "library",
33
+ "description": "A framework to declare expectations and verify that a json object complies with those expectations. You can use this json expectations to validate jsons you send or receive in your application, or to test your API with unit tests.",
34
+ "keywords": ["json", "assertions", "expectations", "validation", "phpunit"],
35
+ "homepage": "https://github.com/cabeza-de-termo/php-json-spec",
36
+ "license": "MIT",
37
+ "authors": [
38
+ {
39
+ "name": "Martin Rubi",
40
+ "email": "martin.rubi@martinrubi.com"
41
+ }
42
+ ],
43
+ "require": {
44
+ "php": ">=5.4.0"
45
+ },
46
+ "require-dev": {
47
+ "phpunit/phpunit": "^4",
48
+ "phpdocumentor/phpdocumentor": "2.*"
49
+ },
50
+ "autoload": {
51
+ "psr-4": {
52
+ "CabezaDeTermo\\JsonSpec\\": ["src/"],
53
+ "CabezaDeTermo\\JsonSpec\\Tests\\": ["tests/"]
54
+ }
55
+ }
56
+ }
57
+ ```
58
+
59
+ ## Starting with a simple validation
60
+
61
+ We will declare each expression we expect the `composer.json` to have, and for each expression we will declare expectations on it:
62
+
63
+ ```ruby
64
+ require 'cabeza-de-termo/json-spec/json-spec'
65
+
66
+ valid_licenses =
67
+ ['Apache-2.0', 'BSD-2-Clause', 'BSD-3-Clause',
68
+ 'BSD-4-Clause' ,'GPL-2.0', 'GPL-2.0+', 'GPL-3.0',
69
+ 'GPL-3.0+', 'LGPL-2.1', 'LGPL-2.1+', 'LGPL-3.0',
70
+ 'LGPL-3.0+', 'MIT']
71
+
72
+ json_spec = CabezaDeTermo::JsonSpec::JsonSpec.new do
73
+ expect_an(:object) do
74
+ expect('name') .to_be_defined .not_blank
75
+ expect('type') .to_be_defined .not_blank
76
+ expect('description') .to_be_defined .not_blank
77
+ expect('keywords') .to_be_a(:list) .can_be_absent .not_empty do
78
+ each do
79
+ expect_a(:scalar) .not_blank
80
+ end
81
+ end
82
+ expect('homepage') .to_be_url
83
+ expect('license') .to_be_defined .to_be_in(valid_licenses)
84
+ expect('authors') .to_be_a(:list) .to_be_defined .not_empty do
85
+ each do
86
+ expect_an(:object) do
87
+ expect('name') .to_be_defined .not_blank
88
+ expect('email') .to_be_defined .to_be_email
89
+ end
90
+ end
91
+ end
92
+ expect('require') .to_be_an(:object) .can_be_absent
93
+ expect('require-dev') .to_be_an(:object) .can_be_absent
94
+ expect('autoload') .to_be_an(:object) .to_be_defined do
95
+ expect('psr-0') .to_be_an(:object) .can_be_absent
96
+ expect('psr-4') .to_be_an(:object) .can_be_absent
97
+ end
98
+ end
99
+ end
100
+ ```
101
+
102
+ That's it. Now we can validate a json string by running:
103
+
104
+ ```ruby
105
+ validator = json_spec.validate_string json_string
106
+
107
+ puts validator.errors
108
+ puts validator.unexpected_fields
109
+ ```
110
+
111
+ If you want to run this example, see the [first example](examples/example-1-simple.rb) in the `examples/` folder.
112
+
113
+ ## Defining default expectations for each expression
114
+
115
+ One thing you may have noticed about the previous example is that it includes a lot of repeated `.to_be_defined` expectations on many fields. It would be easier if we could just state that `.to_be_defined` is expected for every field.
116
+
117
+ We can do that in two different ways.
118
+
119
+ If we want to declare default expectations at a global scope, i.e., for every expression used in any place, then we can do:
120
+
121
+ ```ruby
122
+ CabezaDeTermo::JsonSpec::ExpectationsLibrary.define do
123
+ default_expectations do
124
+ for_every_field do
125
+ to_be_defined
126
+ to_be_string
127
+ not_blank
128
+ ...
129
+ end
130
+ end
131
+ end
132
+ ```
133
+
134
+ If we want to declare default expectations for a json_spec only then we can do:
135
+
136
+ ```ruby
137
+ json_spec.define do
138
+ default_expectations do
139
+ for_every_field do
140
+ to_be_defined
141
+ to_be_string
142
+ not_blank
143
+ ...
144
+ end
145
+ end
146
+ end
147
+ ```
148
+
149
+ We can declare default expectations at any scope for different expressions:
150
+
151
+ ```ruby
152
+ default_expectations do
153
+ for_every_object do
154
+ ...
155
+ end
156
+
157
+ for_every_list do
158
+ ...
159
+ end
160
+
161
+ for_every_scalar do
162
+ ...
163
+ end
164
+
165
+ for_every_field do
166
+ ...
167
+ end
168
+ end
169
+ ```
170
+
171
+ If we want to get rid of the current default expressions, in the corresponding scope we declare any of:
172
+
173
+ ```ruby
174
+ default_expectations do
175
+ drop_all_expectations
176
+ drop_expectations_for(:objects)
177
+ drop_expectations_for(:lists)
178
+ drop_expectations_for(:fields)
179
+ drop_expectations_for(:scalars)
180
+ end
181
+ ```
182
+
183
+ You can see which default expectations are used by the framework in the [DefaultLibraryInitializer](lib/cabeza-de-termo/json-spec/expectations-library/initializers/default-library-initializer.rb) class.
184
+
185
+ So, back to our example, now the validation looks like:
186
+
187
+ ```ruby
188
+ CabezaDeTermo::JsonSpec::ExpectationsLibrary.define do
189
+ default_expectations do
190
+ for_every_field do
191
+ to_be_defined
192
+ end
193
+ end
194
+ end
195
+
196
+ valid_licenses =
197
+ ['Apache-2.0', 'BSD-2-Clause', 'BSD-3-Clause',
198
+ 'BSD-4-Clause' ,'GPL-2.0', 'GPL-2.0+', 'GPL-3.0',
199
+ 'GPL-3.0+', 'LGPL-2.1', 'LGPL-2.1+', 'LGPL-3.0',
200
+ 'LGPL-3.0+', 'MIT']
201
+
202
+ json_spec = CabezaDeTermo::JsonSpec::JsonSpec.new do
203
+ expect_an(:object) do
204
+ expect('name') .not_blank
205
+ expect('type') .not_blank
206
+ expect('description') .not_blank
207
+ expect('keywords') .to_be_a(:list) .can_be_absent .not_empty do
208
+ each do
209
+ expect(:scalar) .not_blank
210
+ end
211
+ end
212
+ expect('homepage') .to_be_url
213
+ expect('license') .to_be_in(valid_licenses)
214
+ expect('authors') .to_be_a(:list) .not_empty do
215
+ each do
216
+ expect_an(:object) do
217
+ expect('name') .not_blank
218
+ expect('email') .to_be_email
219
+ end
220
+ end
221
+ end
222
+ expect('require') .to_be_an(:object) .can_be_absent
223
+ expect('require-dev') .to_be_an(:object) .can_be_absent
224
+ expect('autoload') .to_be_an(:object) do
225
+ expect('psr-0') .to_be_an(:object) .can_be_absent
226
+ expect('psr-4') .to_be_an(:object) .can_be_absent
227
+ end
228
+ end
229
+ end
230
+ ```
231
+
232
+ If you want to run this example, see the [second example](examples/example-2-default-expectations.rb) in the `examples/` folder.
233
+
234
+ ## Declaring expectations for fields not known in advance.
235
+
236
+ If you run the previous example, you may notice that it outputs the folowing:
237
+
238
+ ```
239
+ - Failed expectations: 0
240
+
241
+ - Unexpected fields: 5
242
+ Field: '@.require' message: "An unexpected 'php' field was found."
243
+ Field: '@.require-dev' message: "An unexpected 'phpunit/phpunit' field was found."
244
+ Field: '@.require-dev' message: "An unexpected 'phpdocumentor/phpdocumentor' field was found."
245
+ Field: '@.autoload.psr-4' message: "An unexpected 'CabezaDeTermoJsonSpec\' field was found."
246
+ Field: '@.autoload.psr-4' message: "An unexpected 'CabezaDeTermo\JsonSpec\Tests\' field was found."
247
+ ```
248
+
249
+ That is because the `composer.json` has the following sections:
250
+
251
+ ```json
252
+ "require": {
253
+ "php": ">=5.4.0"
254
+ },
255
+ "require-dev": {
256
+ "phpunit/phpunit": "^4",
257
+ "phpdocumentor/phpdocumentor": "2.*"
258
+ }
259
+
260
+ ...
261
+
262
+ "psr-4": {
263
+ "CabezaDeTermoJsonSpec\\\\": ["src/"],
264
+ "CabezaDeTermo\\\\JsonSpec\\\\Tests\\\\": ["tests/"]
265
+ }
266
+ ```
267
+
268
+ with fields with names that we don't know in advance. They can be anything, as long as they comply with what `composer.json` expects. So we also want to declare expectations on unkown fields.
269
+
270
+ To do that, we use the `:each_field` expectation:
271
+
272
+ ```ruby
273
+ expect('require') .to_be_an(:object) .can_be_absent do
274
+ each_field do
275
+ expect_name .not_blank
276
+ expect_a(:scalar) .not_blank
277
+ end
278
+ end
279
+ ...
280
+ expect('psr-0') .to_be_an(:object) .can_be_absent do
281
+ each_field do
282
+ expect_name
283
+
284
+ expect_a(:list) .not_empty do
285
+ each do
286
+ expect_a(:scalar) .not_blank
287
+ end
288
+ end
289
+ end
290
+ end
291
+ ```
292
+
293
+ If you want to run this example, see the [third example](examples/example-3-each-field.rb) in the `examples/` folder.
294
+
295
+
296
+ ## Refactoring the expectations
297
+
298
+ So by now we have the following expectations:
299
+
300
+ ```ruby
301
+ json_spec = CabezaDeTermo::JsonSpec::JsonSpec.new do
302
+ expect_an(:object) do
303
+ expect('name') .not_blank
304
+ expect('type') .not_blank
305
+ expect('description') .not_blank
306
+
307
+ expect('keywords') .to_be_a(:list) .can_be_absent .not_empty do
308
+ each do
309
+ expect_a(:scalar) .not_blank
310
+ end
311
+ end
312
+
313
+ expect('homepage') .to_be_url
314
+ expect('license') .to_be_in(valid_licenses)
315
+
316
+ expect('authors') .to_be_a(:list) .not_empty do
317
+ each do
318
+ expect_an(:object) do
319
+ expect('name') .not_blank
320
+ expect('email') .to_be_email
321
+ end
322
+ end
323
+ end
324
+
325
+ expect('require') .to_be_an(:object) .can_be_absent do
326
+ each_field do
327
+ expect_name .not_blank
328
+ expect_a(:scalar) .not_blank
329
+ end
330
+ end
331
+
332
+ expect('require-dev') .to_be_an(:object) .can_be_absent do
333
+ each_field do
334
+ expect_name.not_blank
335
+ expect_a(:scalar) .not_blank
336
+ end
337
+ end
338
+
339
+ expect('autoload') .to_be_an(:object) do
340
+ expect('psr-0') .to_be_an(:object) .can_be_absent do
341
+ each_field do
342
+ expect_name
343
+
344
+ expect_a(:list) .not_empty do
345
+ each do
346
+ expect_a(:scalar) .not_blank
347
+ end
348
+ end
349
+ end
350
+ end
351
+
352
+ expect('psr-4') .to_be_an(:object) .can_be_absent do
353
+ each_field do
354
+ expect_name
355
+
356
+ expect_a(:list) .not_empty do
357
+ each do
358
+ expect_a(:scalar) .not_blank
359
+ end
360
+ end
361
+ end
362
+ end
363
+
364
+ expect('classmap') .to_be_a(:list) .can_be_absent
365
+
366
+ expect('files') .to_be_a(:list) .can_be_absent
367
+ end
368
+ end
369
+ end
370
+ ```
371
+
372
+ This have several problems. One is its extension. The spec just got too long. Second, now it is a structure of expectations on json expressions that lacks of some intention revealing names about the expressions. For instance, this section
373
+
374
+ ```ruby
375
+ expect_an(:object) do
376
+ expect('name') .not_blank
377
+ expect('email') .to_be_email
378
+ end
379
+ ```
380
+
381
+ refers to an author, but if we only look at it without its parent expression, we don't know that.
382
+ And third, and worst than the previous reasons, is that we are duplicating expectations for `require` and `require-dev`.
383
+
384
+ So it would be nice to be able to organize the expectations somehow.
385
+
386
+ We can put expectations in methods, and then call those methods from within the json_spec. This methods can be in any class, but we are going to create a `ComposerJson` class to keep the `composer.json` expectations in one place.
387
+ So now we have the class
388
+
389
+ ```ruby
390
+ class ComposerJson
391
+ def spec()
392
+ CabezaDeTermo::JsonSpec::JsonSpec.new do |json_spec|
393
+ json_spec.expect_an(:object) do |object|
394
+ object.expect('name') .not_blank
395
+ object.expect('type') .not_blank
396
+ object.expect('description') .not_blank
397
+ object.expect('keywords') .to_be_as_defined_in(self, :keywords_spec)
398
+ object.expect('homepage').to_be_url
399
+ object.expect('license') .to_be_in(self.valid_licenses)
400
+ object.expect('authors') .to_be_as_defined_in(self, :authors_spec)
401
+ object.expect('require') .to_be_as_defined_in(self, :require_spec)
402
+ object.expect('require-dev') .to_be_as_defined_in(self, :require_spec)
403
+ object.expect('autoload') .to_be_as_defined_in(self, :autoload_spec)
404
+ end
405
+ end
406
+ end
407
+
408
+ def keywords_spec(json_spec)
409
+ json_spec .to_be_a(:list) .can_be_absent .not_empty do
410
+ each do
411
+ expect_a(:scalar) .not_blank
412
+ end
413
+ end
414
+ end
415
+
416
+ def authors_spec(json_spec)
417
+ json_spec .to_be_a(:list) .not_empty do |list|
418
+ list.each do |each|
419
+ each.to_be_as_defined_in(self, :author_spec)
420
+ end
421
+ end
422
+ end
423
+
424
+ def author_spec(json_spec)
425
+ json_spec .expect_an(:object) do
426
+ expect('name') .not_blank
427
+ expect('email') .to_be_email
428
+ end
429
+ end
430
+
431
+ def require_spec(json)
432
+ json .to_be_an(:object) .can_be_absent do
433
+ each_field do
434
+ expect_name .not_blank
435
+ expect_a(:scalar) .not_blank
436
+ end
437
+ end
438
+ end
439
+
440
+ def autoload_spec(json)
441
+ json .to_be_an(:object) do |object|
442
+ object.expect('psr-0') .to_be_as_defined_in(self, :psr_spec)
443
+ object.expect('psr-4') .to_be_as_defined_in(self, :psr_spec)
444
+ object.expect('classmap') .to_be_as_defined_in(self, :classmaps_spec)
445
+ object.expect('files') .to_be_as_defined_in(self, :files_spec)
446
+ end
447
+ end
448
+
449
+ def psr_spec(json_spec)
450
+ json_spec .to_be_an(:object) .can_be_absent do
451
+ each_field do
452
+ expect_name
453
+
454
+ expect_a(:list) .not_empty do
455
+ each do
456
+ expect_a(:scalar) .not_blank
457
+ end
458
+ end
459
+ end
460
+ end
461
+ end
462
+
463
+ def classmaps_spec(json_spec)
464
+ json_spec .to_be_a(:list) .can_be_absent .not_empty
465
+ end
466
+
467
+ def files_spec(json_spec)
468
+ json_spec .to_be_a(:list) .can_be_absent .not_empty
469
+ end
470
+
471
+ def valid_licenses()
472
+ ['Apache-2.0', 'BSD-2-Clause', 'BSD-3-Clause',
473
+ 'BSD-4-Clause' ,'GPL-2.0', 'GPL-2.0+', 'GPL-3.0',
474
+ 'GPL-3.0+', 'LGPL-2.1', 'LGPL-2.1+', 'LGPL-3.0',
475
+ 'LGPL-3.0+', 'MIT']
476
+ end
477
+ end
478
+ ```
479
+
480
+ and to run the validation all we have to do is
481
+
482
+ ```ruby
483
+ validator = ComposerJson.new.spec.validate_string(json_string)
484
+ ```
485
+
486
+ If you want to validate a not very complex json, you can get away with it without factorizing the expectations. But as soon as the validation gets more complex, you can refactor the expectations using `:to_be_as_defined_in(some_object, :some_method)`.
487
+
488
+ One thing to notice in this example is that we declared things like
489
+
490
+ ```ruby
491
+ CabezaDeTermo::JsonSpec::JsonSpec.new do |json_spec|
492
+ json_spec.expect_an(:object) do |object|
493
+ ...
494
+ end
495
+ end
496
+ ```
497
+
498
+ instead of
499
+
500
+ ```ruby
501
+ CabezaDeTermo::JsonSpec::JsonSpec.new do
502
+ json_spec.expect_an(:object) do
503
+ ...
504
+ end
505
+ end
506
+ ```
507
+
508
+ That is because when we don't pass a parameter to the defintion block, it changes the binding of self, and the we can not declare thinkgs like
509
+
510
+ ```ruby
511
+ .to_be_as_defined_in(self, :keywords_spec)
512
+ ```
513
+
514
+ Changing the binding to self is usually a bad idea, but in the definitions of this framework it will only do that if no parameter is given to the definition block.
515
+
516
+ If you want to run this example, see the [fourth example](examples/example-4-to-be-as-defined-in.rb) in the `examples/` folder.
517
+
518
+ ## Expecting different structures for the same expression
519
+
520
+ If we look at the `composer.json` definition, we will notice that sometimes it accepts different structures for the same field. For instance, for `psr-4` values, it can take a list of folder strings or a single folder string.
521
+
522
+ To expect different structures on the same object, we use `:expect(:any_of)`:
523
+
524
+ ```ruby
525
+ expect('psr-4') .to_be_a(:list) do
526
+ expect(:any_of) do
527
+ expect_a(:scalar) .to_be_folder
528
+ or_also
529
+ expect_a(:list) .not_empty do
530
+ each do
531
+ expect_a(:scalar) .to_be_folder
532
+ end
533
+ end
534
+ end
535
+ end
536
+ ```
537
+
538
+ ## Defining custom expectations
539
+
540
+ So far we only used the expectations defined by the `json-spec` framework. But more likely we will want to define our custom expectations. There are several reasons for that. Different APIs use different formats, or in some contexts we may want to use more intention revealing expectation names, to name a few.
541
+
542
+ There are many ways to define new expectations, let's go through them:
543
+
544
+ - Delegate the new expectation to an existing one.
545
+
546
+ Suppose we want to check if a value is equal to 42. We can achieve that by doing
547
+
548
+ ```ruby
549
+ json_spec .to_be_equal_to(42)
550
+ ```
551
+
552
+ But if we want a more meaningful expectation name, we can add it to the [ExpectationsLibrary](lib/cabeza-de-termo/json-spec/expectations-library/expectations-library.rb).
553
+
554
+ Here's an example of doing so:
555
+
556
+ ```ruby
557
+ CabezaDeTermo::JsonSpec::ExpectationsLibrary.define do
558
+ expectations do
559
+ define :to_be_the_answer_to_life_the_universe_and_everything do
560
+ expecting :to_be_equal_to, 42
561
+ message "Nop, this is not the answer to life, the universe and everything."
562
+ end
563
+ end
564
+ end
565
+ ```
566
+
567
+ ```ruby
568
+ json_spec .to_be_the_answer_to_life_the_universe_and_everything
569
+ # is now equivalent to:
570
+ json_spec .to_be_equal_to(42)
571
+ ```
572
+
573
+ Here's another interesting example of defining new expectations by delegation:
574
+
575
+ ```ruby
576
+ CabezaDeTermo::JsonSpec::ExpectationsLibrary.define do
577
+ expectations do
578
+ define :to_be_date do
579
+ expecting :to_match, /^\d\d\d\d-\d\d-\d\d$/
580
+ message "Not a valid date format."
581
+ end
582
+ end
583
+ end
584
+ ```
585
+
586
+ This framework does not include a `to_be_date` expectation because it varies a lot from one API to another, but you can easyly add the one that suits your needs.
587
+
588
+ Now compare using in your specs:
589
+
590
+ ```ruby
591
+ json_spec .to_match(/^\d\d\d\d-\d\d-\d\d$/)
592
+ # vs
593
+ json_spec .to_be_date
594
+ ```
595
+
596
+ - Negate an existing expectation.
597
+
598
+ If you want to expect that something is not what another expectation asserts, do:
599
+
600
+ ```ruby
601
+ CabezaDeTermo::JsonSpec::ExpectationsLibrary.define do
602
+ expectations do
603
+ define :not_date do
604
+ negating :to_be_date
605
+ message "Expected an invalid date, got a valid one."
606
+ end
607
+ end
608
+ end
609
+ ```
610
+
611
+ - Chain several existing expectations:
612
+
613
+ ```ruby
614
+ CabezaDeTermo::JsonSpec::ExpectationsLibrary.define do
615
+ expectations do
616
+ define :to_be_defined do
617
+ expecting_all_of :to_exist
618
+ and_also :not_null
619
+
620
+ message "Failed asserting that the field '<%= field %>' with value = '<%= format value %>' is defined."
621
+ end
622
+ end
623
+ end
624
+ ```
625
+
626
+ - Expect one existing expectation among several ones:
627
+
628
+ ```ruby
629
+ CabezaDeTermo::JsonSpec::ExpectationsLibrary.define do
630
+ expectations do
631
+ define :to_be_accessor do
632
+ expecting_any_of :to_be_getter
633
+ or_also :to_be_setter
634
+
635
+ message "The value is not an accessor string."
636
+ end
637
+ end
638
+ end
639
+ ```
640
+
641
+ So far we only composed existing expectations. Now we are going to define new ones.
642
+
643
+ - Define a new expectation with a closure:
644
+
645
+ ```ruby
646
+ CabezaDeTermo::JsonSpec::ExpectationsLibrary.define do
647
+ expectations do
648
+ define :to_be_greater_than do
649
+ with_block { |value_holder, expected_value| expected_value < value_holder.value }
650
+ message "<%= value %> is not greater than <%= expectation.args[0] %>."
651
+ end
652
+ end
653
+ end
654
+ ```
655
+ - Or finally, if your expectation is more complex, create a class for it:
656
+
657
+ ```ruby
658
+ require 'cabeza-de-termo/json-spec/expectations/expectation'
659
+
660
+ class IsSomeComplexStuff < CabezaDeTermo::JsonSpec::Expectation
661
+ def is_satisfied_by?(value_holder)
662
+ # Check stuff and answer true or false.
663
+ # You can get the inspected value with value_holder.value
664
+ end
665
+ end
666
+ ```
667
+
668
+ and then define the expectation in the ExpectationsLibrary
669
+
670
+ ```ruby
671
+ CabezaDeTermo::JsonSpec::ExpectationsLibrary.define do
672
+ expectations do
673
+ define :to_be_some_complex_stuff do
674
+ with_class IsSomeComplexStuff
675
+ message "<%= value %> is not a ComplexStuff."
676
+ end
677
+ end
678
+ end
679
+ ```
680
+
681
+ If you want to run this example, see the [fifth example](examples/example-5-custom-expectations.rb) in the `examples/` folder.
682
+
683
+ ## Defining custom messages
684
+
685
+ We saw how we can define new Expectations with their own validation error message. But it would be nice to be able to change the validation error messages for the existing expectations as well.
686
+
687
+ We can do that at 2 different scopes, just like when we defined default expectations.
688
+
689
+ To override the validation messages globally, use
690
+
691
+ ```ruby
692
+ CabezaDeTermo::JsonSpec::ExpectationsLibrary.define do
693
+ expectations do
694
+ define :to_be_defined do
695
+ message "If the question is to be or not to be, this value has chosen not to be. Alas, poor <%= field %>. My ValidationError rises at it."
696
+ end
697
+ end
698
+ end
699
+ ```
700
+
701
+ If we want to override the message only for a single `json-spec`, do
702
+
703
+ ```ruby
704
+ json_spec.define do
705
+ expectations do
706
+ define :to_be_integer do
707
+ message "An integer, a integer! My <%= field %> for an integer!"
708
+ end
709
+ end
710
+ end
711
+ ```
712
+
713
+ You have to admit that these custom messages, although they are in a slang so boring and outdated that will get you to sleep in, like, 10 minutes, have a lot more of what we might call `poetic flight` than the default ones.
714
+
715
+ Just like when defining new Expectations, we have several ways to define custom messages. Lets walk through them:
716
+
717
+ - Using a [ErbMessageFormatter](lib/cabeza-de-termo/json-spec/message-formatters/erb-message-formatter.rb) object
718
+
719
+ ```ruby
720
+ CabezaDeTermo::JsonSpec::ExpectationsLibrary.define do
721
+ expectations do
722
+ define :to_be_integer do
723
+ message "An integer, a integer! My <%= field %> for an integer!"
724
+ end
725
+ end
726
+ end
727
+ ```
728
+
729
+ This is the simpliest way, and it should be enough most of the times. You can reference :value_holder, :value, :accessors_chain, :field and :expectation objects from within a erb block: `<%= field %>`.
730
+
731
+ - Using a [BlockMessageFormatter](lib/cabeza-de-termo/json-spec/message-formatters/block-message-formatter.rb) object
732
+
733
+ ```ruby
734
+ CabezaDeTermo::JsonSpec::ExpectationsLibrary.define do
735
+ expectations do
736
+ define :to_be_integer do
737
+ message_block { |expectation, value_holder| "No a valid integer." }
738
+ end
739
+ end
740
+ end
741
+ ```
742
+
743
+ - Using you own [MessageFormatter](lib/cabeza-de-termo/json-spec/message-formatters/message-formatter.rb) subclass
744
+
745
+ Define a new class with a `:message_on(expectation, value_holder)` method
746
+
747
+ ```ruby
748
+ class MyCustomMessageFormatter
749
+ def message_on(expectation, value_holder)
750
+ "Nop!"
751
+ end
752
+ end
753
+ ```
754
+
755
+ and then override the message
756
+
757
+ ```ruby
758
+ CabezaDeTermo::JsonSpec::ExpectationsLibrary.define do
759
+ expectations do
760
+ define :to_be_integer do
761
+ message_formatter MyCustomMessageFormatter.new
762
+ end
763
+ end
764
+ end
765
+ ```
766
+
767
+ If you want to run the custom messages example, see the [sixth example](examples/example-6-custom-messages.rb) in the `examples/` folder.
768
+
769
+ ## Conditional expectations and expression modifiers
770
+
771
+ So far we only talked about Expectations. However, sometimes expectations are not enough. For instance, sometimes you may want to decide whether to keep running expectations or not, but without failing. An example of that are the `:can_be_something` statements. `:can_be_absent` states that a field may be missing and it's ok, but if it is present we want to keep running expectations on that expression.
772
+ In the `json-spec` framework, each expression does not hold a collection of expectations, but an [ExpectationsRunner](lib/cabeza-de-termo/json-spec/expectations/runner/expectations-runner.rb) instead.
773
+ If you plug your own [AbstractExpectationsRunner](lib/cabeza-de-termo/json-spec/expectations/runner/abstract-expectations-runner.rb) you can alter the execution flow of the expectations for that expression.
774
+
775
+ And how would you plug an ExpectationsRunner in a json_expression?
776
+
777
+ With the help of an [ExpressionModifier](lib/cabeza-de-termo/json-spec/modifiers/expression-modifier.rb), which you can add to the [ExpectationsLibrary](lib/cabeza-de-termo/json-spec/expectations-library/expectations-library.rb) using its `:define` method, just like with the custom expectations.
778
+
779
+ Or perhaps some expectation needs to remove other expectations for an expression to make sense.
780
+ In that case you can also use your own ExpressionModifier.
781
+
782
+ This sounds more complicated than it actually is. Check the [CanBeNullModifier](lib/cabeza-de-termo/json-spec/modifiers/can-be-null-modifier.rb) to see a real example and see that it's actually quite easy to do weird stuff on the expressions and expectations execution flow.
783
+
784
+ ## Putting it all together
785
+
786
+ Ok, so we can define default expectations for each expression, add new custom expectations, add new custom modifiers and replace the default expectation messages without the need of subclassing any existing class of the framework. That is really handy for simple validations. But what if we have done a nice and complete set of expectations and messages that we want to keep together to use it in several places?
787
+
788
+ In that case, it is a good idea to bundle it all together in one place.
789
+
790
+ That place can be the [LibraryInitializer](lib/cabeza-de-termo/json-spec/expectations-library/initializers/library-initializer.rb).
791
+
792
+ Create you own class that implements the [LibraryInitializer](lib/cabeza-de-termo/json-spec/expectations-library/initializers/library-initializer.rb) protocol, and there create a new and fully configured [ExpectationsLibrary](lib/cabeza-de-termo/json-spec/expectations-library/expectations-library.rb).
793
+
794
+ Something like this:
795
+
796
+ ```ruby
797
+ class ComposerLibraryInitializer
798
+ def new_library()
799
+ initialize_library CabezaDeTermo::JsonSpec::DefaultLibraryInitializer.new.new_library
800
+ end
801
+
802
+ def initialize_library(library)
803
+ # add more custom stuff to the library here
804
+
805
+ library
806
+ end
807
+ end
808
+ ```
809
+
810
+ and now plug the initializer into the ExpectationsLibrary at your application bootstrap with
811
+
812
+ ```ruby
813
+ CabezaDeTermo::JsonSpec::ExpectationsLibrary
814
+ .set_default_library_initializer( ComposerLibraryInitializer.new )
815
+ ```
816
+
817
+ and that's it, now you can use the ExpectationsLibrary with all your custom additions.
818
+
819
+ To see how all the pieces fitted together, check and run the [seventh example](examples/example-7-full-example.rb) in the `examples/` folder.
820
+
821
+ ## Inspecting the expectations
822
+
823
+ With the default expectations and expression modifiers, it is quite sure that sooner or later you will want to see what expectations are actually set to each expression in a json_spec.
824
+ If you need to debug the expectations, run
825
+
826
+ ```ruby
827
+ puts json_spec.explain
828
+ ```
829
+
830
+ and will get something like this
831
+
832
+ ```ruby
833
+ {
834
+ "name":
835
+ anything .not_blank()
836
+ "type":
837
+ anything .not_blank()
838
+ "description":
839
+ anything .not_blank()
840
+ "keywords":
841
+ [
842
+ scalar .not_blank()
843
+ ]
844
+ if present
845
+ .to_be_list(Array) .not_empty()
846
+ "homepage":
847
+ anything .to_be_url()
848
+ "license":
849
+ anything .to_be_valid_lincense(Apache-2.0, BSD-2-Clause, BSD-3-Clause, BSD-4-Clause, GPL-2.0, GPL-2.0+, GPL-3.0, GPL-3.0+, LGPL-2.1, LGPL-2.1+, LGPL-3.0, LGPL-3.0+, MIT)
850
+ "authors":
851
+ [
852
+ {
853
+ "name":
854
+ anything .not_blank()
855
+ "email":
856
+ anything .to_be_email()
857
+ } .to_be_object(Hash)
858
+ ] .to_be_list(Array) .not_empty()
859
+ "require":
860
+ {
861
+ each field
862
+ name .not_blank()
863
+ value
864
+ scalar .to_be_version()
865
+ }
866
+ if present
867
+ .to_be_object(Hash)
868
+ "require-dev":
869
+ {
870
+ each field
871
+ name .not_blank()
872
+ value
873
+ scalar .to_be_version()
874
+ }
875
+ if present
876
+ .to_be_object(Hash)
877
+ "autoload":
878
+ {
879
+ "psr-0":
880
+ {
881
+ each field
882
+ name .to_be_string(String)
883
+ value
884
+ any of
885
+ scalar .to_be_folder()
886
+ or
887
+ [
888
+ scalar .to_be_folder()
889
+ ] .to_be_list(Array) .not_empty()
890
+ }
891
+ if present
892
+ .to_be_object(Hash)
893
+ "psr-4":
894
+ {
895
+ each field
896
+ name .to_be_psr4_key()
897
+ value
898
+ any of
899
+ scalar .to_be_folder()
900
+ or
901
+ [
902
+ scalar .to_be_folder()
903
+ ] .to_be_list(Array) .not_empty()
904
+ }
905
+ if present
906
+ .to_be_object(Hash)
907
+ "classmap":
908
+ [
909
+ ]
910
+ if present
911
+ .to_be_list(Array) .not_empty()
912
+ "files":
913
+ [
914
+ ]
915
+ if present
916
+ .to_be_list(Array) .not_empty()
917
+ } .to_be_object(Hash)
918
+ } .to_be_object(Hash)
919
+ ```
920
+
921
+ ## Development environment
922
+
923
+ So, you are too lazy to setup the development environment for this project. Yeah, I feel you. I am too.
924
+
925
+ Anyways, you can use the Vagrant configuration for that. To do so:
926
+
927
+ * Install [VirtualBox](https://www.virtualbox.org/)
928
+ * Install [Vagrant](https://www.vagrantup.com/)
929
+ * `git clone --recursive git@github.com:mrubi/ruby-json-spec.git`
930
+ * `cd ruby-json-spec/cachivache`
931
+ * `vagrant up`
932
+
933
+ and that will install all the necessary things to run the tests and examples in this project.
934
+
935
+ Then to start playing around with the code, do
936
+
937
+ * `vagrant ssh`
938
+ * `cd src/json-spec`
939
+
940
+ and that's it. You have a fully prepared, ready to use development environment.
941
+
942
+ # Running the tests
943
+
944
+ * `rake test`
945
+
946
+ ## Development
947
+
948
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
949
+
950
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
951
+
952
+ ## Contributing
953
+
954
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/json-spec.
955
+
956
+
957
+ ## License
958
+
959
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
960
+