rack-json_schema 1.0.0
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 +7 -0
- data/.gitignore +17 -0
- data/CHANGELOG.md +52 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +200 -0
- data/Rakefile +1 -0
- data/bin/specup +30 -0
- data/lib/rack-json_schema.rb +1 -0
- data/lib/rack/json_schema.rb +15 -0
- data/lib/rack/json_schema/base_request_handler.rb +64 -0
- data/lib/rack/json_schema/docs.rb +38 -0
- data/lib/rack/json_schema/error.rb +30 -0
- data/lib/rack/json_schema/error_handler.rb +19 -0
- data/lib/rack/json_schema/mock.rb +93 -0
- data/lib/rack/json_schema/request_validation.rb +184 -0
- data/lib/rack/json_schema/response_validation.rb +120 -0
- data/lib/rack/json_schema/schema.rb +55 -0
- data/lib/rack/json_schema/version.rb +5 -0
- data/rack-json_schema.gemspec +31 -0
- data/spec/fixtures/schema.json +152 -0
- data/spec/rack/spec/docs_spec.rb +93 -0
- data/spec/rack/spec/mock_spec.rb +126 -0
- data/spec/rack/spec_spec.rb +221 -0
- data/spec/spec_helper.rb +11 -0
- metadata +229 -0
@@ -0,0 +1,55 @@
|
|
1
|
+
module Rack
|
2
|
+
module JsonSchema
|
3
|
+
|
4
|
+
# Utility wrapper class for JsonSchema::Schema
|
5
|
+
class Schema
|
6
|
+
# Recursively extracts all links in given JSON schema
|
7
|
+
# @param json_schema [JsonSchema::Schema]
|
8
|
+
# @return [Array] An array of JsonSchema::Schema::Link
|
9
|
+
def self.extract_links(json_schema)
|
10
|
+
links = json_schema.links.select {|link| link.method && link.href }
|
11
|
+
links + json_schema.properties.map {|key, schema| extract_links(schema) }.flatten
|
12
|
+
end
|
13
|
+
|
14
|
+
# @param schema [Hash]
|
15
|
+
# @raise [JsonSchema::SchemaError]
|
16
|
+
# @example
|
17
|
+
# hash = JSON.parse("schema.json")
|
18
|
+
# schema = Rack::JsonSchema::Schema.new(hash)
|
19
|
+
def initialize(schema)
|
20
|
+
@json_schema = ::JsonSchema.parse!(schema).tap(&:expand_references!)
|
21
|
+
end
|
22
|
+
|
23
|
+
# @param method [String] Uppercase HTTP method name (e.g. GET, POST)
|
24
|
+
# @param path [String] Path string, which may include URI template
|
25
|
+
# @return [JsonSchema::Scheam::Link, nil] Link defined for the given method and path
|
26
|
+
# @example
|
27
|
+
# schema.has_link_for?(method: "GET", path: "/recipes/{+id}") #=> nil
|
28
|
+
def link_for(method: nil, path: nil)
|
29
|
+
links_indexed_by_method[method].find do |link|
|
30
|
+
%r<^#{link.href.gsub(/\{(.*?)\}/, "[^/]+")}$> === path
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# @return [Array] All links defined in given JSON schema
|
35
|
+
# @example
|
36
|
+
# schema.links #=> [#<JsonSchema::Schema::Link>]
|
37
|
+
def links
|
38
|
+
@links ||= self.class.extract_links(@json_schema)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
# @return [Hash] A key-value pair of HTTP method and an Array of links
|
44
|
+
# @note This Hash always returns an Array for any key
|
45
|
+
# @example
|
46
|
+
# schema.links_indexed_by_method #=> { "GET" => [#<JsonSchema::Schema::Link>] }
|
47
|
+
def links_indexed_by_method
|
48
|
+
@links_indexed_by_method ||= links.inject(Hash.new {|hash, key| hash[key] = [] }) do |result, link|
|
49
|
+
result[link.method.to_s.upcase] << link
|
50
|
+
result
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
lib = File.expand_path("../lib", __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require "rack/json_schema/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "rack-json_schema"
|
7
|
+
spec.version = Rack::JsonSchema::VERSION
|
8
|
+
spec.authors = ["Ryo Nakamura"]
|
9
|
+
spec.email = ["r7kamura@gmail.com"]
|
10
|
+
spec.summary = "JSON Schema based Rack middlewares"
|
11
|
+
|
12
|
+
spec.homepage = "https://github.com/r7kamura/rack-json_schema"
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0")
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_dependency "jdoc", ">= 0.0.3"
|
21
|
+
spec.add_dependency "json_schema"
|
22
|
+
spec.add_dependency "multi_json"
|
23
|
+
spec.add_dependency "rack"
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.5"
|
25
|
+
spec.add_development_dependency "pry"
|
26
|
+
spec.add_development_dependency "rack-test"
|
27
|
+
spec.add_development_dependency "rake"
|
28
|
+
spec.add_development_dependency "rspec", "2.14.1"
|
29
|
+
spec.add_development_dependency "rspec-console"
|
30
|
+
spec.add_development_dependency "rspec-json_matcher"
|
31
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
{
|
2
|
+
"$schema": "http://json-schema.org/draft-04/hyper-schema",
|
3
|
+
"definitions": {
|
4
|
+
"app": {
|
5
|
+
"$schema": "http://json-schema.org/draft-04/hyper-schema",
|
6
|
+
"description": "An app is a program to be deployed.",
|
7
|
+
"id": "schemata/app",
|
8
|
+
"title": "App",
|
9
|
+
"type": [
|
10
|
+
"object"
|
11
|
+
],
|
12
|
+
"required": ["id"],
|
13
|
+
"definitions": {
|
14
|
+
"id": {
|
15
|
+
"description": "unique identifier of app",
|
16
|
+
"example": "01234567-89ab-cdef-0123-456789abcdef",
|
17
|
+
"format": "uuid",
|
18
|
+
"readOnly": true,
|
19
|
+
"type": [
|
20
|
+
"string"
|
21
|
+
]
|
22
|
+
},
|
23
|
+
"name": {
|
24
|
+
"description": "unique name of app",
|
25
|
+
"example": "example",
|
26
|
+
"pattern": "^[a-z][a-z0-9-]{3,50}$",
|
27
|
+
"readOnly": false,
|
28
|
+
"type": [
|
29
|
+
"string"
|
30
|
+
]
|
31
|
+
}
|
32
|
+
},
|
33
|
+
"links": [
|
34
|
+
{
|
35
|
+
"description": "Create a new app.",
|
36
|
+
"href": "/apps",
|
37
|
+
"method": "POST",
|
38
|
+
"rel": "create",
|
39
|
+
"schema": {
|
40
|
+
"properties": {
|
41
|
+
"name": {
|
42
|
+
"$ref": "#/definitions/app/definitions/name"
|
43
|
+
}
|
44
|
+
},
|
45
|
+
"type": [
|
46
|
+
"object"
|
47
|
+
]
|
48
|
+
},
|
49
|
+
"title": "Create"
|
50
|
+
},
|
51
|
+
{
|
52
|
+
"description": "Delete an existing app.",
|
53
|
+
"href": "/apps/{(%23%2Fdefinitions%2Fapp%2Fdefinitions%2Fid)}",
|
54
|
+
"method": "DELETE",
|
55
|
+
"rel": "destroy",
|
56
|
+
"title": "Delete"
|
57
|
+
},
|
58
|
+
{
|
59
|
+
"description": "Info for existing app.",
|
60
|
+
"href": "/apps/{(%23%2Fdefinitions%2Fapp%2Fdefinitions%2Fid)}",
|
61
|
+
"method": "GET",
|
62
|
+
"rel": "self",
|
63
|
+
"title": "Info"
|
64
|
+
},
|
65
|
+
{
|
66
|
+
"description": "List existing apps.",
|
67
|
+
"href": "/apps",
|
68
|
+
"method": "GET",
|
69
|
+
"rel": "instances",
|
70
|
+
"title": "List"
|
71
|
+
},
|
72
|
+
{
|
73
|
+
"description": "Update an existing app.",
|
74
|
+
"href": "/apps/{(%23%2Fdefinitions%2Fapp%2Fdefinitions%2Fid)}",
|
75
|
+
"method": "PATCH",
|
76
|
+
"rel": "update",
|
77
|
+
"schema": {
|
78
|
+
"properties": {
|
79
|
+
"name": {
|
80
|
+
"$ref": "#/definitions/app/definitions/name"
|
81
|
+
}
|
82
|
+
},
|
83
|
+
"type": [
|
84
|
+
"object"
|
85
|
+
]
|
86
|
+
},
|
87
|
+
"title": "Update"
|
88
|
+
}
|
89
|
+
],
|
90
|
+
"properties": {
|
91
|
+
"id": {
|
92
|
+
"$ref": "#/definitions/app/definitions/id"
|
93
|
+
},
|
94
|
+
"name": {
|
95
|
+
"$ref": "#/definitions/app/definitions/name"
|
96
|
+
}
|
97
|
+
}
|
98
|
+
},
|
99
|
+
"recipe": {
|
100
|
+
"$schema": "http://json-schema.org/draft-04/hyper-schema",
|
101
|
+
"description": "cooking Recipe",
|
102
|
+
"title": "Recipe",
|
103
|
+
"type": [
|
104
|
+
"object"
|
105
|
+
],
|
106
|
+
"definitions": {
|
107
|
+
"id": {
|
108
|
+
"description": "unique identifier of recipe",
|
109
|
+
"format": "uuid",
|
110
|
+
"readOnly": true,
|
111
|
+
"example": 1,
|
112
|
+
"type": [
|
113
|
+
"string"
|
114
|
+
]
|
115
|
+
}
|
116
|
+
},
|
117
|
+
"links": [
|
118
|
+
{
|
119
|
+
"description": "List recipes",
|
120
|
+
"href": "/recipes",
|
121
|
+
"method": "GET",
|
122
|
+
"rel": "instances",
|
123
|
+
"title": "list"
|
124
|
+
}
|
125
|
+
],
|
126
|
+
"properties": {
|
127
|
+
"id": {
|
128
|
+
"$ref": "#/definitions/recipe/definitions/id"
|
129
|
+
}
|
130
|
+
}
|
131
|
+
}
|
132
|
+
},
|
133
|
+
"properties": {
|
134
|
+
"app": {
|
135
|
+
"$ref": "#/definitions/app"
|
136
|
+
},
|
137
|
+
"recipe": {
|
138
|
+
"$ref": "#/definitions/recipe"
|
139
|
+
}
|
140
|
+
},
|
141
|
+
"type": [
|
142
|
+
"object"
|
143
|
+
],
|
144
|
+
"description": "A schema for a small example API.",
|
145
|
+
"links": [
|
146
|
+
{
|
147
|
+
"href": "http://localhost:5000",
|
148
|
+
"rel": "self"
|
149
|
+
}
|
150
|
+
],
|
151
|
+
"title": "Example API"
|
152
|
+
}
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Rack::JsonSchema::Docs do
|
4
|
+
include Rack::Test::Methods
|
5
|
+
|
6
|
+
let(:app) do
|
7
|
+
local_docs_path = docs_path
|
8
|
+
local_schema = schema
|
9
|
+
Rack::Builder.app do
|
10
|
+
use Rack::JsonSchema::Docs, path: local_docs_path, schema: local_schema
|
11
|
+
run ->(env) do
|
12
|
+
[
|
13
|
+
200,
|
14
|
+
{},
|
15
|
+
["dummy"],
|
16
|
+
]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
let(:schema) do
|
22
|
+
str = File.read(schema_path)
|
23
|
+
JSON.parse(str)
|
24
|
+
end
|
25
|
+
|
26
|
+
let(:schema_path) do
|
27
|
+
File.expand_path("../../../fixtures/schema.json", __FILE__)
|
28
|
+
end
|
29
|
+
|
30
|
+
let(:docs_path) do
|
31
|
+
nil
|
32
|
+
end
|
33
|
+
|
34
|
+
let(:response) do
|
35
|
+
last_response
|
36
|
+
end
|
37
|
+
|
38
|
+
let(:env) do
|
39
|
+
{}
|
40
|
+
end
|
41
|
+
|
42
|
+
let(:params) do
|
43
|
+
{}
|
44
|
+
end
|
45
|
+
|
46
|
+
subject do
|
47
|
+
send(verb, path, params, env)
|
48
|
+
response.status
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "#call" do
|
52
|
+
let(:verb) do
|
53
|
+
:get
|
54
|
+
end
|
55
|
+
|
56
|
+
context "without :path option" do
|
57
|
+
let(:path) do
|
58
|
+
"/docs"
|
59
|
+
end
|
60
|
+
|
61
|
+
it "generates API documentation and returns it to request to GET /docs" do
|
62
|
+
should == 200
|
63
|
+
response.body.should include("Example API")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context "with :path option" do
|
68
|
+
let(:docs_path) do
|
69
|
+
"/api_document"
|
70
|
+
end
|
71
|
+
|
72
|
+
let(:path) do
|
73
|
+
"/api_document"
|
74
|
+
end
|
75
|
+
|
76
|
+
it "responds to specified path" do
|
77
|
+
should == 200
|
78
|
+
response.body.should_not == "dummy"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context "without targeted request" do
|
83
|
+
let(:path) do
|
84
|
+
"/apps"
|
85
|
+
end
|
86
|
+
|
87
|
+
it "delegates request to inner app" do
|
88
|
+
should == 200
|
89
|
+
response.body.should == "dummy"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Rack::JsonSchema::Mock do
|
4
|
+
include Rack::Test::Methods
|
5
|
+
|
6
|
+
let(:app) do
|
7
|
+
local_schema = schema
|
8
|
+
Rack::Builder.app do
|
9
|
+
use Rack::JsonSchema::ErrorHandler
|
10
|
+
use Rack::JsonSchema::Mock, schema: local_schema
|
11
|
+
run ->(env) do
|
12
|
+
[
|
13
|
+
200,
|
14
|
+
{},
|
15
|
+
["dummy"],
|
16
|
+
]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
let(:schema) do
|
22
|
+
str = File.read(schema_path)
|
23
|
+
JSON.parse(str)
|
24
|
+
end
|
25
|
+
|
26
|
+
let(:schema_path) do
|
27
|
+
File.expand_path("../../../fixtures/schema.json", __FILE__)
|
28
|
+
end
|
29
|
+
|
30
|
+
let(:response) do
|
31
|
+
last_response
|
32
|
+
end
|
33
|
+
|
34
|
+
let(:env) do
|
35
|
+
{}
|
36
|
+
end
|
37
|
+
|
38
|
+
let(:params) do
|
39
|
+
{}
|
40
|
+
end
|
41
|
+
|
42
|
+
subject do
|
43
|
+
send(verb, path, params, env)
|
44
|
+
response.status
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "#call" do
|
48
|
+
context "with list API" do
|
49
|
+
let(:verb) do
|
50
|
+
:get
|
51
|
+
end
|
52
|
+
|
53
|
+
let(:path) do
|
54
|
+
"/apps"
|
55
|
+
end
|
56
|
+
|
57
|
+
it "returns Array dummy response" do
|
58
|
+
should == 200
|
59
|
+
response.body.should be_json_as(
|
60
|
+
[
|
61
|
+
{
|
62
|
+
id: schema["definitions"]["app"]["definitions"]["id"]["example"],
|
63
|
+
name: schema["definitions"]["app"]["definitions"]["name"]["example"],
|
64
|
+
}
|
65
|
+
]
|
66
|
+
)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
context "with info API" do
|
71
|
+
let(:verb) do
|
72
|
+
:get
|
73
|
+
end
|
74
|
+
|
75
|
+
let(:path) do
|
76
|
+
"/apps/1"
|
77
|
+
end
|
78
|
+
|
79
|
+
it "returns dummy response" do
|
80
|
+
should == 200
|
81
|
+
response.body.should be_json_as(
|
82
|
+
{
|
83
|
+
id: schema["definitions"]["app"]["definitions"]["id"]["example"],
|
84
|
+
name: schema["definitions"]["app"]["definitions"]["name"]["example"],
|
85
|
+
}
|
86
|
+
)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
context "with POST API" do
|
91
|
+
let(:verb) do
|
92
|
+
:post
|
93
|
+
end
|
94
|
+
|
95
|
+
let(:path) do
|
96
|
+
"/apps"
|
97
|
+
end
|
98
|
+
|
99
|
+
it "returns dummy response with 201" do
|
100
|
+
should == 201
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
context "without example" do
|
105
|
+
before do
|
106
|
+
schema["definitions"]["recipe"]["definitions"]["id"].delete("example")
|
107
|
+
end
|
108
|
+
|
109
|
+
let(:verb) do
|
110
|
+
:get
|
111
|
+
end
|
112
|
+
|
113
|
+
let(:path) do
|
114
|
+
"/recipes"
|
115
|
+
end
|
116
|
+
|
117
|
+
it "returns example_not_found error" do
|
118
|
+
should == 500
|
119
|
+
response.body.should be_json_as(
|
120
|
+
id: "example_not_found",
|
121
|
+
message: "No example found for #/definitions/recipe/id",
|
122
|
+
)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|