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 CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- MjFmZjFhMTc3NTMzOTFiNjcwZDc0NGVmMjc0MzYyNmE4MjU4ZDU5MA==
4
+ MDY3YmZhNWJiOTc1YmI1NWU5OTk1MmVkZDg2ZjkyZTA3ZmZiMzZiOA==
5
5
  data.tar.gz: !binary |-
6
- NjljZDE3YjZkMGJhYzA2YmE2MjE2NjFjMGQzMDIyYmU3ODRkYzY2Nw==
6
+ ODVkYjE3NDFiODdhMjdiOGNkYmRhYTU4NjIzMTE3Nzk4OGJjYThhZQ==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- NmRjN2E4YjRjNzBiZGNhZTBhNTVlM2I1NWUxZGRlYzFjODBkOTQwMzQxMjFi
10
- ZDQ0YTViMTQ0MzAzZDc0OTk3M2E2NzgzNzRlOGI0MWY4NTE4YTg1M2E5ZDNi
11
- YzQzMWM1OGI3NDE0NWZkODkzMjYxN2EyM2M2NTUwOTM1MDkwNTQ=
9
+ ZDhmNDgxZDhlY2FkODI3MDczMmJhMTE4OWFjYWZhMzM2MWQ2NTgzMWY2N2Zm
10
+ MWEyYzU5ZWI2Y2Q5ZmQ5MGFkMzMwZWEyMmNhNDcyM2RiZjU4MTA4M2VjYzgy
11
+ MDk2N2MwM2ZiNDA5MDRiYjBlMzdjODVjODM5ZWQ2MGNjZmE0ODI=
12
12
  data.tar.gz: !binary |-
13
- YWM3ZTI2YmE3OTk3NTdlOTRmNTQwNzM4ZGQzOTE3ODNlY2VlNDJmMmQyZDU3
14
- MTQ2MGRjNDg1MzA0OWUzODJiMzAwZWNlOGYxMzU3NWU2YmE2NjZlNzgxZmUz
15
- YjI0M2ViYTZkY2Q3MmM1NjFkOWZlYTBjYTQxMjI5NTg2ZjY5OTA=
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` and define resources like:
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("/accounts") do
15
- title "Account"
16
- description "The account of a user signed up to our service"
17
- serializer AccountSerializer
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
- res.link(:post) do
20
- title "Create"
21
- description "Create a new account"
22
- action do
23
- Account.create(email: params[:email])
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
- ## See also
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
- - [sinatra-param](https://github.com/mattt/sinatra-param): nice take on validating request parameters.
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
@@ -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, :action
4
+ attr_accessor :action_block, :resource, :title, :description, :href, :method, :properties, :rel
5
5
 
6
6
  def initialize(options)
7
- @resource = options[:resource]
8
- @method = options[:method]
9
- @href = options[:href]
10
- end
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.validate_params!(params)
25
- res = link.action_block.call(params)
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
- MultiJson.encode(res)
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(app, path)
7
- @app = app
8
- @path = path.chomp("/")
9
- @links = []
10
- @defs = {}
6
+ def initialize(options)
7
+ @path = options.fetch(:path).chomp("/")
8
+ @links = []
9
+ @defs = {}
10
+ @properties = {}
11
11
  end
12
12
 
13
- def define(id)
14
- @defs[id] = Definition.new
15
- yield @defs[id]
13
+ def id
14
+ @id ||= ActiveSupport::Inflector.singularize(path.split("/").last).to_sym
16
15
  end
17
16
 
18
- def link(method, href="/", &blk)
19
- href = "#{path}/#{href.chomp("/")}".chomp("/")
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!(res)
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
- if properties
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
@@ -0,0 +1,5 @@
1
+ desc "Print out the JSON schema for this app"
2
+ task :schema do
3
+ schema = Sinatra::Schema::Root.instance.to_schema
4
+ puts MultiJson.encode(schema, pretty: true)
5
+ end
@@ -1,16 +1,22 @@
1
1
  module Sinatra
2
2
  module Schema
3
3
  class Utils
4
- def self.validate_keys!(expected, received)
5
- missing = expected.map(&:to_s).sort - received.keys.map(&:to_s).sort
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 - expected.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
@@ -0,0 +1,5 @@
1
+ module Sinatra
2
+ module Schema
3
+ VERSION = "0.0.2"
4
+ end
5
+ end
@@ -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
- MultiJson.encode(
11
- "$schema" => "http://json-schema.org/draft-04/hyper-schema",
12
- "definitions" => app.resources.inject({}) { |result, (id, resource)|
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 resources
21
- @resources ||= {}
29
+ def schema_root
30
+ Root.instance
22
31
  end
23
32
 
24
33
  def resource(path)
25
- res = Resource.new(self, path)
26
- yield(res)
27
- resources[res.id] = res
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
@@ -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 links" do
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 links" do
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(:default, :test)
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
@@ -0,0 +1,3 @@
1
+ def last_json
2
+ @last_json ||= MultiJson.decode(last_response.body)
3
+ end
@@ -6,35 +6,29 @@ class TestApp < Sinatra::Base
6
6
  end
7
7
 
8
8
  resource("/accounts") do |res|
9
- res.id = :account
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.define(:email) do |d|
14
- d.description = "unique email address of account"
15
- d.example = "username@example.com"
16
- d.format = "email"
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 = "Info"
24
- link.rel = "self"
25
- link.description = "Info for account"
17
+ link.title "Info"
18
+ link.rel "self"
19
+ link.description "Info for account"
26
20
  link.action do
27
- { email: "foo@bar.com" }
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 = "Create"
33
- link.rel = "create"
34
- link.description = "Create a new account"
35
- link.properties = [:email]
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
- { email: params[:email] }
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.1
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-05 00:00:00.000000000 Z
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: