heroics 0.0.12 → 0.0.13
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 +4 -4
- data/.travis.yml +3 -0
- data/bin/heroics-generate +4 -3
- data/lib/heroics/link.rb +4 -4
- data/lib/heroics/schema.rb +20 -3
- data/lib/heroics/version.rb +1 -1
- data/test/helper.rb +19 -0
- data/test/link_test.rb +53 -4
- data/test/schema_test.rb +24 -1
- metadata +23 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e1f56fc7f7100a44079eaaec68a2e7194992fc9c
|
4
|
+
data.tar.gz: 0c3730316fa3f9329b3406cc4fa25ce44331d409
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 461418c693655d9d717f7a9cdf00dc678f6c569655a3eb954678be101c5da93ba1f242cfa25e3a1c3c4683b3dd0e9a0b29467a9813428f5089c39792496adba9
|
7
|
+
data.tar.gz: 83c9808500e1927af547fa5fa2bb679667c9812133a6065dcf6714d6b32f8539e76477d2ccfaf02e26f28e7388c83c8de2b9aefba7c91354c357834798a71cd6
|
data/.travis.yml
CHANGED
data/bin/heroics-generate
CHANGED
@@ -2,10 +2,11 @@
|
|
2
2
|
|
3
3
|
require 'optparse'
|
4
4
|
require 'heroics'
|
5
|
+
require 'open-uri'
|
5
6
|
|
6
7
|
options = {headers: {}, cache_path: nil}
|
7
8
|
option_parser = OptionParser.new do |opts|
|
8
|
-
opts.banner = 'Usage: heroics-generate module_name
|
9
|
+
opts.banner = 'Usage: heroics-generate module_name schema_filepath url'
|
9
10
|
|
10
11
|
opts.on('-h', '--help', 'Display this screen') do
|
11
12
|
puts opts
|
@@ -28,8 +29,8 @@ option_parser.parse!
|
|
28
29
|
if ARGV.length != 3
|
29
30
|
puts option_parser
|
30
31
|
else
|
31
|
-
module_name,
|
32
|
-
schema = Heroics::Schema.new(MultiJson.decode(
|
32
|
+
module_name, schema_filepath, url = ARGV
|
33
|
+
schema = Heroics::Schema.new(MultiJson.decode(open(schema_filepath).read))
|
33
34
|
cache = 'Moneta.new(:Memory)'
|
34
35
|
if options[:cache_path]
|
35
36
|
cache = "Moneta.new(:File, dir: \"#{options[:cache_path]}\")"
|
data/lib/heroics/link.rb
CHANGED
@@ -47,8 +47,8 @@ module Heroics
|
|
47
47
|
path = "#{@path_prefix}#{path}" unless @path_prefix == '/'
|
48
48
|
headers = @default_headers
|
49
49
|
if body
|
50
|
-
headers = headers.merge({'Content-Type' =>
|
51
|
-
body =
|
50
|
+
headers = headers.merge({'Content-Type' => @link_schema.content_type})
|
51
|
+
body = @link_schema.encode(body)
|
52
52
|
end
|
53
53
|
cache_key = "#{path}:#{headers.hash}"
|
54
54
|
if @link_schema.method == :get
|
@@ -59,11 +59,11 @@ module Heroics
|
|
59
59
|
connection = Excon.new(@root_url)
|
60
60
|
response = connection.request(method: @link_schema.method, path: path,
|
61
61
|
headers: headers, body: body,
|
62
|
-
expects: [200, 201, 202, 206, 304])
|
62
|
+
expects: [200, 201, 202, 204, 206, 304])
|
63
63
|
content_type = response.headers['Content-Type']
|
64
64
|
if response.status == 304
|
65
65
|
MultiJson.load(@cache["data:#{cache_key}"])
|
66
|
-
elsif content_type && content_type
|
66
|
+
elsif content_type && content_type =~ /application\/.*json/
|
67
67
|
etag = response.headers['ETag']
|
68
68
|
if etag
|
69
69
|
@cache["etag:#{cache_key}"] = etag
|
data/lib/heroics/schema.rb
CHANGED
@@ -24,7 +24,6 @@ module Heroics
|
|
24
24
|
# @param name [String] The name of the resource.
|
25
25
|
# @raise [SchemaError] Raised if an unknown resource name is provided.
|
26
26
|
def resource(name)
|
27
|
-
resource_schema = @resources[name]
|
28
27
|
if @schema['definitions'].has_key?(name)
|
29
28
|
ResourceSchema.new(@schema, name)
|
30
29
|
else
|
@@ -58,7 +57,8 @@ module Heroics
|
|
58
57
|
def initialize(schema, name)
|
59
58
|
@schema = schema
|
60
59
|
@name = name
|
61
|
-
link_schema = schema['definitions'][name]['links']
|
60
|
+
link_schema = schema['definitions'][name]['links'] || []
|
61
|
+
|
62
62
|
@links = Hash[link_schema.each_with_index.map do |link, link_index|
|
63
63
|
link_name = Heroics.ruby_name(link['title'])
|
64
64
|
[link_name, LinkSchema.new(schema, name, link_index)]
|
@@ -128,6 +128,22 @@ module Heroics
|
|
128
128
|
link_schema['method'].downcase.to_sym
|
129
129
|
end
|
130
130
|
|
131
|
+
# Get the Content-Type for this link.
|
132
|
+
#
|
133
|
+
# @return [String] The Content-Type value
|
134
|
+
def content_type
|
135
|
+
link_schema['encType'] || 'application/json'
|
136
|
+
end
|
137
|
+
|
138
|
+
def encode(body)
|
139
|
+
case content_type
|
140
|
+
when 'application/x-www-form-urlencoded'
|
141
|
+
URI.encode_www_form(body)
|
142
|
+
when /application\/.*json/
|
143
|
+
MultiJson.dump(body)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
131
147
|
# Get the names of the parameters this link expects.
|
132
148
|
#
|
133
149
|
# @return [Array<String>] The parameters.
|
@@ -307,7 +323,8 @@ module Heroics
|
|
307
323
|
# @param [Fixnum,String,TrueClass,FalseClass,Time] The parameter to format.
|
308
324
|
# @return [String] The formatted parameter.
|
309
325
|
def format_parameter(parameter)
|
310
|
-
parameter.instance_of?(Time) ? iso_format(parameter) : parameter.to_s
|
326
|
+
formatted_parameter = parameter.instance_of?(Time) ? iso_format(parameter) : parameter.to_s
|
327
|
+
URI.escape formatted_parameter
|
311
328
|
end
|
312
329
|
|
313
330
|
# Convert a time to an ISO 8601 combined data and time format.
|
data/lib/heroics/version.rb
CHANGED
data/test/helper.rb
CHANGED
@@ -127,6 +127,25 @@ SAMPLE_SCHEMA = {
|
|
127
127
|
'email_field' => {
|
128
128
|
'$ref' => '#/definitions/resource/definitions/email_field'}}}},
|
129
129
|
|
130
|
+
{'description' => 'Submit a sample resource as form data',
|
131
|
+
'encType' => 'application/x-www-form-urlencoded',
|
132
|
+
'href' => '/resource',
|
133
|
+
'method' => 'POST',
|
134
|
+
'rel' => 'submit',
|
135
|
+
'title' => 'Submit',
|
136
|
+
'schema' => {
|
137
|
+
'properties' => {
|
138
|
+
'date_field' => {
|
139
|
+
'$ref' => '#/definitions/resource/definitions/date_field'},
|
140
|
+
'string_field' => {
|
141
|
+
'$ref' => '#/definitions/resource/definitions/string_field'},
|
142
|
+
'boolean_field' => {
|
143
|
+
'$ref' => '#/definitions/resource/definitions/boolean_field'},
|
144
|
+
'uuid_field' => {
|
145
|
+
'$ref' => '#/definitions/resource/definitions/uuid_field'},
|
146
|
+
'email_field' => {
|
147
|
+
'$ref' => '#/definitions/resource/definitions/email_field'}}}},
|
148
|
+
|
130
149
|
{'description' => 'Update a sample resource',
|
131
150
|
'href' => '/resource/{(%23%2Fdefinitions%2Fresource%2Fdefinitions%2Fuuid_field)}',
|
132
151
|
'method' => 'PATCH',
|
data/test/link_test.rb
CHANGED
@@ -38,6 +38,20 @@ class LinkTest < MiniTest::Unit::TestCase
|
|
38
38
|
assert_equal(nil, link.run('44724831-bf66-4bc2-865f-e2c4c2b14c78'))
|
39
39
|
end
|
40
40
|
|
41
|
+
# Link.run URL-escapes special characters in parameters.
|
42
|
+
def test_run_with_parameters_needing_escaping
|
43
|
+
Excon.stub(method: :get) do |request|
|
44
|
+
assert_equal('/resource/foo%23bar', request[:path])
|
45
|
+
Excon.stubs.pop
|
46
|
+
{status: 200, body: ''}
|
47
|
+
end
|
48
|
+
|
49
|
+
schema = Heroics::Schema.new(SAMPLE_SCHEMA)
|
50
|
+
link = Heroics::Link.new('https://example.com',
|
51
|
+
schema.resource('resource').link('info'))
|
52
|
+
assert_equal(nil, link.run('foo#bar'))
|
53
|
+
end
|
54
|
+
|
41
55
|
# Link.run converts Time parameters to UTC before sending them to the
|
42
56
|
# server.
|
43
57
|
def test_run_converts_time_parameters_to_utc
|
@@ -71,6 +85,25 @@ class LinkTest < MiniTest::Unit::TestCase
|
|
71
85
|
assert_equal(nil, link.run(body))
|
72
86
|
end
|
73
87
|
|
88
|
+
# Link.run optionally takes an extra parameter to send in the request body.
|
89
|
+
# It automatically converts the specified object to the specified encoding
|
90
|
+
# type and includes a Content-Type header in the request
|
91
|
+
def test_run_without_parameters_and_with_non_json_request_body
|
92
|
+
body = {'Hello' => 'world!'}
|
93
|
+
Excon.stub(method: :post) do |request|
|
94
|
+
assert_equal('application/x-www-form-urlencoded', request[:headers]['Content-Type'])
|
95
|
+
assert_equal('Hello=world%21', request[:body])
|
96
|
+
Excon.stubs.pop
|
97
|
+
{status: 200, body: ''}
|
98
|
+
end
|
99
|
+
|
100
|
+
schema = Heroics::Schema.new(SAMPLE_SCHEMA)
|
101
|
+
link = Heroics::Link.new('https://example.com',
|
102
|
+
schema.resource('resource').link('submit'))
|
103
|
+
assert_equal(nil, link.run(body))
|
104
|
+
end
|
105
|
+
|
106
|
+
|
74
107
|
# Link.run passes custom headers to the server when they've been provided.
|
75
108
|
def test_run_with_custom_request_headers
|
76
109
|
Excon.stub(method: :get) do |request|
|
@@ -178,7 +211,7 @@ class LinkTest < MiniTest::Unit::TestCase
|
|
178
211
|
assert_equal('/resource', request[:path])
|
179
212
|
Excon.stubs.pop
|
180
213
|
{status: 200,
|
181
|
-
headers: {'Content-Type' => 'application/json;charset=utf-8'},
|
214
|
+
headers: {'Content-Type' => 'application/vnd.api+json;charset=utf-8'},
|
182
215
|
body: MultiJson.dump(body)}
|
183
216
|
end
|
184
217
|
|
@@ -204,6 +237,20 @@ class LinkTest < MiniTest::Unit::TestCase
|
|
204
237
|
assert_equal(body, link.run)
|
205
238
|
end
|
206
239
|
|
240
|
+
# Link.run considers HTTP 204 No Content responses as successful.
|
241
|
+
def test_run_with_no_content_response
|
242
|
+
Excon.stub(method: :delete) do |request|
|
243
|
+
assert_equal("/resource/2013-01-01T08:00:00Z", request[:path])
|
244
|
+
Excon.stubs.pop
|
245
|
+
{status: 204, body: ''}
|
246
|
+
end
|
247
|
+
|
248
|
+
schema = Heroics::Schema.new(SAMPLE_SCHEMA)
|
249
|
+
link = Heroics::Link.new('https://example.com',
|
250
|
+
schema.resource('resource').link('delete'))
|
251
|
+
assert_equal(nil, link.run(Time.parse('2013-01-01 00:00:00-0800')))
|
252
|
+
end
|
253
|
+
|
207
254
|
# Link.run raises an Excon error if anything other than a 200 or 201 HTTP
|
208
255
|
# status code was returned by the server.
|
209
256
|
def test_run_with_failed_request
|
@@ -251,8 +298,9 @@ class LinkTest < MiniTest::Unit::TestCase
|
|
251
298
|
{status: 200}
|
252
299
|
end
|
253
300
|
|
301
|
+
headers = {}
|
254
302
|
cache = Moneta.new(:Memory)
|
255
|
-
cache[
|
303
|
+
cache["etag:/resource:#{headers.hash}"] = 'etag-contents'
|
256
304
|
schema = Heroics::Schema.new(SAMPLE_SCHEMA)
|
257
305
|
link = Heroics::Link.new('https://example.com',
|
258
306
|
schema.resource('resource').link('list'),
|
@@ -287,9 +335,10 @@ class LinkTest < MiniTest::Unit::TestCase
|
|
287
335
|
{status: 304, headers: {'Content-Type' => 'application/json'}}
|
288
336
|
end
|
289
337
|
|
338
|
+
headers = {}
|
290
339
|
cache = Moneta.new(:Memory)
|
291
|
-
cache[
|
292
|
-
cache[
|
340
|
+
cache["etag:/resource:#{headers.hash}"] = 'etag-contents'
|
341
|
+
cache["data:/resource:#{headers.hash}"] = MultiJson.dump(body)
|
293
342
|
schema = Heroics::Schema.new(SAMPLE_SCHEMA)
|
294
343
|
link = Heroics::Link.new('https://example.com',
|
295
344
|
schema.resource('resource').link('list'),
|
data/test/schema_test.rb
CHANGED
@@ -54,7 +54,7 @@ class ResourceSchemaTest < MiniTest::Unit::TestCase
|
|
54
54
|
def test_links
|
55
55
|
schema = Heroics::Schema.new(SAMPLE_SCHEMA)
|
56
56
|
assert_equal(
|
57
|
-
['list', 'info', 'identify_resource', 'create', 'update', 'delete'],
|
57
|
+
['list', 'info', 'identify_resource', 'create', 'submit', 'update', 'delete'],
|
58
58
|
schema.resource('resource').links.map { |link| link.name })
|
59
59
|
end
|
60
60
|
end
|
@@ -167,6 +167,14 @@ class LinkSchemaTest < MiniTest::Unit::TestCase
|
|
167
167
|
link.format_path(['44724831-bf66-4bc2-865f-e2c4c2b14c78']))
|
168
168
|
end
|
169
169
|
|
170
|
+
# LinkSchema.format_path escapes special URL characters in parameters.
|
171
|
+
def test_format_path_with_illegal_literals
|
172
|
+
schema = Heroics::Schema.new(SAMPLE_SCHEMA)
|
173
|
+
link = schema.resource('resource').link('info')
|
174
|
+
assert_equal(['/resource/foobar%25', nil],
|
175
|
+
link.format_path(['foobar%']))
|
176
|
+
end
|
177
|
+
|
170
178
|
# LinkSchema.format_path correctly returns a parameter as a body if a path
|
171
179
|
# doesn't have any parameters.
|
172
180
|
def test_format_path_with_body
|
@@ -224,6 +232,21 @@ class LinkSchemaTest < MiniTest::Unit::TestCase
|
|
224
232
|
link = schema.resource('resource').link('identify_resource')
|
225
233
|
assert_equal('identify-resource', link.pretty_name)
|
226
234
|
end
|
235
|
+
|
236
|
+
# LinkSchema.content_type returns the media type associated with this
|
237
|
+
# resource.
|
238
|
+
def test_content_type
|
239
|
+
schema = Heroics::Schema.new(SAMPLE_SCHEMA)
|
240
|
+
link = schema.resource('resource').link('submit')
|
241
|
+
assert_equal('application/x-www-form-urlencoded', link.content_type)
|
242
|
+
end
|
243
|
+
|
244
|
+
# The content type should default to application/json
|
245
|
+
def test_default_content_type
|
246
|
+
schema = Heroics::Schema.new(SAMPLE_SCHEMA)
|
247
|
+
link = schema.resource('resource').link('identify_resource')
|
248
|
+
assert_equal('application/json', link.content_type)
|
249
|
+
end
|
227
250
|
end
|
228
251
|
|
229
252
|
class DownloadSchemaTest < MiniTest::Unit::TestCase
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: heroics
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.13
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- geemus
|
@@ -9,20 +9,20 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2015-03-09 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
16
16
|
requirement: !ruby/object:Gem::Requirement
|
17
17
|
requirements:
|
18
|
-
- - ~>
|
18
|
+
- - "~>"
|
19
19
|
- !ruby/object:Gem::Version
|
20
20
|
version: '1.3'
|
21
21
|
type: :development
|
22
22
|
prerelease: false
|
23
23
|
version_requirements: !ruby/object:Gem::Requirement
|
24
24
|
requirements:
|
25
|
-
- - ~>
|
25
|
+
- - "~>"
|
26
26
|
- !ruby/object:Gem::Version
|
27
27
|
version: '1.3'
|
28
28
|
- !ruby/object:Gem::Dependency
|
@@ -43,98 +43,98 @@ dependencies:
|
|
43
43
|
name: rake
|
44
44
|
requirement: !ruby/object:Gem::Requirement
|
45
45
|
requirements:
|
46
|
-
- -
|
46
|
+
- - ">="
|
47
47
|
- !ruby/object:Gem::Version
|
48
48
|
version: '0'
|
49
49
|
type: :development
|
50
50
|
prerelease: false
|
51
51
|
version_requirements: !ruby/object:Gem::Requirement
|
52
52
|
requirements:
|
53
|
-
- -
|
53
|
+
- - ">="
|
54
54
|
- !ruby/object:Gem::Version
|
55
55
|
version: '0'
|
56
56
|
- !ruby/object:Gem::Dependency
|
57
57
|
name: turn
|
58
58
|
requirement: !ruby/object:Gem::Requirement
|
59
59
|
requirements:
|
60
|
-
- -
|
60
|
+
- - ">="
|
61
61
|
- !ruby/object:Gem::Version
|
62
62
|
version: '0'
|
63
63
|
type: :development
|
64
64
|
prerelease: false
|
65
65
|
version_requirements: !ruby/object:Gem::Requirement
|
66
66
|
requirements:
|
67
|
-
- -
|
67
|
+
- - ">="
|
68
68
|
- !ruby/object:Gem::Version
|
69
69
|
version: '0'
|
70
70
|
- !ruby/object:Gem::Dependency
|
71
71
|
name: erubis
|
72
72
|
requirement: !ruby/object:Gem::Requirement
|
73
73
|
requirements:
|
74
|
-
- - ~>
|
74
|
+
- - "~>"
|
75
75
|
- !ruby/object:Gem::Version
|
76
76
|
version: 2.7.0
|
77
77
|
type: :runtime
|
78
78
|
prerelease: false
|
79
79
|
version_requirements: !ruby/object:Gem::Requirement
|
80
80
|
requirements:
|
81
|
-
- - ~>
|
81
|
+
- - "~>"
|
82
82
|
- !ruby/object:Gem::Version
|
83
83
|
version: 2.7.0
|
84
84
|
- !ruby/object:Gem::Dependency
|
85
85
|
name: excon
|
86
86
|
requirement: !ruby/object:Gem::Requirement
|
87
87
|
requirements:
|
88
|
-
- -
|
88
|
+
- - ">="
|
89
89
|
- !ruby/object:Gem::Version
|
90
90
|
version: '0'
|
91
91
|
type: :runtime
|
92
92
|
prerelease: false
|
93
93
|
version_requirements: !ruby/object:Gem::Requirement
|
94
94
|
requirements:
|
95
|
-
- -
|
95
|
+
- - ">="
|
96
96
|
- !ruby/object:Gem::Version
|
97
97
|
version: '0'
|
98
98
|
- !ruby/object:Gem::Dependency
|
99
99
|
name: moneta
|
100
100
|
requirement: !ruby/object:Gem::Requirement
|
101
101
|
requirements:
|
102
|
-
- -
|
102
|
+
- - ">="
|
103
103
|
- !ruby/object:Gem::Version
|
104
104
|
version: '0'
|
105
105
|
type: :runtime
|
106
106
|
prerelease: false
|
107
107
|
version_requirements: !ruby/object:Gem::Requirement
|
108
108
|
requirements:
|
109
|
-
- -
|
109
|
+
- - ">="
|
110
110
|
- !ruby/object:Gem::Version
|
111
111
|
version: '0'
|
112
112
|
- !ruby/object:Gem::Dependency
|
113
113
|
name: multi_json
|
114
114
|
requirement: !ruby/object:Gem::Requirement
|
115
115
|
requirements:
|
116
|
-
- -
|
116
|
+
- - ">="
|
117
117
|
- !ruby/object:Gem::Version
|
118
118
|
version: 1.9.2
|
119
119
|
type: :runtime
|
120
120
|
prerelease: false
|
121
121
|
version_requirements: !ruby/object:Gem::Requirement
|
122
122
|
requirements:
|
123
|
-
- -
|
123
|
+
- - ">="
|
124
124
|
- !ruby/object:Gem::Version
|
125
125
|
version: 1.9.2
|
126
126
|
- !ruby/object:Gem::Dependency
|
127
127
|
name: netrc
|
128
128
|
requirement: !ruby/object:Gem::Requirement
|
129
129
|
requirements:
|
130
|
-
- -
|
130
|
+
- - ">="
|
131
131
|
- !ruby/object:Gem::Version
|
132
132
|
version: '0'
|
133
133
|
type: :runtime
|
134
134
|
prerelease: false
|
135
135
|
version_requirements: !ruby/object:Gem::Requirement
|
136
136
|
requirements:
|
137
|
-
- -
|
137
|
+
- - ">="
|
138
138
|
- !ruby/object:Gem::Version
|
139
139
|
version: '0'
|
140
140
|
description: A Ruby client generator for HTTP APIs described with a JSON schema
|
@@ -147,8 +147,8 @@ executables:
|
|
147
147
|
extensions: []
|
148
148
|
extra_rdoc_files: []
|
149
149
|
files:
|
150
|
-
- .gitignore
|
151
|
-
- .travis.yml
|
150
|
+
- ".gitignore"
|
151
|
+
- ".travis.yml"
|
152
152
|
- CONTRIBUTING.md
|
153
153
|
- CONTRIBUTORS.md
|
154
154
|
- Gemfile
|
@@ -192,17 +192,17 @@ require_paths:
|
|
192
192
|
- lib
|
193
193
|
required_ruby_version: !ruby/object:Gem::Requirement
|
194
194
|
requirements:
|
195
|
-
- -
|
195
|
+
- - ">="
|
196
196
|
- !ruby/object:Gem::Version
|
197
197
|
version: '0'
|
198
198
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
199
199
|
requirements:
|
200
|
-
- -
|
200
|
+
- - ">="
|
201
201
|
- !ruby/object:Gem::Version
|
202
202
|
version: '0'
|
203
203
|
requirements: []
|
204
204
|
rubyforge_project:
|
205
|
-
rubygems_version: 2.
|
205
|
+
rubygems_version: 2.2.2
|
206
206
|
signing_key:
|
207
207
|
specification_version: 4
|
208
208
|
summary: A Ruby client generator for HTTP APIs described with a JSON schema
|