sinatra-schema 0.0.1 → 0.0.2
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 +8 -8
- data/README.md +103 -12
- data/lib/sinatra/schema/definition.rb +32 -1
- data/lib/sinatra/schema/dsl/definitions.rb +49 -0
- data/lib/sinatra/schema/dsl/links.rb +43 -0
- data/lib/sinatra/schema/dsl/resources.rb +30 -0
- data/lib/sinatra/schema/link.rb +9 -27
- data/lib/sinatra/schema/param_handling.rb +44 -0
- data/lib/sinatra/schema/param_validation.rb +23 -0
- data/lib/sinatra/schema/resource.rb +32 -18
- data/lib/sinatra/schema/root.rb +33 -0
- data/lib/sinatra/schema/tasks/schema.rake +5 -0
- data/lib/sinatra/schema/utils.rb +9 -3
- data/lib/sinatra/schema/version.rb +5 -0
- data/lib/sinatra/schema.rb +21 -13
- data/spec/dsl/definitions_spec.rb +41 -0
- data/spec/dsl/links_spec.rb +42 -0
- data/spec/dsl/resources_spec.rb +25 -0
- data/spec/integration_spec.rb +2 -15
- data/spec/json_schema_spec.rb +30 -0
- data/spec/param_handling_spec.rb +67 -0
- data/spec/param_validation_spec.rb +35 -0
- data/spec/resource_spec.rb +17 -0
- data/spec/spec_helper.rb +3 -2
- data/spec/support/last_json.rb +3 -0
- data/spec/support/test_app.rb +14 -20
- metadata +52 -3
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
MDY3YmZhNWJiOTc1YmI1NWU5OTk1MmVkZDg2ZjkyZTA3ZmZiMzZiOA==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
ODVkYjE3NDFiODdhMjdiOGNkYmRhYTU4NjIzMTE3Nzk4OGJjYThhZQ==
|
7
7
|
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
ZDhmNDgxZDhlY2FkODI3MDczMmJhMTE4OWFjYWZhMzM2MWQ2NTgzMWY2N2Zm
|
10
|
+
MWEyYzU5ZWI2Y2Q5ZmQ5MGFkMzMwZWEyMmNhNDcyM2RiZjU4MTA4M2VjYzgy
|
11
|
+
MDk2N2MwM2ZiNDA5MDRiYjBlMzdjODVjODM5ZWQ2MGNjZmE0ODI=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
MTZjNDNjODZhYmE1OTEyODU4MmY3MjBmZTAyZWRlYTY3OWUzYjVhMDEyM2Iw
|
14
|
+
YWY4OTE5MWU2ZGIxN2RlNzc3NDEwODllZDAwMWY2Y2VlNDdiMDAzNTA5ZmE1
|
15
|
+
NDI4NTFjNjViYWI1ZTc5MDA5NzJhNWQwMGQyZGY2Y2U3YWU0NjQ=
|
data/README.md
CHANGED
@@ -1,33 +1,124 @@
|
|
1
1
|
# Sinatra Schema
|
2
2
|
|
3
|
+
[](https://rubygems.org/gems/sinatra-schema)
|
4
|
+
[](https://travis-ci.org/pedro/sinatra-schema)
|
5
|
+
|
3
6
|
Define a schema for your Sinatra application to get requests and responses validated. Dump it schema as a JSON Schema to aid client generation and more!
|
4
7
|
|
8
|
+
**Under design**. The reference below might be outdated – please check specs to see the latest.
|
9
|
+
|
5
10
|
|
6
11
|
## Usage
|
7
12
|
|
8
|
-
Register `Sinatra::Schema`
|
13
|
+
Register `Sinatra::Schema` to define your resource like:
|
9
14
|
|
10
15
|
```ruby
|
11
16
|
class MyApi < Sinatra::Base
|
12
17
|
register Sinatra::Schema
|
13
18
|
|
14
|
-
resource("/
|
15
|
-
|
16
|
-
|
17
|
-
|
19
|
+
resource("/account") do |res|
|
20
|
+
res.property.text :email
|
21
|
+
|
22
|
+
res.link(:get) do |link|
|
23
|
+
link.action do
|
24
|
+
# note per definition above we need to serialize "email"
|
25
|
+
MultiJson.encode(email: current_user.email)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
```
|
31
|
+
|
32
|
+
### Params
|
18
33
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
34
|
+
Links can have properties too:
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
resource("/account") do |res|
|
38
|
+
res.property.text :email
|
39
|
+
|
40
|
+
res.link(:post) do |link|
|
41
|
+
link.property.ref :email # reuse the property defined above
|
42
|
+
link.property.bool :admin
|
43
|
+
|
44
|
+
link.action do |params|
|
45
|
+
user = User.new(email: params[:email])
|
46
|
+
if params[:admin] # params are casted accordingly!
|
47
|
+
# ...
|
24
48
|
end
|
25
49
|
end
|
26
50
|
end
|
51
|
+
```
|
52
|
+
|
53
|
+
### Cross-resource params
|
54
|
+
|
55
|
+
Reuse properties from other resources when appropriate:
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
resource("/artists") do |res|
|
59
|
+
res.property.text :name, description: "Artist name"
|
27
60
|
end
|
61
|
+
|
62
|
+
resource("/albums") do |res|
|
63
|
+
res.property.text :name, description: "Album name"
|
64
|
+
res.property.ref :artist_name, "artists/name"
|
65
|
+
end
|
66
|
+
```
|
67
|
+
|
68
|
+
### JSON Schema
|
69
|
+
|
70
|
+
The extension will serve a JSON Schema dump at `GET /schema` for you.
|
71
|
+
|
72
|
+
You can also include the `schema` Rake task to print it out. To do so, add to your `Rakefile`:
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
require "./app" # load your app to have the endpoints defined
|
76
|
+
load "sinatra/schema/tasks/schema.rake"
|
77
|
+
```
|
78
|
+
|
79
|
+
Then dump it like:
|
80
|
+
|
81
|
+
```
|
82
|
+
$ rake schema
|
83
|
+
{
|
84
|
+
"$schema":"http://json-schema.org/draft-04/hyper-schema",
|
85
|
+
"definitions":{
|
86
|
+
"account":{
|
87
|
+
"title":"Account",
|
88
|
+
"type":"object",
|
89
|
+
"definitions":{
|
90
|
+
"email":{
|
91
|
+
"type":"string"
|
92
|
+
}
|
93
|
+
},
|
94
|
+
"links":[
|
95
|
+
{
|
96
|
+
"href":"/account",
|
97
|
+
"method":"GET"
|
98
|
+
}
|
99
|
+
]
|
100
|
+
}
|
101
|
+
}
|
102
|
+
}
|
28
103
|
```
|
29
104
|
|
30
105
|
|
31
|
-
##
|
106
|
+
## Context
|
107
|
+
|
108
|
+
There are [lots of reasons why you should consider describe your API with a machine-readable format](http://pedro.by4am.com/past/2014/5/23/get_more_out_of_your_service_with_machinereadable_api_specs/):
|
109
|
+
|
110
|
+
- Describe what endpoints are available
|
111
|
+
- Validate requests and responses
|
112
|
+
- Embrace constraints for consistent API design
|
113
|
+
- Generate documentation
|
114
|
+
- Generate clients
|
115
|
+
- Generate service stubs
|
116
|
+
|
117
|
+
Sinatra Schema is a thin layer on top of JSON Schema, trying to bring all of the benefits without any of the JSON.
|
118
|
+
|
119
|
+
If you need more flexibility, or if you think the schema should live by itself, then you should consider writing the schema yourself. Tools like [prmd](https://github.com/interagent/prmd) can really help you get started, and [committee](https://github.com/interagent/committee) can help you get benefits out of that schema.
|
120
|
+
|
121
|
+
|
122
|
+
## Meta
|
32
123
|
|
33
|
-
|
124
|
+
Created by Pedro Belo. MIT license.
|
@@ -1,7 +1,38 @@
|
|
1
1
|
module Sinatra
|
2
2
|
module Schema
|
3
3
|
class Definition
|
4
|
-
attr_accessor :description, :example, :format, :type
|
4
|
+
attr_accessor :description, :example, :format, :id, :type
|
5
|
+
|
6
|
+
def initialize(options={})
|
7
|
+
@description = options[:description]
|
8
|
+
@example = options[:example]
|
9
|
+
@id = options[:id]
|
10
|
+
@type = options[:type]
|
11
|
+
end
|
12
|
+
|
13
|
+
def cast(value)
|
14
|
+
# do not touch nulls:
|
15
|
+
return unless value
|
16
|
+
|
17
|
+
case type
|
18
|
+
when "string"
|
19
|
+
value.to_s
|
20
|
+
when "boolean"
|
21
|
+
%w( t true 1 ).include?(value.to_s)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def valid?(value)
|
26
|
+
# always accept nils for now
|
27
|
+
return if value.nil?
|
28
|
+
|
29
|
+
case type
|
30
|
+
when "string"
|
31
|
+
value.is_a?(String)
|
32
|
+
when "boolean"
|
33
|
+
[true, false].include?(value)
|
34
|
+
end
|
35
|
+
end
|
5
36
|
end
|
6
37
|
end
|
7
38
|
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Sinatra
|
2
|
+
module Schema
|
3
|
+
module DSL
|
4
|
+
class Definitions
|
5
|
+
attr_accessor :definition, :resource, :options
|
6
|
+
|
7
|
+
def initialize(resource, options={})
|
8
|
+
@options = options
|
9
|
+
@resource = resource
|
10
|
+
end
|
11
|
+
|
12
|
+
def text(id, local_options={})
|
13
|
+
def_options = options.merge(local_options)
|
14
|
+
def_options.merge!(id: id, type: "string")
|
15
|
+
add Definition.new(def_options)
|
16
|
+
end
|
17
|
+
|
18
|
+
def bool(id, local_options={})
|
19
|
+
def_options = options.merge(local_options)
|
20
|
+
def_options.merge!(id: id, type: "boolean")
|
21
|
+
add Definition.new(def_options)
|
22
|
+
end
|
23
|
+
|
24
|
+
def ref(id)
|
25
|
+
unless definition = resource.defs[id] || Sinatra::Schema::Root.instance.find_definition(id)
|
26
|
+
raise "Unknown reference: #{id}"
|
27
|
+
end
|
28
|
+
add definition, true
|
29
|
+
end
|
30
|
+
|
31
|
+
# TODO support other types
|
32
|
+
|
33
|
+
protected
|
34
|
+
|
35
|
+
def add(definition, reference=false)
|
36
|
+
unless reference
|
37
|
+
@resource.defs[definition.id] = definition
|
38
|
+
end
|
39
|
+
if options[:serialize]
|
40
|
+
@resource.properties[definition.id] = definition
|
41
|
+
end
|
42
|
+
if link = options[:link]
|
43
|
+
link.properties[definition.id] = definition
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Sinatra
|
2
|
+
module Schema
|
3
|
+
module DSL
|
4
|
+
class Links
|
5
|
+
attr_accessor :href, :link, :method, :resource
|
6
|
+
|
7
|
+
def initialize(options)
|
8
|
+
@href = options.fetch(:href)
|
9
|
+
@method = options.fetch(:method)
|
10
|
+
@resource = options.fetch(:resource)
|
11
|
+
@link = build_link
|
12
|
+
end
|
13
|
+
|
14
|
+
def title(title)
|
15
|
+
link.title = title
|
16
|
+
end
|
17
|
+
|
18
|
+
def rel(rel)
|
19
|
+
link.rel = rel
|
20
|
+
end
|
21
|
+
|
22
|
+
def description(description)
|
23
|
+
link.description = description
|
24
|
+
end
|
25
|
+
|
26
|
+
def action(&blk)
|
27
|
+
link.action_block = blk
|
28
|
+
end
|
29
|
+
|
30
|
+
def property
|
31
|
+
DSL::Definitions.new(resource, link: link, serialize: false)
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
def build_link
|
37
|
+
full_href = "#{resource.path}/#{href.chomp("/")}".chomp("/")
|
38
|
+
Link.new(resource: resource, method: method, href: full_href)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Sinatra
|
2
|
+
module Schema
|
3
|
+
module DSL
|
4
|
+
class Resources
|
5
|
+
attr_accessor :app, :resource
|
6
|
+
|
7
|
+
def initialize(app, path)
|
8
|
+
@app = app
|
9
|
+
@resource = Resource.new(path: path)
|
10
|
+
end
|
11
|
+
|
12
|
+
def description(description)
|
13
|
+
@resource.description = description
|
14
|
+
end
|
15
|
+
|
16
|
+
def property
|
17
|
+
DSL::Definitions.new(resource, serialize: true)
|
18
|
+
end
|
19
|
+
|
20
|
+
def link(method, href="/", &blk)
|
21
|
+
dsl = DSL::Links.new(resource: resource, method: method, href: href)
|
22
|
+
blk.call(dsl)
|
23
|
+
link = dsl.link
|
24
|
+
link.register(app)
|
25
|
+
resource.links << link
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/sinatra/schema/link.rb
CHANGED
@@ -1,47 +1,29 @@
|
|
1
1
|
module Sinatra
|
2
2
|
module Schema
|
3
3
|
class Link
|
4
|
-
attr_accessor :resource, :title, :description, :href, :method, :properties, :rel
|
4
|
+
attr_accessor :action_block, :resource, :title, :description, :href, :method, :properties, :rel
|
5
5
|
|
6
6
|
def initialize(options)
|
7
|
-
@resource
|
8
|
-
@method
|
9
|
-
@href
|
10
|
-
|
11
|
-
|
12
|
-
def action(&blk)
|
13
|
-
@action = blk
|
14
|
-
end
|
15
|
-
|
16
|
-
def action_block
|
17
|
-
@action
|
7
|
+
@resource = options[:resource]
|
8
|
+
@method = options[:method]
|
9
|
+
@href = options[:href]
|
10
|
+
@properties = {}
|
18
11
|
end
|
19
12
|
|
20
13
|
def register(app)
|
21
14
|
link = self
|
22
15
|
app.send(method.downcase, href) do
|
23
16
|
begin
|
24
|
-
link.
|
25
|
-
|
17
|
+
schema_params = parse_params(link.properties)
|
18
|
+
validate_params!(schema_params, link.properties)
|
19
|
+
res = link.action_block.call(schema_params)
|
26
20
|
link.resource.validate_response!(res)
|
27
|
-
|
21
|
+
res
|
28
22
|
rescue RuntimeError => e
|
29
23
|
halt(400)
|
30
24
|
end
|
31
25
|
end
|
32
26
|
end
|
33
|
-
|
34
|
-
def validate_params!(params)
|
35
|
-
unless properties
|
36
|
-
if params.empty?
|
37
|
-
return
|
38
|
-
else
|
39
|
-
raise "Did not expect params"
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
Utils.validate_keys!(properties, params)
|
44
|
-
end
|
45
27
|
end
|
46
28
|
end
|
47
29
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Sinatra
|
2
|
+
module Schema
|
3
|
+
module ParamHandling
|
4
|
+
def parse_params(properties)
|
5
|
+
case request.media_type
|
6
|
+
when nil, "application/json"
|
7
|
+
parse_json_params
|
8
|
+
when "application/x-www-form-urlencoded"
|
9
|
+
cast_regular_params(properties)
|
10
|
+
else
|
11
|
+
raise "Cannot handle media type #{request.media_type}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def parse_json_params
|
18
|
+
body = request.body.read
|
19
|
+
return {} if body.length == 0 # nothing supplied
|
20
|
+
|
21
|
+
request.body.rewind # leave it ready for other calls
|
22
|
+
supplied_params = MultiJson.decode(body)
|
23
|
+
unless supplied_params.is_a?(Hash)
|
24
|
+
raise "Invalid request, expecting a hash"
|
25
|
+
end
|
26
|
+
|
27
|
+
indifferent_params(supplied_params)
|
28
|
+
rescue MultiJson::ParseError
|
29
|
+
raise "Invalid JSON"
|
30
|
+
end
|
31
|
+
|
32
|
+
def cast_regular_params(properties)
|
33
|
+
casted_params = params.inject({}) do |casted, (k, v)|
|
34
|
+
definition = properties[k.to_sym]
|
35
|
+
# if there's no definition just leave the original param,
|
36
|
+
# let the validation raise on this later:
|
37
|
+
casted[k] = definition ? definition.cast(v) : v
|
38
|
+
casted
|
39
|
+
end
|
40
|
+
indifferent_params(casted_params)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Sinatra
|
2
|
+
module Schema
|
3
|
+
module ParamValidation
|
4
|
+
def validate_params!(params, properties)
|
5
|
+
missing = properties.keys.map(&:to_s).sort - params.keys.map(&:to_s).sort
|
6
|
+
unless missing.empty?
|
7
|
+
raise "Missing properties: #{missing}"
|
8
|
+
end
|
9
|
+
|
10
|
+
extra = params.keys.map(&:to_s).sort - properties.keys.map(&:to_s).sort
|
11
|
+
unless extra.empty?
|
12
|
+
raise "Unexpected properties: #{extra}"
|
13
|
+
end
|
14
|
+
|
15
|
+
properties.each do |id, definition|
|
16
|
+
unless definition.valid?(params[id])
|
17
|
+
raise "Bad param: #{id}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -1,34 +1,33 @@
|
|
1
1
|
module Sinatra
|
2
2
|
module Schema
|
3
3
|
class Resource
|
4
|
-
attr_accessor :id, :path, :title, :description, :properties
|
4
|
+
attr_accessor :id, :path, :title, :defs, :links, :description, :properties
|
5
5
|
|
6
|
-
def initialize(
|
7
|
-
@
|
8
|
-
@
|
9
|
-
@
|
10
|
-
@
|
6
|
+
def initialize(options)
|
7
|
+
@path = options.fetch(:path).chomp("/")
|
8
|
+
@links = []
|
9
|
+
@defs = {}
|
10
|
+
@properties = {}
|
11
11
|
end
|
12
12
|
|
13
|
-
def
|
14
|
-
@
|
15
|
-
yield @defs[id]
|
13
|
+
def id
|
14
|
+
@id ||= ActiveSupport::Inflector.singularize(path.split("/").last).to_sym
|
16
15
|
end
|
17
16
|
|
18
|
-
def
|
19
|
-
|
20
|
-
link = Link.new(resource: self, method: method, href: href)
|
21
|
-
yield(link)
|
22
|
-
link.register(@app)
|
23
|
-
@links << link
|
17
|
+
def title
|
18
|
+
@title ||= ActiveSupport::Inflector.singularize(path.split("/").last).capitalize
|
24
19
|
end
|
25
20
|
|
26
|
-
def validate_response!(
|
21
|
+
def validate_response!(raw)
|
22
|
+
# only validate responses in tests
|
23
|
+
return unless ENV["RACK_ENV"] == "test"
|
24
|
+
|
25
|
+
res = MultiJson.decode(raw)
|
27
26
|
unless res.is_a?(Hash)
|
28
27
|
raise "Response should return a hash"
|
29
28
|
end
|
30
29
|
|
31
|
-
|
30
|
+
unless properties.empty?
|
32
31
|
Utils.validate_keys!(properties, res)
|
33
32
|
end
|
34
33
|
end
|
@@ -36,7 +35,22 @@ module Sinatra
|
|
36
35
|
def to_schema
|
37
36
|
{
|
38
37
|
title: title,
|
39
|
-
description: description
|
38
|
+
description: description,
|
39
|
+
type: "object",
|
40
|
+
definitions: defs.inject({}) { |h, (id, definition)|
|
41
|
+
h[id] = {
|
42
|
+
description: definition.description,
|
43
|
+
type: definition.type,
|
44
|
+
}
|
45
|
+
h
|
46
|
+
},
|
47
|
+
links: links.map { |link|
|
48
|
+
{
|
49
|
+
description: link.description,
|
50
|
+
href: link.href,
|
51
|
+
method: link.method.to_s.upcase,
|
52
|
+
}
|
53
|
+
}
|
40
54
|
}
|
41
55
|
end
|
42
56
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Sinatra
|
2
|
+
module Schema
|
3
|
+
class Root
|
4
|
+
include Singleton
|
5
|
+
|
6
|
+
attr_accessor :resources
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@resources = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_resource(res)
|
13
|
+
@resources[res.id] = res
|
14
|
+
end
|
15
|
+
|
16
|
+
def find_definition(id)
|
17
|
+
resource_id, def_id = id.to_s.split("/", 2)
|
18
|
+
return unless resource = resources[resource_id.to_sym]
|
19
|
+
resource.defs[def_id.to_sym]
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_schema
|
23
|
+
{
|
24
|
+
"$schema" => "http://json-schema.org/draft-04/hyper-schema",
|
25
|
+
"definitions" => resources.inject({}) { |result, (id, resource)|
|
26
|
+
result[id] = resource.to_schema
|
27
|
+
result
|
28
|
+
}
|
29
|
+
}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/sinatra/schema/utils.rb
CHANGED
@@ -1,16 +1,22 @@
|
|
1
1
|
module Sinatra
|
2
2
|
module Schema
|
3
3
|
class Utils
|
4
|
-
def self.validate_keys!(
|
5
|
-
missing =
|
4
|
+
def self.validate_keys!(properties, received)
|
5
|
+
missing = properties.keys.map(&:to_s).sort - received.keys.map(&:to_s).sort
|
6
6
|
unless missing.empty?
|
7
7
|
raise "Missing properties: #{missing}"
|
8
8
|
end
|
9
9
|
|
10
|
-
extra = received.keys.map(&:to_s).sort -
|
10
|
+
extra = received.keys.map(&:to_s).sort - properties.keys.map(&:to_s).sort
|
11
11
|
unless extra.empty?
|
12
12
|
raise "Unexpected properties: #{extra}"
|
13
13
|
end
|
14
|
+
|
15
|
+
properties.each do |id, definition|
|
16
|
+
unless definition.valid?(received[id.to_s])
|
17
|
+
raise "Bad response property: #{id}"
|
18
|
+
end
|
19
|
+
end
|
14
20
|
end
|
15
21
|
end
|
16
22
|
end
|
data/lib/sinatra/schema.rb
CHANGED
@@ -1,32 +1,40 @@
|
|
1
|
+
require "active_support/inflector"
|
2
|
+
require "sinatra/base"
|
3
|
+
require "singleton"
|
4
|
+
require "multi_json"
|
5
|
+
|
1
6
|
require "sinatra/schema/definition"
|
2
7
|
require "sinatra/schema/link"
|
8
|
+
require "sinatra/schema/param_handling"
|
9
|
+
require "sinatra/schema/param_validation"
|
3
10
|
require "sinatra/schema/resource"
|
11
|
+
require "sinatra/schema/root"
|
4
12
|
require "sinatra/schema/utils"
|
13
|
+
require "sinatra/schema/dsl/definitions"
|
14
|
+
require "sinatra/schema/dsl/links"
|
15
|
+
require "sinatra/schema/dsl/resources"
|
5
16
|
|
6
17
|
module Sinatra
|
7
18
|
module Schema
|
8
19
|
def self.registered(app)
|
20
|
+
app.helpers ParamHandling
|
21
|
+
app.helpers ParamValidation
|
9
22
|
app.get "/schema" do
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
result[id] = resource.to_schema
|
14
|
-
result
|
15
|
-
}
|
16
|
-
)
|
23
|
+
content_type("application/schema+json")
|
24
|
+
response.headers["Cache-Control"] = "public, max-age=3600"
|
25
|
+
MultiJson.encode(app.schema_root.to_schema, pretty: true)
|
17
26
|
end
|
18
27
|
end
|
19
28
|
|
20
|
-
def
|
21
|
-
|
29
|
+
def schema_root
|
30
|
+
Root.instance
|
22
31
|
end
|
23
32
|
|
24
33
|
def resource(path)
|
25
|
-
|
26
|
-
yield(
|
27
|
-
|
34
|
+
spec = DSL::Resources.new(self, path)
|
35
|
+
yield(spec)
|
36
|
+
schema_root.add_resource(spec.resource)
|
28
37
|
end
|
29
|
-
|
30
38
|
end
|
31
39
|
|
32
40
|
register Schema
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Sinatra::Schema::DSL::Definitions do
|
4
|
+
let(:resource) { Sinatra::Schema::Resource.new(path: "/foobar") }
|
5
|
+
let(:dsl) { described_class.new(resource, options) }
|
6
|
+
let(:options) { Hash.new }
|
7
|
+
let(:root) { Sinatra::Schema::Root.instance }
|
8
|
+
|
9
|
+
it "adds a string definition to the resource" do
|
10
|
+
dsl.text(:foobar)
|
11
|
+
assert_equal 1, resource.defs.size
|
12
|
+
assert_equal "string", resource.defs[:foobar].type
|
13
|
+
end
|
14
|
+
|
15
|
+
it "adds a boolean definition to the resource" do
|
16
|
+
dsl.bool(:foobar)
|
17
|
+
assert_equal 1, resource.defs.size
|
18
|
+
assert_equal "boolean", resource.defs[:foobar].type
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "#ref" do
|
22
|
+
let(:definition) { Sinatra::Schema::Definition.new }
|
23
|
+
before { options[:serialize] = true }
|
24
|
+
|
25
|
+
it "adds a reference to another definition in the resource" do
|
26
|
+
resource.defs[:foobar] = definition
|
27
|
+
dsl.ref :foobar
|
28
|
+
assert_equal 1, resource.defs.size
|
29
|
+
assert_equal 1, resource.properties.size
|
30
|
+
end
|
31
|
+
|
32
|
+
it "adds a reference to a definition in a different resource" do
|
33
|
+
other = Sinatra::Schema::Resource.new(path: "/others")
|
34
|
+
root.add_resource(other)
|
35
|
+
other.defs[:foobar] = definition
|
36
|
+
dsl.ref "other/foobar"
|
37
|
+
assert_equal 1, other.defs.size
|
38
|
+
assert_equal 1, resource.properties.size
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Sinatra::Schema::DSL::Links do
|
4
|
+
let(:resource) { Sinatra::Schema::Resource.new(path: "/foobar") }
|
5
|
+
let(:dsl) { described_class.new(options) }
|
6
|
+
let(:options) {{ resource: resource, method: :get, href: "/" }}
|
7
|
+
|
8
|
+
it "sets the link title" do
|
9
|
+
dsl.title("foo")
|
10
|
+
assert_equal "foo", dsl.link.title
|
11
|
+
end
|
12
|
+
|
13
|
+
it "sets the link rel" do
|
14
|
+
dsl.rel("foo")
|
15
|
+
assert_equal "foo", dsl.link.rel
|
16
|
+
end
|
17
|
+
|
18
|
+
it "sets the link description" do
|
19
|
+
dsl.description("Create foo")
|
20
|
+
assert_equal "Create foo", dsl.link.description
|
21
|
+
end
|
22
|
+
|
23
|
+
it "sets the link action_block" do
|
24
|
+
action = lambda { :foo }
|
25
|
+
dsl.action(&action)
|
26
|
+
assert_equal action, dsl.link.action_block
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#property" do
|
30
|
+
it "adds new definiitons to the resource" do
|
31
|
+
dsl.property.text :foo
|
32
|
+
assert_equal 1, resource.defs.size
|
33
|
+
assert resource.defs.has_key?(:foo)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "makes them a property of the link" do
|
37
|
+
dsl.property.text :foo
|
38
|
+
assert_equal 1, dsl.link.properties.size
|
39
|
+
assert dsl.link.properties.has_key?(:foo)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Sinatra::Schema::DSL::Resources do
|
4
|
+
let(:app) { Sinatra::Application.new }
|
5
|
+
let(:dsl) { described_class.new(app, "/foobar") }
|
6
|
+
|
7
|
+
it "sets the resource description" do
|
8
|
+
dsl.description("This is a foobar")
|
9
|
+
assert_equal "This is a foobar", dsl.resource.description
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "#property" do
|
13
|
+
it "adds new definitions to the resource" do
|
14
|
+
dsl.property.text :foo
|
15
|
+
assert_equal 1, dsl.resource.defs.size
|
16
|
+
assert dsl.resource.defs.has_key?(:foo)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "makes them a property of the resource" do
|
20
|
+
dsl.property.text :foo
|
21
|
+
assert_equal 1, dsl.resource.properties.size
|
22
|
+
assert dsl.resource.properties.has_key?(:foo)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/spec/integration_spec.rb
CHANGED
@@ -7,14 +7,14 @@ describe Sinatra::Schema do
|
|
7
7
|
assert_equal "hi", last_response.body
|
8
8
|
end
|
9
9
|
|
10
|
-
it "support resource
|
10
|
+
it "support resource gets" do
|
11
11
|
get "/accounts"
|
12
12
|
assert_equal 200, last_response.status
|
13
13
|
assert_equal({ "email" => "foo@bar.com" },
|
14
14
|
MultiJson.decode(last_response.body))
|
15
15
|
end
|
16
16
|
|
17
|
-
it "support resource
|
17
|
+
it "support resource posts" do
|
18
18
|
post "/accounts", email: "omg"
|
19
19
|
assert_equal 200, last_response.status
|
20
20
|
assert_equal({ "email" => "omg" },
|
@@ -25,17 +25,4 @@ describe Sinatra::Schema do
|
|
25
25
|
post "/accounts", foo: "bar"
|
26
26
|
assert_equal 400, last_response.status
|
27
27
|
end
|
28
|
-
|
29
|
-
it "exposes the json schema" do
|
30
|
-
get "/schema"
|
31
|
-
assert_equal 200, last_response.status
|
32
|
-
schema = MultiJson.decode(last_response.body)
|
33
|
-
assert_equal "http://json-schema.org/draft-04/hyper-schema",
|
34
|
-
schema["$schema"]
|
35
|
-
assert_equal Hash, schema["definitions"].class
|
36
|
-
assert_equal Hash, schema["definitions"]["account"].class
|
37
|
-
assert_equal "Account", schema["definitions"]["account"]["title"]
|
38
|
-
assert_equal "An account represents an individual signed up to use the service",
|
39
|
-
schema["definitions"]["account"]["description"]
|
40
|
-
end
|
41
28
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe "JSON Schema" do
|
4
|
+
before do
|
5
|
+
get "/schema"
|
6
|
+
@schema = MultiJson.decode(last_response.body)
|
7
|
+
end
|
8
|
+
|
9
|
+
it "renders a 200" do
|
10
|
+
assert_equal 200, last_response.status
|
11
|
+
end
|
12
|
+
|
13
|
+
it "sets the appropriate content-type" do
|
14
|
+
assert_equal "application/schema+json",
|
15
|
+
last_response.headers["Content-Type"]
|
16
|
+
end
|
17
|
+
|
18
|
+
it "sets $schema to hyper-schema" do
|
19
|
+
assert_equal "http://json-schema.org/draft-04/hyper-schema",
|
20
|
+
@schema["$schema"]
|
21
|
+
end
|
22
|
+
|
23
|
+
it "generates definitions" do
|
24
|
+
assert_equal Hash, @schema["definitions"].class
|
25
|
+
assert_equal Hash, @schema["definitions"]["account"].class
|
26
|
+
assert_equal "Account", @schema["definitions"]["account"]["title"]
|
27
|
+
assert_equal "An account represents an individual signed up to use the service",
|
28
|
+
@schema["definitions"]["account"]["description"]
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Sinatra::Schema::ParamHandling do
|
4
|
+
before do
|
5
|
+
@rack_app = Sinatra.new do
|
6
|
+
helpers Sinatra::Schema::ParamHandling
|
7
|
+
post("/") do
|
8
|
+
MultiJson.encode(parse_params($properties))
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "JSON encoded request params" do
|
14
|
+
before do
|
15
|
+
header "Content-Type", "application/json"
|
16
|
+
end
|
17
|
+
|
18
|
+
it "handles invalid json" do
|
19
|
+
assert_raises(RuntimeError, "Invalid JSON") do
|
20
|
+
post "/", "{"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
it "considers an empty body an empty hash" do
|
25
|
+
post "/", ""
|
26
|
+
assert_equal Hash.new, last_json
|
27
|
+
end
|
28
|
+
|
29
|
+
it "just parses the json" do
|
30
|
+
params = { "foo" => "bar", "baz" => 42 }
|
31
|
+
post "/", MultiJson.encode(params)
|
32
|
+
assert_equal params, last_json
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe "form-encoded params" do
|
37
|
+
it "casts values according to the schema" do
|
38
|
+
$properties = {
|
39
|
+
some_text: Sinatra::Schema::Definition.new(type: "string"),
|
40
|
+
some_bool: Sinatra::Schema::Definition.new(type: "boolean"),
|
41
|
+
}
|
42
|
+
post "/", some_text: "true", some_bool: "true"
|
43
|
+
assert_equal({ "some_text" => "true", "some_bool" => true }, last_json)
|
44
|
+
end
|
45
|
+
|
46
|
+
it "leaves params without the corresponding property untouched" do
|
47
|
+
$properies = {}
|
48
|
+
params = { "foo" => "bar" }
|
49
|
+
post "/", params
|
50
|
+
assert_equal params, last_json
|
51
|
+
end
|
52
|
+
|
53
|
+
it "leaves null params" do
|
54
|
+
$properties = { bool: Sinatra::Schema::Definition.new(type: "boolean") }
|
55
|
+
params = { "bool" => nil }
|
56
|
+
post "/", params
|
57
|
+
assert_equal params, last_json
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
it "errors out on other formats" do
|
62
|
+
assert_raises(RuntimeError, "Cannot handle media type application/xml") do
|
63
|
+
header "Content-Type", "application/xml"
|
64
|
+
post "/", "<omg />"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Sinatra::Schema::ParamValidation do
|
4
|
+
before do
|
5
|
+
@rack_app = Sinatra.new do
|
6
|
+
helpers Sinatra::Schema::ParamValidation
|
7
|
+
post("/") do
|
8
|
+
params = MultiJson.decode(request.body.read)
|
9
|
+
validate_params!(indifferent_params(params), $properties)
|
10
|
+
"ok"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
it "errors out on unexpected params" do
|
16
|
+
$properties = {}
|
17
|
+
assert_raises(RuntimeError, "omg just realized this doesn't do shit") do
|
18
|
+
post "/", MultiJson.encode(foo: "bar")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
it "errors out on missing params" do
|
23
|
+
$properties = { foo: Sinatra::Schema::Definition.new(type: "string") }
|
24
|
+
assert_raises(RuntimeError) do
|
25
|
+
post "/", "{}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
it "errors out on wrong format" do
|
30
|
+
$properties = { bool: Sinatra::Schema::Definition.new(type: "boolean") }
|
31
|
+
assert_raises(RuntimeError) do
|
32
|
+
post "/", MultiJson.encode(bool: "omg")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Sinatra::Schema::Resource do
|
4
|
+
let(:resource) { Sinatra::Schema::Resource.new(path: "/artists") }
|
5
|
+
|
6
|
+
describe "#id" do
|
7
|
+
it "is inferred from the path" do
|
8
|
+
assert_equal :artist, resource.id
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "#title" do
|
13
|
+
it "is inferred from the path" do
|
14
|
+
assert_equal "Artist", resource.title
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -3,8 +3,9 @@ ENV["RACK_ENV"] = "test"
|
|
3
3
|
require "rubygems"
|
4
4
|
require "bundler"
|
5
5
|
|
6
|
-
Bundler.require
|
6
|
+
Bundler.require
|
7
7
|
|
8
|
+
require "rack/test"
|
8
9
|
require "sinatra"
|
9
10
|
require "sinatra/schema"
|
10
11
|
|
@@ -15,6 +16,6 @@ RSpec.configure do |config|
|
|
15
16
|
config.expect_with :minitest
|
16
17
|
|
17
18
|
def app
|
18
|
-
TestApp
|
19
|
+
@rack_app || TestApp
|
19
20
|
end
|
20
21
|
end
|
data/spec/support/test_app.rb
CHANGED
@@ -6,35 +6,29 @@ class TestApp < Sinatra::Base
|
|
6
6
|
end
|
7
7
|
|
8
8
|
resource("/accounts") do |res|
|
9
|
-
res.
|
10
|
-
res.title = "Account"
|
11
|
-
res.description = "An account represents an individual signed up to use the service"
|
9
|
+
res.description "An account represents an individual signed up to use the service"
|
12
10
|
|
13
|
-
res.
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
d.type = :string
|
18
|
-
end
|
19
|
-
|
20
|
-
res.properties = [:email]
|
11
|
+
res.property.text :email,
|
12
|
+
description: "unique email address of account",
|
13
|
+
example: "username@example.com",
|
14
|
+
format: "email"
|
21
15
|
|
22
16
|
res.link(:get) do |link|
|
23
|
-
link.title
|
24
|
-
link.rel
|
25
|
-
link.description
|
17
|
+
link.title "Info"
|
18
|
+
link.rel "self"
|
19
|
+
link.description "Info for account"
|
26
20
|
link.action do
|
27
|
-
|
21
|
+
MultiJson.encode(email: "foo@bar.com")
|
28
22
|
end
|
29
23
|
end
|
30
24
|
|
31
25
|
res.link(:post) do |link|
|
32
|
-
link.title
|
33
|
-
link.rel
|
34
|
-
link.description
|
35
|
-
link.
|
26
|
+
link.title "Create"
|
27
|
+
link.rel "create"
|
28
|
+
link.description "Create a new account"
|
29
|
+
link.property.ref :email
|
36
30
|
link.action do |params|
|
37
|
-
|
31
|
+
MultiJson.encode(email: params[:email])
|
38
32
|
end
|
39
33
|
end
|
40
34
|
end
|
metadata
CHANGED
@@ -1,15 +1,35 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sinatra-schema
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Pedro Belo
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-12-
|
11
|
+
date: 2014-12-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.0'
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '4.0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '4.0'
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '4.0'
|
13
33
|
- !ruby/object:Gem::Dependency
|
14
34
|
name: multi_json
|
15
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -70,6 +90,20 @@ dependencies:
|
|
70
90
|
- - ! '>='
|
71
91
|
- !ruby/object:Gem::Version
|
72
92
|
version: 0.6.2
|
93
|
+
- !ruby/object:Gem::Dependency
|
94
|
+
name: rake
|
95
|
+
requirement: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - ! '>'
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '0'
|
100
|
+
type: :development
|
101
|
+
prerelease: false
|
102
|
+
version_requirements: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - ! '>'
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '0'
|
73
107
|
- !ruby/object:Gem::Dependency
|
74
108
|
name: rspec
|
75
109
|
requirement: !ruby/object:Gem::Requirement
|
@@ -101,11 +135,27 @@ files:
|
|
101
135
|
- README.md
|
102
136
|
- lib/sinatra/schema.rb
|
103
137
|
- lib/sinatra/schema/definition.rb
|
138
|
+
- lib/sinatra/schema/dsl/definitions.rb
|
139
|
+
- lib/sinatra/schema/dsl/links.rb
|
140
|
+
- lib/sinatra/schema/dsl/resources.rb
|
104
141
|
- lib/sinatra/schema/link.rb
|
142
|
+
- lib/sinatra/schema/param_handling.rb
|
143
|
+
- lib/sinatra/schema/param_validation.rb
|
105
144
|
- lib/sinatra/schema/resource.rb
|
145
|
+
- lib/sinatra/schema/root.rb
|
146
|
+
- lib/sinatra/schema/tasks/schema.rake
|
106
147
|
- lib/sinatra/schema/utils.rb
|
148
|
+
- lib/sinatra/schema/version.rb
|
149
|
+
- spec/dsl/definitions_spec.rb
|
150
|
+
- spec/dsl/links_spec.rb
|
151
|
+
- spec/dsl/resources_spec.rb
|
107
152
|
- spec/integration_spec.rb
|
153
|
+
- spec/json_schema_spec.rb
|
154
|
+
- spec/param_handling_spec.rb
|
155
|
+
- spec/param_validation_spec.rb
|
156
|
+
- spec/resource_spec.rb
|
108
157
|
- spec/spec_helper.rb
|
158
|
+
- spec/support/last_json.rb
|
109
159
|
- spec/support/test_app.rb
|
110
160
|
homepage: https://github.com/pedro/sinatra-schema
|
111
161
|
licenses:
|
@@ -132,4 +182,3 @@ signing_key:
|
|
132
182
|
specification_version: 4
|
133
183
|
summary: Sinatra extension to support schemas
|
134
184
|
test_files: []
|
135
|
-
has_rdoc:
|