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.
@@ -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,5 @@
1
+ module Rack
2
+ module JsonSchema
3
+ VERSION = "1.0.0"
4
+ end
5
+ 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