sinatra-schema 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Gem version](http://img.shields.io/gem/v/sinatra-schema.svg)](https://rubygems.org/gems/sinatra-schema)
|
4
|
+
[![Build Status](https://travis-ci.org/pedro/sinatra-schema.svg?branch=master)](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:
|