sinatra-schema 0.0.2 → 0.0.3

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
- MDY3YmZhNWJiOTc1YmI1NWU5OTk1MmVkZDg2ZjkyZTA3ZmZiMzZiOA==
4
+ MGZiZDNiMDcyM2EzNzM1MjdmZWUxOTkzZGNjY2QxNmVmN2RmODRiMg==
5
5
  data.tar.gz: !binary |-
6
- ODVkYjE3NDFiODdhMjdiOGNkYmRhYTU4NjIzMTE3Nzk4OGJjYThhZQ==
6
+ ODY0YjViYzM3YmJhYjIzODc3Y2U3ZmMwMzJmM2Q5YzRiN2M4ZjNmYw==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- ZDhmNDgxZDhlY2FkODI3MDczMmJhMTE4OWFjYWZhMzM2MWQ2NTgzMWY2N2Zm
10
- MWEyYzU5ZWI2Y2Q5ZmQ5MGFkMzMwZWEyMmNhNDcyM2RiZjU4MTA4M2VjYzgy
11
- MDk2N2MwM2ZiNDA5MDRiYjBlMzdjODVjODM5ZWQ2MGNjZmE0ODI=
9
+ MDQwY2YwMzU3MTg3ZDkzMGNiMDY2MTFlNjM0MWFkNjYzNzdhZTQzMWVlYjg1
10
+ NzdhNzk2NjZlM2YyNjQ1MzQxZjA5ZTNiNWQ0NGI5MjYxYjI3MTUzYjYxODMx
11
+ NDJhYzMyNDI2NDZjZGJlNjVhYzQxMTQ0OTdjOTRkN2Q1ZmQ3NmY=
12
12
  data.tar.gz: !binary |-
13
- MTZjNDNjODZhYmE1OTEyODU4MmY3MjBmZTAyZWRlYTY3OWUzYjVhMDEyM2Iw
14
- YWY4OTE5MWU2ZGIxN2RlNzc3NDEwODllZDAwMWY2Y2VlNDdiMDAzNTA5ZmE1
15
- NDI4NTFjNjViYWI1ZTc5MDA5NzJhNWQwMGQyZGY2Y2U3YWU0NjQ=
13
+ YjE1MWY4MTVhYTMxYzEyODU0Mjk3Y2NiY2I5YTA5YjBkMTlmOGE4MmZlOTMw
14
+ NDlkNzU2MmU2OTg0ZGNiYzdkNWRkZjAwYmQ5OWE0MzNhNGE0MDVjZmVkZWFk
15
+ NzM3MDNkZmEwOGI1OTY4ZWQ3NTg2ODY0MjBiODRlNDU5YTZlOGE=
data/README.md CHANGED
@@ -65,6 +65,20 @@ resource("/albums") do |res|
65
65
  end
66
66
  ```
67
67
 
68
+ ### Nested params
69
+
70
+ These are also casted and validated as you'd expect:
71
+
72
+ ```ruby
73
+ resource("/albums") do |res|
74
+ resource.link(:post) do |link|
75
+ link.nested :artist do |a|
76
+ a.property.text :name
77
+ end
78
+ end
79
+ end
80
+ ```
81
+
68
82
  ### JSON Schema
69
83
 
70
84
  The extension will serve a JSON Schema dump at `GET /schema` for you.
@@ -1,7 +1,7 @@
1
1
  module Sinatra
2
2
  module Schema
3
3
  class Definition
4
- attr_accessor :description, :example, :format, :id, :type
4
+ attr_accessor :description, :example, :id, :type
5
5
 
6
6
  def initialize(options={})
7
7
  @description = options[:description]
@@ -15,10 +15,10 @@ module Sinatra
15
15
  return unless value
16
16
 
17
17
  case type
18
- when "string"
19
- value.to_s
20
18
  when "boolean"
21
19
  %w( t true 1 ).include?(value.to_s)
20
+ when "email", "string", "uuid"
21
+ value.to_s
22
22
  end
23
23
  end
24
24
 
@@ -27,10 +27,41 @@ module Sinatra
27
27
  return if value.nil?
28
28
 
29
29
  case type
30
+ when "boolean"
31
+ [true, false].include?(value)
32
+ when "email"
33
+ value.to_s =~ /\A[^@\s]+@([^@\s]+\.)+[^@\s]+\z/
30
34
  when "string"
31
35
  value.is_a?(String)
36
+ when "uuid"
37
+ value.to_s =~ /\A[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}\Z/
38
+ end
39
+ end
40
+
41
+ def to_schema
42
+ schema_type, schema_format = json_schema_type_and_format
43
+ attrs = { type: schema_type }
44
+ if schema_format
45
+ attrs[:format] = schema_format
46
+ end
47
+ if description
48
+ attrs[:description] = description
49
+ end
50
+ attrs
51
+ end
52
+
53
+ protected
54
+
55
+ def json_schema_type_and_format
56
+ case type
32
57
  when "boolean"
33
- [true, false].include?(value)
58
+ "boolean"
59
+ when "email"
60
+ ["string", "email"]
61
+ when "string"
62
+ "string"
63
+ when "uuid"
64
+ ["string", "uuid"]
34
65
  end
35
66
  end
36
67
  end
@@ -2,23 +2,40 @@ module Sinatra
2
2
  module Schema
3
3
  module DSL
4
4
  class Definitions
5
- attr_accessor :definition, :resource, :options
5
+ attr_accessor :definition, :resource, :options, :targets
6
6
 
7
- def initialize(resource, options={})
8
- @options = options
7
+ def initialize(resource, targets)
9
8
  @resource = resource
9
+ # array of hashes to receive the definition, first is the resource defs
10
+ @targets = targets
10
11
  end
11
12
 
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)
13
+ def text(id, options={})
14
+ options.merge!(id: id, type: "string")
15
+ add Definition.new(options)
16
16
  end
17
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)
18
+ def bool(id, options={})
19
+ options.merge!(id: id, type: "boolean")
20
+ add Definition.new(options)
21
+ end
22
+
23
+ def uuid(id, options={})
24
+ options.merge!(id: id, type: "uuid")
25
+ add Definition.new(options)
26
+ end
27
+
28
+ def email(id, options={})
29
+ options.merge!(id: id, type: "email")
30
+ add Definition.new(options)
31
+ end
32
+
33
+ def nested(id)
34
+ # add a space in the definitions/properties for the nested def:
35
+ targets.each { |h| h[id] = {} }
36
+
37
+ # yield a new DSL with updated targets
38
+ yield Definitions.new(resource, targets.map { |h| h[id] })
22
39
  end
23
40
 
24
41
  def ref(id)
@@ -33,14 +50,12 @@ module Sinatra
33
50
  protected
34
51
 
35
52
  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
53
+ targets.each_with_index do |hash, i|
54
+ # here's the trick, and here's why the first target is always the
55
+ # resource def: skip it when adding a reference (eg: it's already)
56
+ # in the resource def, just add the property!
57
+ next if reference && i == 0
58
+ hash[definition.id] = definition
44
59
  end
45
60
  end
46
61
  end
@@ -28,13 +28,13 @@ module Sinatra
28
28
  end
29
29
 
30
30
  def property
31
- DSL::Definitions.new(resource, link: link, serialize: false)
31
+ DSL::Definitions.new(resource, [resource.defs, link.properties])
32
32
  end
33
33
 
34
34
  protected
35
35
 
36
36
  def build_link
37
- full_href = "#{resource.path}/#{href.chomp("/")}".chomp("/")
37
+ full_href = "#{resource.path}/#{href}".gsub("//", "/").chomp("/")
38
38
  Link.new(resource: resource, method: method, href: full_href)
39
39
  end
40
40
  end
@@ -14,7 +14,7 @@ module Sinatra
14
14
  end
15
15
 
16
16
  def property
17
- DSL::Definitions.new(resource, serialize: true)
17
+ DSL::Definitions.new(resource, [resource.defs, resource.properties])
18
18
  end
19
19
 
20
20
  def link(method, href="/", &blk)
@@ -11,12 +11,16 @@ module Sinatra
11
11
  end
12
12
 
13
13
  def register(app)
14
+ app.send(method.downcase, href, &handler)
15
+ end
16
+
17
+ def handler
14
18
  link = self
15
- app.send(method.downcase, href) do
19
+ lambda do
16
20
  begin
17
21
  schema_params = parse_params(link.properties)
18
22
  validate_params!(schema_params, link.properties)
19
- res = link.action_block.call(schema_params)
23
+ res = instance_exec(schema_params, &link.action_block)
20
24
  link.resource.validate_response!(res)
21
25
  res
22
26
  rescue RuntimeError => e
@@ -1,6 +1,6 @@
1
1
  module Sinatra
2
2
  module Schema
3
- module ParamHandling
3
+ module ParamParsing
4
4
  def parse_params(properties)
5
5
  case request.media_type
6
6
  when nil, "application/json"
@@ -29,12 +29,18 @@ module Sinatra
29
29
  raise "Invalid JSON"
30
30
  end
31
31
 
32
- def cast_regular_params(properties)
33
- casted_params = params.inject({}) do |casted, (k, v)|
32
+ def cast_regular_params(properties, root=params)
33
+ casted_params = root.inject({}) do |casted, (k, v)|
34
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
35
+
36
+ # handle nested params
37
+ if definition.is_a?(Hash) || v.is_a?(Hash)
38
+ casted[k] = cast_regular_params(definition, v)
39
+ else
40
+ # if there's no definition just leave the original param,
41
+ # let the validation raise on this later:
42
+ casted[k] = definition ? definition.cast(v) : v
43
+ end
38
44
  casted
39
45
  end
40
46
  indifferent_params(casted_params)
@@ -13,8 +13,13 @@ module Sinatra
13
13
  end
14
14
 
15
15
  properties.each do |id, definition|
16
- unless definition.valid?(params[id])
17
- raise "Bad param: #{id}"
16
+ # handle nested params
17
+ if definition.is_a?(Hash)
18
+ validate_params!(params[id], definition)
19
+ else
20
+ unless definition.valid?(params[id])
21
+ raise "Bad param: #{id}"
22
+ end
18
23
  end
19
24
  end
20
25
  end
@@ -38,10 +38,7 @@ module Sinatra
38
38
  description: description,
39
39
  type: "object",
40
40
  definitions: defs.inject({}) { |h, (id, definition)|
41
- h[id] = {
42
- description: definition.description,
43
- type: definition.type,
44
- }
41
+ h[id] = definition.to_schema
45
42
  h
46
43
  },
47
44
  links: links.map { |link|
@@ -1,5 +1,5 @@
1
1
  module Sinatra
2
2
  module Schema
3
- VERSION = "0.0.2"
3
+ VERSION = "0.0.3"
4
4
  end
5
5
  end
@@ -5,7 +5,7 @@ require "multi_json"
5
5
 
6
6
  require "sinatra/schema/definition"
7
7
  require "sinatra/schema/link"
8
- require "sinatra/schema/param_handling"
8
+ require "sinatra/schema/param_parsing"
9
9
  require "sinatra/schema/param_validation"
10
10
  require "sinatra/schema/resource"
11
11
  require "sinatra/schema/root"
@@ -17,7 +17,7 @@ require "sinatra/schema/dsl/resources"
17
17
  module Sinatra
18
18
  module Schema
19
19
  def self.registered(app)
20
- app.helpers ParamHandling
20
+ app.helpers ParamParsing
21
21
  app.helpers ParamValidation
22
22
  app.get "/schema" do
23
23
  content_type("application/schema+json")
@@ -0,0 +1,64 @@
1
+ require "spec_helper"
2
+
3
+ describe Sinatra::Schema::Definition do
4
+ let(:definition) { Sinatra::Schema::Definition.new }
5
+
6
+ describe "#cast" do
7
+ it "casts booleans" do
8
+ definition.type = "boolean"
9
+ assert_equal true, definition.cast("t")
10
+ assert_equal true, definition.cast("true")
11
+ assert_equal true, definition.cast("1")
12
+ assert_equal false, definition.cast("0")
13
+ assert_equal false, definition.cast("false")
14
+ end
15
+
16
+ it "casts text" do
17
+ definition.type = "string"
18
+ assert_equal "123", definition.cast(123)
19
+ end
20
+ end
21
+
22
+ describe "#valid?" do
23
+ it "detects booleans" do
24
+ definition.type = "boolean"
25
+ assert definition.valid?(true)
26
+ assert definition.valid?(false)
27
+ refute definition.valid?("true")
28
+ end
29
+
30
+ it "detects emails" do
31
+ definition.type = "email"
32
+ assert definition.valid?("foo@bar.com")
33
+ refute definition.valid?("foobar.com")
34
+ end
35
+
36
+ it "detects text" do
37
+ definition.type = "string"
38
+ assert definition.valid?("foo")
39
+ refute definition.valid?(123)
40
+ end
41
+
42
+ it "detects uuids" do
43
+ definition.type = "uuid"
44
+ assert definition.valid?(SecureRandom.uuid)
45
+ refute definition.valid?("wrong")
46
+ end
47
+ end
48
+
49
+ describe "#to_schema" do
50
+ it "dumps emails" do
51
+ definition.type = "email"
52
+ schema = definition.to_schema
53
+ assert_equal "string", schema[:type]
54
+ assert_equal "email", schema[:format]
55
+ end
56
+
57
+ it "dumps uuids" do
58
+ definition.type = "uuid"
59
+ schema = definition.to_schema
60
+ assert_equal "string", schema[:type]
61
+ assert_equal "uuid", schema[:format]
62
+ end
63
+ end
64
+ end
@@ -2,8 +2,7 @@ require "spec_helper"
2
2
 
3
3
  describe Sinatra::Schema::DSL::Definitions do
4
4
  let(:resource) { Sinatra::Schema::Resource.new(path: "/foobar") }
5
- let(:dsl) { described_class.new(resource, options) }
6
- let(:options) { Hash.new }
5
+ let(:dsl) { described_class.new(resource, [resource.defs, resource.properties]) }
7
6
  let(:root) { Sinatra::Schema::Root.instance }
8
7
 
9
8
  it "adds a string definition to the resource" do
@@ -18,9 +17,18 @@ describe Sinatra::Schema::DSL::Definitions do
18
17
  assert_equal "boolean", resource.defs[:foobar].type
19
18
  end
20
19
 
20
+ it "adds nested definitions" do
21
+ dsl.nested(:user) do |prop|
22
+ prop.text :email
23
+ prop.bool :admin
24
+ end
25
+ assert_equal 2, resource.defs[:user].size
26
+ assert_equal "string", resource.defs[:user][:email].type
27
+ assert_equal "boolean", resource.defs[:user][:admin].type
28
+ end
29
+
21
30
  describe "#ref" do
22
- let(:definition) { Sinatra::Schema::Definition.new }
23
- before { options[:serialize] = true }
31
+ let(:definition) { Sinatra::Schema::Definition.new(id: :foobar) }
24
32
 
25
33
  it "adds a reference to another definition in the resource" do
26
34
  resource.defs[:foobar] = definition
@@ -21,5 +21,13 @@ describe Sinatra::Schema::DSL::Resources do
21
21
  assert_equal 1, dsl.resource.properties.size
22
22
  assert dsl.resource.properties.has_key?(:foo)
23
23
  end
24
+
25
+ it "supports nested properties" do
26
+ dsl.property.nested :user do |prop|
27
+ prop.text :email
28
+ prop.bool :admin
29
+ end
30
+ assert_equal 2, dsl.resource.properties[:user].size
31
+ end
24
32
  end
25
33
  end
@@ -1,9 +1,9 @@
1
1
  require "spec_helper"
2
2
 
3
- describe Sinatra::Schema::ParamHandling do
3
+ describe Sinatra::Schema::ParamParsing do
4
4
  before do
5
5
  @rack_app = Sinatra.new do
6
- helpers Sinatra::Schema::ParamHandling
6
+ helpers Sinatra::Schema::ParamParsing
7
7
  post("/") do
8
8
  MultiJson.encode(parse_params($properties))
9
9
  end
@@ -31,6 +31,12 @@ describe Sinatra::Schema::ParamHandling do
31
31
  post "/", MultiJson.encode(params)
32
32
  assert_equal params, last_json
33
33
  end
34
+
35
+ it "preserves nested params" do
36
+ params = { "foo" => { "bar" => 42 }}
37
+ post "/", MultiJson.encode(params)
38
+ assert_equal params, last_json
39
+ end
34
40
  end
35
41
 
36
42
  describe "form-encoded params" do
@@ -43,8 +49,16 @@ describe Sinatra::Schema::ParamHandling do
43
49
  assert_equal({ "some_text" => "true", "some_bool" => true }, last_json)
44
50
  end
45
51
 
52
+ it "handles nested params" do
53
+ $properties = {
54
+ foo: { bar: Sinatra::Schema::Definition.new(type: "boolean") }
55
+ }
56
+ post "/", foo: { bar: "true" }
57
+ assert_equal({ "foo" => { "bar" => true }}, last_json)
58
+ end
59
+
46
60
  it "leaves params without the corresponding property untouched" do
47
- $properies = {}
61
+ $properties = {}
48
62
  params = { "foo" => "bar" }
49
63
  post "/", params
50
64
  assert_equal params, last_json
@@ -32,4 +32,12 @@ describe Sinatra::Schema::ParamValidation do
32
32
  post "/", MultiJson.encode(bool: "omg")
33
33
  end
34
34
  end
35
+
36
+ it "supports nested params" do
37
+ $properties = { foo: { bar: Sinatra::Schema::Definition.new(type: "boolean") }}
38
+ assert_raises(RuntimeError) do
39
+ post "/", MultiJson.encode(foo: { bar: "omg" })
40
+ puts last_response.body
41
+ end
42
+ end
35
43
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sinatra-schema
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
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-06 00:00:00.000000000 Z
11
+ date: 2014-12-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -139,19 +139,20 @@ files:
139
139
  - lib/sinatra/schema/dsl/links.rb
140
140
  - lib/sinatra/schema/dsl/resources.rb
141
141
  - lib/sinatra/schema/link.rb
142
- - lib/sinatra/schema/param_handling.rb
142
+ - lib/sinatra/schema/param_parsing.rb
143
143
  - lib/sinatra/schema/param_validation.rb
144
144
  - lib/sinatra/schema/resource.rb
145
145
  - lib/sinatra/schema/root.rb
146
146
  - lib/sinatra/schema/tasks/schema.rake
147
147
  - lib/sinatra/schema/utils.rb
148
148
  - lib/sinatra/schema/version.rb
149
+ - spec/definition_spec.rb
149
150
  - spec/dsl/definitions_spec.rb
150
151
  - spec/dsl/links_spec.rb
151
152
  - spec/dsl/resources_spec.rb
152
153
  - spec/integration_spec.rb
153
154
  - spec/json_schema_spec.rb
154
- - spec/param_handling_spec.rb
155
+ - spec/param_parsing_spec.rb
155
156
  - spec/param_validation_spec.rb
156
157
  - spec/resource_spec.rb
157
158
  - spec/spec_helper.rb