sinja 0.1.0.beta1
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 +9 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +755 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/sinatra/jsonapi.rb +127 -0
- data/lib/sinatra/jsonapi/config.rb +125 -0
- data/lib/sinatra/jsonapi/helpers/relationships.rb +23 -0
- data/lib/sinatra/jsonapi/helpers/sequel.rb +62 -0
- data/lib/sinatra/jsonapi/helpers/serializers.rb +131 -0
- data/lib/sinatra/jsonapi/relationship_routes/has_many.rb +35 -0
- data/lib/sinatra/jsonapi/relationship_routes/has_one.rb +25 -0
- data/lib/sinatra/jsonapi/resource.rb +137 -0
- data/lib/sinatra/jsonapi/resource_routes.rb +63 -0
- data/lib/sinja.rb +13 -0
- data/lib/sinja/version.rb +6 -0
- data/sinja.gemspec +30 -0
- metadata +163 -0
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Sinatra::JSONAPI
|
3
|
+
module RelationshipRoutes
|
4
|
+
module HasMany
|
5
|
+
ACTIONS = %i[fetch clear merge subtract].freeze
|
6
|
+
CONFLICT_ACTIONS = %i[merge].freeze
|
7
|
+
|
8
|
+
def self.registered(app)
|
9
|
+
app.def_action_helpers(ACTIONS, app)
|
10
|
+
|
11
|
+
app.get '', :actions=>:fetch do
|
12
|
+
serialize_models(*fetch)
|
13
|
+
end
|
14
|
+
|
15
|
+
app.patch '', :nullif=>proc(&:empty?), :actions=>:clear do
|
16
|
+
serialize_linkages?(*clear)
|
17
|
+
end
|
18
|
+
|
19
|
+
app.patch '', :actions=>%i[clear merge] do
|
20
|
+
clear_updated, clear_opts = clear
|
21
|
+
merge_updated, merge_opts = merge(data)
|
22
|
+
serialize_linkages?(clear_updated||merge_updated, clear_opts.merge(merge_opts)) # TODO: DWIM?
|
23
|
+
end
|
24
|
+
|
25
|
+
app.post '', :actions=>%i[merge] do
|
26
|
+
serialize_linkages?(*merge(data))
|
27
|
+
end
|
28
|
+
|
29
|
+
app.delete '', :actions=>%i[subtract] do
|
30
|
+
serialize_linkages?(*subtract(data))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Sinatra::JSONAPI
|
3
|
+
module RelationshipRoutes
|
4
|
+
module HasOne
|
5
|
+
ACTIONS = %i[pluck prune graft].freeze
|
6
|
+
CONFLICT_ACTIONS = %i[graft].freeze
|
7
|
+
|
8
|
+
def self.registered(app)
|
9
|
+
app.def_action_helpers(ACTIONS, app)
|
10
|
+
|
11
|
+
app.get '', :actions=>:pluck do
|
12
|
+
serialize_model(*pluck)
|
13
|
+
end
|
14
|
+
|
15
|
+
app.patch '', :nullif=>proc(&:nil?), :actions=>:prune do
|
16
|
+
serialize_linkage?(*prune)
|
17
|
+
end
|
18
|
+
|
19
|
+
app.patch '', :actions=>:graft do
|
20
|
+
serialize_linkage?(*graft(data))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'sinatra/base'
|
3
|
+
require 'sinatra/namespace'
|
4
|
+
|
5
|
+
require 'sinatra/jsonapi/helpers/relationships'
|
6
|
+
require 'sinatra/jsonapi/relationship_routes/has_many'
|
7
|
+
require 'sinatra/jsonapi/relationship_routes/has_one'
|
8
|
+
require 'sinatra/jsonapi/resource_routes'
|
9
|
+
|
10
|
+
module Sinatra::JSONAPI
|
11
|
+
module Resource
|
12
|
+
def def_action_helper(action, context=nil)
|
13
|
+
abort "JSONAPI resource actions can't be HTTP verbs!" \
|
14
|
+
if Sinatra::Base.respond_to?(action)
|
15
|
+
|
16
|
+
context.define_singleton_method(action) do |**opts, &block|
|
17
|
+
can(action, opts[:roles]) if opts.key?(:roles)
|
18
|
+
|
19
|
+
if block.nil?
|
20
|
+
if respond_to?(action)
|
21
|
+
# TODO: Is this safe?
|
22
|
+
remove_method(action)
|
23
|
+
return
|
24
|
+
else
|
25
|
+
# TODO: Throw an error?
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
define_method(action) do |*args|
|
30
|
+
result =
|
31
|
+
begin
|
32
|
+
instance_exec(*args.take(block.arity.abs), &block)
|
33
|
+
rescue Exception=>e
|
34
|
+
halt 409, e.message if settings.sinja_config.conflict?(action, e.class)
|
35
|
+
#halt 422, resource.errors if settings.sinja_config.invalid?(action, e.class) # TODO
|
36
|
+
#not_found if settings.sinja_config.not_found?(action, e.class) # TODO
|
37
|
+
raise
|
38
|
+
end
|
39
|
+
|
40
|
+
# TODO: Move this to a constant or configurable?
|
41
|
+
required_arity = {
|
42
|
+
:create=>2,
|
43
|
+
:index=>-1,
|
44
|
+
:fetch=>-1
|
45
|
+
}.freeze[action] || 1
|
46
|
+
|
47
|
+
case result
|
48
|
+
when Array
|
49
|
+
opts = {}
|
50
|
+
if Hash === result.last
|
51
|
+
opts = result.pop
|
52
|
+
elsif required_arity < 0 && !(Array === result.first)
|
53
|
+
result = [result]
|
54
|
+
end
|
55
|
+
|
56
|
+
raise ActionHelperError, "Unexpected return value(s) from `#{action}' action helper" \
|
57
|
+
unless result.length == required_arity.abs
|
58
|
+
|
59
|
+
result.push(opts)
|
60
|
+
when Hash
|
61
|
+
Array.new(required_arity.abs).push(result)
|
62
|
+
else
|
63
|
+
[result, nil].take(required_arity.abs).push({})
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def def_action_helpers(actions, context=nil)
|
70
|
+
[*actions].each { |action| def_action_helper(action, context) }
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.registered(app)
|
74
|
+
app.helpers Helpers::Relationships do
|
75
|
+
attr_accessor :resource
|
76
|
+
|
77
|
+
def sanity_check!(id=nil)
|
78
|
+
halt 409, 'Resource type in payload does not match endpoint' \
|
79
|
+
if data[:type] != request.path.split('/').last # TODO
|
80
|
+
|
81
|
+
halt 409, 'Resource ID in payload does not match endpoint' \
|
82
|
+
if id && data[:id].to_s != id.to_s
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
app.register ResourceRoutes
|
87
|
+
end
|
88
|
+
|
89
|
+
%i[has_one has_many].each do |rel_type|
|
90
|
+
define_method(rel_type) do |rel, &block|
|
91
|
+
rel_path = rel.to_s.tr('_', '-')
|
92
|
+
|
93
|
+
namespace %r{/(?<resource_id>[^/]+)(?<r>/relationships)?/#{rel_path}}, :actions=>:show do
|
94
|
+
helpers do
|
95
|
+
def relationship_link?
|
96
|
+
!params[:r].nil?
|
97
|
+
end
|
98
|
+
|
99
|
+
def resource
|
100
|
+
super || self.resource = show(params[:resource_id]).first
|
101
|
+
end
|
102
|
+
|
103
|
+
def sanity_check!
|
104
|
+
super(params[:resource_id])
|
105
|
+
end
|
106
|
+
|
107
|
+
define_method(:linkage) do
|
108
|
+
# TODO: This is extremely wasteful. Refactor JAS to expose the linkage serializer?
|
109
|
+
serialize_model(resource, :include=>rel_path)['data']['relationships'][rel_path]
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
before do
|
114
|
+
not_found unless resource
|
115
|
+
end
|
116
|
+
|
117
|
+
get '' do
|
118
|
+
pass unless relationship_link?
|
119
|
+
|
120
|
+
serialize_linkage
|
121
|
+
end
|
122
|
+
|
123
|
+
register RelationshipRoutes.const_get \
|
124
|
+
rel_type.to_s.split('_').map(&:capitalize).join.to_sym
|
125
|
+
|
126
|
+
if rel_type == :has_one
|
127
|
+
pluck { resource.send(rel) }
|
128
|
+
elsif rel_type == :has_many
|
129
|
+
fetch { resource.send(rel) }
|
130
|
+
end
|
131
|
+
|
132
|
+
instance_eval(&block) if block
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Sinatra::JSONAPI
|
3
|
+
module ResourceRoutes
|
4
|
+
ACTIONS = %i[index show create update destroy].freeze
|
5
|
+
CONFLICT_ACTIONS = %i[create update].freeze
|
6
|
+
|
7
|
+
def self.registered(app)
|
8
|
+
app.def_action_helpers(ACTIONS, app)
|
9
|
+
|
10
|
+
app.get '', :actions=>:index do
|
11
|
+
serialize_models(*index)
|
12
|
+
end
|
13
|
+
|
14
|
+
app.get '/:id', :actions=>:show do |id|
|
15
|
+
self.resource, opts = show(id)
|
16
|
+
not_found unless resource
|
17
|
+
serialize_model(resource, opts)
|
18
|
+
end
|
19
|
+
|
20
|
+
app.post '', :actions=>:create do
|
21
|
+
sanity_check!
|
22
|
+
halt 403, 'Client-generated IDs not supported' \
|
23
|
+
if data[:id] && method(:create).arity != 2
|
24
|
+
|
25
|
+
_, self.resource, opts = transaction do
|
26
|
+
create(data.fetch(:attributes, {}), data[:id]).tap do |id, *|
|
27
|
+
dispatch_relationship_requests!(id, :method=>:patch)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
if resource
|
32
|
+
content = serialize_model(resource, opts)
|
33
|
+
if content.respond_to?(:dig) && self_link = content.dig(*%w[data links self])
|
34
|
+
headers 'Location'=>self_link
|
35
|
+
end
|
36
|
+
[201, content]
|
37
|
+
elsif data[:id]
|
38
|
+
204
|
39
|
+
else
|
40
|
+
raise ActionHelperError, "Unexpected return value(s) from `create' action helper"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
app.patch '/:id', :actions=>%i[show update] do |id|
|
45
|
+
sanity_check!(id)
|
46
|
+
self.resource, = show(id)
|
47
|
+
not_found unless resource
|
48
|
+
serialize_model?(transaction do
|
49
|
+
update(data.fetch(attributes, {})).tap do
|
50
|
+
dispatch_relationship_requests!(id, :method=>:patch)
|
51
|
+
end
|
52
|
+
end)
|
53
|
+
end
|
54
|
+
|
55
|
+
app.delete '/:id', :actions=>%i[show destroy] do |id|
|
56
|
+
self.resource, = show(id)
|
57
|
+
not_found unless resource
|
58
|
+
_, opts = destroy
|
59
|
+
serialize_model?(nil, opts)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/sinja.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Sinatra
|
3
|
+
module JSONAPI
|
4
|
+
MIME_TYPE = 'application/vnd.api+json'
|
5
|
+
|
6
|
+
SinjaError = Class.new(StandardError)
|
7
|
+
ActionHelperError = Class.new(SinjaError)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
Sinja = Sinatra::JSONAPI
|
12
|
+
|
13
|
+
require 'sinatra/jsonapi'
|
data/sinja.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'sinja/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'sinja'
|
8
|
+
spec.version = Sinatra::JSONAPI::VERSION
|
9
|
+
spec.authors = ['Mike Pastore']
|
10
|
+
spec.email = ['mike@oobak.org']
|
11
|
+
|
12
|
+
spec.summary = 'RESTful, JSON:API-compliant web services in Sinatra'
|
13
|
+
spec.homepage = 'https://github.com/mwpastore/sinja'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
17
|
+
f.match(%r{^(test|spec|features)/})
|
18
|
+
end
|
19
|
+
spec.require_paths = %w[lib]
|
20
|
+
|
21
|
+
# TODO: relax these dependencies
|
22
|
+
spec.add_dependency 'json', '~> 2.0.1'
|
23
|
+
spec.add_dependency 'jsonapi-serializers', '~> 0.16.0'
|
24
|
+
spec.add_dependency 'sinatra', '~> 2.0.0.beta2'
|
25
|
+
spec.add_dependency 'sinatra-contrib', '~> 2.0.0.beta2'
|
26
|
+
|
27
|
+
spec.add_development_dependency 'bundler', '~> 1.13'
|
28
|
+
spec.add_development_dependency 'rake', '~> 11.3'
|
29
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sinja
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0.beta1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mike Pastore
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-11-07 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: json
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 2.0.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 2.0.1
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: jsonapi-serializers
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.16.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.16.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: sinatra
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 2.0.0.beta2
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 2.0.0.beta2
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: sinatra-contrib
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 2.0.0.beta2
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 2.0.0.beta2
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: bundler
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.13'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.13'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rake
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '11.3'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '11.3'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rspec
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '3.0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '3.0'
|
111
|
+
description:
|
112
|
+
email:
|
113
|
+
- mike@oobak.org
|
114
|
+
executables: []
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- ".gitignore"
|
119
|
+
- ".rspec"
|
120
|
+
- ".travis.yml"
|
121
|
+
- Gemfile
|
122
|
+
- LICENSE.txt
|
123
|
+
- README.md
|
124
|
+
- Rakefile
|
125
|
+
- bin/console
|
126
|
+
- bin/setup
|
127
|
+
- lib/sinatra/jsonapi.rb
|
128
|
+
- lib/sinatra/jsonapi/config.rb
|
129
|
+
- lib/sinatra/jsonapi/helpers/relationships.rb
|
130
|
+
- lib/sinatra/jsonapi/helpers/sequel.rb
|
131
|
+
- lib/sinatra/jsonapi/helpers/serializers.rb
|
132
|
+
- lib/sinatra/jsonapi/relationship_routes/has_many.rb
|
133
|
+
- lib/sinatra/jsonapi/relationship_routes/has_one.rb
|
134
|
+
- lib/sinatra/jsonapi/resource.rb
|
135
|
+
- lib/sinatra/jsonapi/resource_routes.rb
|
136
|
+
- lib/sinja.rb
|
137
|
+
- lib/sinja/version.rb
|
138
|
+
- sinja.gemspec
|
139
|
+
homepage: https://github.com/mwpastore/sinja
|
140
|
+
licenses:
|
141
|
+
- MIT
|
142
|
+
metadata: {}
|
143
|
+
post_install_message:
|
144
|
+
rdoc_options: []
|
145
|
+
require_paths:
|
146
|
+
- lib
|
147
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - ">="
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '0'
|
152
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
153
|
+
requirements:
|
154
|
+
- - ">"
|
155
|
+
- !ruby/object:Gem::Version
|
156
|
+
version: 1.3.1
|
157
|
+
requirements: []
|
158
|
+
rubyforge_project:
|
159
|
+
rubygems_version: 2.5.1
|
160
|
+
signing_key:
|
161
|
+
specification_version: 4
|
162
|
+
summary: RESTful, JSON:API-compliant web services in Sinatra
|
163
|
+
test_files: []
|