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 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: