siresta 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.yardopts +1 -0
- data/README.md +144 -0
- data/Rakefile +63 -0
- data/lib/siresta.rb +5 -0
- data/lib/siresta/api.rb +190 -0
- data/lib/siresta/client.rb +71 -0
- data/lib/siresta/env.rb +6 -0
- data/lib/siresta/response.rb +176 -0
- data/lib/siresta/routes.rb +32 -0
- data/lib/siresta/spec.rb +79 -0
- data/lib/siresta/version.rb +4 -0
- data/lib/siresta/xml.rb +72 -0
- data/siresta.gemspec +36 -0
- metadata +159 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 9217dfe568d7730704e4e45843b624407d854749
|
4
|
+
data.tar.gz: 5f9e29a33a17aa6ea78a085b5d3aa27e64ef5cad
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 362d5b662cdedb546d5df3a9601b0339139ad9900fc2bd8d67a003bcba464f17d9f9a342bb466c0a440449a281191550d289809b77cf3d87715f195acf8889c2
|
7
|
+
data.tar.gz: 70d5e0b4ecca360daf376dd9adcece4d0bae1dda08299a975ec0164ef1af0722ae4a2e87f55ef5ec92c51d1e87666b817d3e2bc2d18d27e4804ba120c04154e9
|
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--markup markdown
|
data/README.md
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
[]: {{{1
|
2
|
+
|
3
|
+
File : README.md
|
4
|
+
Maintainer : Felix C. Stegerman <flx@obfusk.net>
|
5
|
+
Date : 2014-06-18
|
6
|
+
|
7
|
+
Copyright : Copyright (C) 2014 Felix C. Stegerman
|
8
|
+
Version : v0.0.2
|
9
|
+
|
10
|
+
[]: }}}1
|
11
|
+
|
12
|
+
[![Gem Version](https://badge.fury.io/rb/siresta.png)](https://badge.fury.io/rb/siresta)
|
13
|
+
|
14
|
+
## Description
|
15
|
+
[]: {{{1
|
16
|
+
|
17
|
+
siRESTa - declarative REST APIs
|
18
|
+
|
19
|
+
siRESTa is a DSL for declarative REST APIs. It can generate a ruby
|
20
|
+
API (w/ sinatra [1]) and Client (w/ excon [2]) for you, based on a
|
21
|
+
YAML file. Processing requests is done using a monad.
|
22
|
+
|
23
|
+
More documentation is underway. For now, see `example/` and
|
24
|
+
`features/`.
|
25
|
+
|
26
|
+
...
|
27
|
+
|
28
|
+
[]: }}}1
|
29
|
+
|
30
|
+
## Examples
|
31
|
+
[]: {{{1
|
32
|
+
|
33
|
+
```yaml
|
34
|
+
name: FooBarBaz
|
35
|
+
version: v1
|
36
|
+
request_formats: [json, xml]
|
37
|
+
response_formats: [json, xml]
|
38
|
+
api:
|
39
|
+
- resource: foos
|
40
|
+
contains:
|
41
|
+
- desc: Gets foos
|
42
|
+
get: get_foos
|
43
|
+
- post: create_foo
|
44
|
+
- resource: :foo_id
|
45
|
+
contains:
|
46
|
+
- desc: Get a foo
|
47
|
+
get: get_foo
|
48
|
+
- put: create_foo
|
49
|
+
- delete: delete_foo
|
50
|
+
- resource: bars
|
51
|
+
contains:
|
52
|
+
- get: get_bars
|
53
|
+
# ...
|
54
|
+
- resource: baz
|
55
|
+
contains:
|
56
|
+
- get: get_baz
|
57
|
+
# ...
|
58
|
+
```
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
require 'siresta'
|
62
|
+
API = Siresta.api file: 'config/api.yml'
|
63
|
+
class API
|
64
|
+
data :foos, []
|
65
|
+
|
66
|
+
handle :get_foos do |m, h, p, b|
|
67
|
+
m.get_data(:foos) { |foos| m.ok foos }
|
68
|
+
end
|
69
|
+
|
70
|
+
# ...
|
71
|
+
end
|
72
|
+
API.run!
|
73
|
+
```
|
74
|
+
|
75
|
+
```
|
76
|
+
GET /foos
|
77
|
+
POST /foos
|
78
|
+
GET /foos/:foo_id
|
79
|
+
PUT /foos/:foo_id
|
80
|
+
DELETE /foos/:foo_id
|
81
|
+
GET /foos/:foo_id/bars
|
82
|
+
GET /baz
|
83
|
+
...
|
84
|
+
```
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
require 'siresta'
|
88
|
+
Client = Siresta.client
|
89
|
+
c = Client.new 'http://localhost:4567'
|
90
|
+
|
91
|
+
c.foos.get
|
92
|
+
c.foos.post headers: { 'Content-Type' => 'foo/bar' }
|
93
|
+
c.foos[some_foo_id].get query: { foo: 'bar' }
|
94
|
+
c.foos[some_foo_id].put
|
95
|
+
c.foos[some_foo_id].delete
|
96
|
+
c.foos[some_foo_id].bars.get
|
97
|
+
c.baz.get
|
98
|
+
```
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
require 'siresta'
|
102
|
+
Siresta.routes
|
103
|
+
# => [["GET", "/foos", "Gets foos" ],
|
104
|
+
# ["POST", "/foos", nil ],
|
105
|
+
# ["GET", "/foos/:foo_id", "Get a foo" ],
|
106
|
+
# ["PUT", "/foos/:foo_id", nil ],
|
107
|
+
# ["DELETE", "/foos/:foo_id", nil ],
|
108
|
+
# ["GET", "/foos/:foo_id/bars", nil ],
|
109
|
+
# ["GET", "/baz", nil ]]
|
110
|
+
```
|
111
|
+
|
112
|
+
[]: }}}1
|
113
|
+
|
114
|
+
## Specs & Docs
|
115
|
+
|
116
|
+
```bash
|
117
|
+
$ rake cuke
|
118
|
+
$ rake docs
|
119
|
+
```
|
120
|
+
|
121
|
+
## TODO
|
122
|
+
|
123
|
+
* finish monad, api
|
124
|
+
* specs
|
125
|
+
* docs
|
126
|
+
* authorization?
|
127
|
+
* authentication?
|
128
|
+
|
129
|
+
## License
|
130
|
+
|
131
|
+
LGPLv3+ [3].
|
132
|
+
|
133
|
+
## References
|
134
|
+
|
135
|
+
[1] Sinatra
|
136
|
+
--- http://www.sinatrarb.com
|
137
|
+
|
138
|
+
[2] Excon
|
139
|
+
--- https://github.com/excon/excon
|
140
|
+
|
141
|
+
[3] GNU Lesser General Public License, version 3
|
142
|
+
--- http://www.gnu.org/licenses/lgpl-3.0.html
|
143
|
+
|
144
|
+
[]: ! ( vim: set tw=70 sw=2 sts=2 et fdm=marker : )
|
data/Rakefile
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
cuke = ENV['CUKE']
|
2
|
+
|
3
|
+
desc 'Run cucumber'
|
4
|
+
task :cuke do
|
5
|
+
sh "cucumber -fprogress #{cuke}"
|
6
|
+
end
|
7
|
+
|
8
|
+
desc 'Run cucumber strictly'
|
9
|
+
task 'cuke:strict' do
|
10
|
+
sh "cucumber -fprogress -S #{cuke}"
|
11
|
+
end
|
12
|
+
|
13
|
+
desc 'Run cucumber verbosely'
|
14
|
+
task 'cuke:verbose' do
|
15
|
+
sh "cucumber #{cuke}"
|
16
|
+
end
|
17
|
+
|
18
|
+
desc 'Run cucumber verbosely, view w/ less'
|
19
|
+
task 'cuke:less' do
|
20
|
+
sh "cucumber -c #{cuke} | less -R"
|
21
|
+
end
|
22
|
+
|
23
|
+
desc 'Cucumber step defs'
|
24
|
+
task 'cuke:steps' do
|
25
|
+
sh 'cucumber -c -fstepdefs | less -R'
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
desc 'Check for warnings'
|
30
|
+
task :warn do
|
31
|
+
sh 'ruby -w -I lib -r siresta -e ""' # TODO
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
desc 'Generate docs'
|
36
|
+
task :docs do
|
37
|
+
sh 'yardoc | cat'
|
38
|
+
end
|
39
|
+
|
40
|
+
desc 'List undocumented objects'
|
41
|
+
task 'docs:undoc' do
|
42
|
+
sh 'yard stats --list-undoc'
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
desc 'Cleanup'
|
47
|
+
task :clean do
|
48
|
+
sh 'rm -rf .yardoc/ doc/ *.gem examples/*/db/*.sqlite3'
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
desc 'Build SNAPSHOT gem'
|
53
|
+
task :snapshot do
|
54
|
+
v = Time.new.strftime '%Y%m%d%H%M%S'
|
55
|
+
f = 'lib/siresta/version.rb'
|
56
|
+
sh "sed -ri~ 's!(SNAPSHOT)!\\1.#{v}!' #{f}"
|
57
|
+
sh 'gem build siresta.gemspec'
|
58
|
+
end
|
59
|
+
|
60
|
+
desc 'Undo SNAPSHOT gem'
|
61
|
+
task 'snapshot:undo' do
|
62
|
+
sh 'git checkout -- lib/siresta/version.rb'
|
63
|
+
end
|
data/lib/siresta.rb
ADDED
data/lib/siresta/api.rb
ADDED
@@ -0,0 +1,190 @@
|
|
1
|
+
# -- ; {{{1
|
2
|
+
#
|
3
|
+
# File : siresta/api.rb
|
4
|
+
# Maintainer : Felix C. Stegerman <flx@obfusk.net>
|
5
|
+
# Date : 2014-06-17
|
6
|
+
#
|
7
|
+
# Copyright : Copyright (C) 2014 Felix C. Stegerman
|
8
|
+
# Licence : LGPLv3+
|
9
|
+
#
|
10
|
+
# -- ; }}}1
|
11
|
+
|
12
|
+
require 'json'
|
13
|
+
require 'obfusk/atom'
|
14
|
+
require 'sinatra/base'
|
15
|
+
require 'siresta/response'
|
16
|
+
require 'siresta/spec'
|
17
|
+
|
18
|
+
module Siresta
|
19
|
+
module API
|
20
|
+
# handle request for generated route
|
21
|
+
def _handle_request(meth, path, formats, pipe, handler)
|
22
|
+
b = method handler
|
23
|
+
r = Response
|
24
|
+
fs = [] # TODO
|
25
|
+
|
26
|
+
fs << r.choose_request_format(handler, formats) \
|
27
|
+
unless formats[:request].empty?
|
28
|
+
fs << r.get
|
29
|
+
fs << -> s { b[r, s.request.headers, s.request.params, s.request.body] }
|
30
|
+
fs << r.choose_response_format(handler, formats) \
|
31
|
+
unless formats[:response].empty?
|
32
|
+
|
33
|
+
x = r.pipeline(r.return(nil), *fs)
|
34
|
+
s = x.exec _begin_state
|
35
|
+
|
36
|
+
# TODO
|
37
|
+
if s.status.is_a? r::ResponseError
|
38
|
+
[500, s.status.message]
|
39
|
+
elsif s.response.data.is_a? r::ResponseBody
|
40
|
+
[s.response.status, s.response.headers, s.response.data.data]
|
41
|
+
else
|
42
|
+
raise 'OOPS' # TODO
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# begin state for Response monad
|
47
|
+
def _begin_state
|
48
|
+
r = Response
|
49
|
+
r.ResponseState(
|
50
|
+
r.RequestData(
|
51
|
+
{}, # TODO: headers
|
52
|
+
request.body.read, params
|
53
|
+
),
|
54
|
+
r.ResponseInfo(nil, {}, r.ResponseEmpty()),
|
55
|
+
r.ResponseContinue(), self
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
# preferred formats
|
60
|
+
def preferred_formats(formats)
|
61
|
+
@preferred_formats ||= {}
|
62
|
+
return @preferred_formats[formats] if @preferred_formats[formats]
|
63
|
+
fmts = formats[:request] | formats[:response]
|
64
|
+
m2f = Hash[fmts.map { |x| [mime_type(x), x] }]
|
65
|
+
f_in = m2f[request.content_type] || formats[:request].first
|
66
|
+
f_out = m2f[request.preferred_type m2f.keys]
|
67
|
+
@preferred_formats[formats] = [f_in, f_out]
|
68
|
+
end
|
69
|
+
|
70
|
+
# convert from preferred format
|
71
|
+
def convert_from(handler, formats, body)
|
72
|
+
f_in, f_out = preferred_formats formats
|
73
|
+
ss = settings.siresta[:convert_from][f_in] || {}
|
74
|
+
f = ss[handler] || ss[:__all__]
|
75
|
+
f ? f[body] : raise('OOPS') # TODO
|
76
|
+
end
|
77
|
+
|
78
|
+
# convert to preferred format
|
79
|
+
def convert_to(handler, formats, body)
|
80
|
+
f_in, f_out = preferred_formats formats
|
81
|
+
ss = settings.siresta[:convert_to][f_out] || {}
|
82
|
+
f = ss[handler] || ss[:__all__]
|
83
|
+
f ? f[body] : raise('OOPS') # TODO
|
84
|
+
end
|
85
|
+
|
86
|
+
# atoms
|
87
|
+
def data
|
88
|
+
settings.siresta[:data]
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.included(base)
|
92
|
+
base.extend ClassMethods
|
93
|
+
end
|
94
|
+
|
95
|
+
module ClassMethods
|
96
|
+
# generate route
|
97
|
+
def _gen_route(meth, path, formats, pipe, handler)
|
98
|
+
send(meth, path) do
|
99
|
+
_handle_request meth, path, formats, pipe, handler
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# named handler
|
104
|
+
def handle(name, &b)
|
105
|
+
define_method name, &b
|
106
|
+
end
|
107
|
+
|
108
|
+
# declare data (atom)
|
109
|
+
def data(k, v)
|
110
|
+
settings.siresta[:data][k] = Obfusk.atom v
|
111
|
+
end
|
112
|
+
|
113
|
+
# handle authorization
|
114
|
+
def to_authorize(handler, &b)
|
115
|
+
settings.siresta[:authorize][handler] = b
|
116
|
+
end
|
117
|
+
|
118
|
+
# convert body from format
|
119
|
+
def to_convert_from(format, handler = :__all__, &b)
|
120
|
+
(settings.siresta[:convert_from][format] ||= {})[handler] = b
|
121
|
+
end
|
122
|
+
|
123
|
+
# convert params
|
124
|
+
def to_convert_params(handler, &b)
|
125
|
+
settings.siresta[:convert_params][handler] = b
|
126
|
+
end
|
127
|
+
|
128
|
+
# convert body to format
|
129
|
+
def to_convert_to(format, handler = :__all__, &b)
|
130
|
+
(settings.siresta[:convert_to][format] ||= {})[handler] = b
|
131
|
+
end
|
132
|
+
|
133
|
+
# validate body
|
134
|
+
def to_validate_body(handler, &b)
|
135
|
+
settings.siresta[:validate_body][handler] = b
|
136
|
+
end
|
137
|
+
|
138
|
+
# validate params
|
139
|
+
def to_validate_params(handler, &b)
|
140
|
+
settings.siresta[:validate_params][handler] = b
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
class ApiBase < Sinatra::Base
|
146
|
+
include Siresta::API
|
147
|
+
|
148
|
+
set :siresta, { data: {}, authorize: {}, convert_from: {},
|
149
|
+
convert_params: {}, convert_to: {},
|
150
|
+
validate_body: {}, validate_params: {} }
|
151
|
+
|
152
|
+
to_convert_from :json do |body|
|
153
|
+
body.empty? ? nil : JSON.parse(body)
|
154
|
+
end
|
155
|
+
|
156
|
+
to_convert_to :json do |data|
|
157
|
+
data.to_json
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# generate an API (Sinatra::Base subclass) based on a YAML
|
162
|
+
# description
|
163
|
+
def self.api(opts = {})
|
164
|
+
api = Class.new ApiBase
|
165
|
+
Spec.walk api_spec(opts), {
|
166
|
+
root: -> (info) {
|
167
|
+
api.class_eval do
|
168
|
+
enable :sessions if info[:sessions] # TODO
|
169
|
+
set :name , info[:name]
|
170
|
+
set :version, info[:version]
|
171
|
+
end
|
172
|
+
api
|
173
|
+
},
|
174
|
+
resource: -> (info) {
|
175
|
+
api.class_eval do
|
176
|
+
info[:methods].each do |m|
|
177
|
+
formats = info[:specs][m][:formats]
|
178
|
+
symfmts = Hash[formats.map { |k,v| [k,v.map(&:to_sym)] }]
|
179
|
+
_gen_route m.to_sym, info[:path], symfmts,
|
180
|
+
info[:specs][m]['pipeline'], info[:specs][m][m].to_sym
|
181
|
+
end
|
182
|
+
end
|
183
|
+
nil
|
184
|
+
},
|
185
|
+
subresource: -> (_) {}, parametrized_subresource: -> (_) {},
|
186
|
+
}
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# vim: set tw=70 sw=2 sts=2 et fdm=marker :
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# -- ; {{{1
|
2
|
+
#
|
3
|
+
# File : siresta/client.rb
|
4
|
+
# Maintainer : Felix C. Stegerman <flx@obfusk.net>
|
5
|
+
# Date : 2014-06-17
|
6
|
+
#
|
7
|
+
# Copyright : Copyright (C) 2014 Felix C. Stegerman
|
8
|
+
# Licence : LGPLv3+
|
9
|
+
#
|
10
|
+
# -- ; }}}1
|
11
|
+
|
12
|
+
require 'excon'
|
13
|
+
require 'siresta/spec'
|
14
|
+
|
15
|
+
module Siresta
|
16
|
+
module Client
|
17
|
+
# (sub)resource: wraps url, gets extendes as-needed w/ `.post`,
|
18
|
+
# `.get`, `.put`, `.delete`, `.some_resource`, `[some_param]` etc.
|
19
|
+
# Route methods take the same arguments as Excon's.
|
20
|
+
class Resource
|
21
|
+
attr_reader :url
|
22
|
+
def initialize(url, *path)
|
23
|
+
@url = ([url]+path)*'/'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# generate a client (Excon wrapper) based on a YAML description
|
29
|
+
def self.client(opts = {})
|
30
|
+
opts_ = opts.dup
|
31
|
+
http_client = opts_.delete(:http_client) || Excon
|
32
|
+
Spec.walk api_spec(opts_), {
|
33
|
+
root: -> (info) {
|
34
|
+
info[:res].class_eval do
|
35
|
+
define_method(:name) { info[:name] }
|
36
|
+
define_method(:version) { info[:version] }
|
37
|
+
end
|
38
|
+
info[:res]
|
39
|
+
},
|
40
|
+
resource: -> (info) {
|
41
|
+
res = Class.new Client::Resource
|
42
|
+
res.class_eval do
|
43
|
+
info[:methods].map(&:to_sym).each do |m|
|
44
|
+
# resource.{get,post,put,delete}
|
45
|
+
define_method(m) do |*params, &b|
|
46
|
+
# TODO: quoting
|
47
|
+
http_client.send m, url, *params, &b
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
res
|
52
|
+
},
|
53
|
+
subresource: -> (info) {
|
54
|
+
info[:res].class_eval do
|
55
|
+
# resource.some_route
|
56
|
+
define_method(info[:route].to_sym) do
|
57
|
+
info[:sub].new url, info[:route]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
},
|
61
|
+
parametrized_subresource: -> (info) {
|
62
|
+
info[:res].class_eval do
|
63
|
+
# resource[some_param]
|
64
|
+
define_method(:[]) { |param| info[:sub].new url, param }
|
65
|
+
end
|
66
|
+
},
|
67
|
+
}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# vim: set tw=70 sw=2 sts=2 et fdm=marker :
|
data/lib/siresta/env.rb
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
# -- ; {{{1
|
2
|
+
#
|
3
|
+
# File : siresta/response.rb
|
4
|
+
# Maintainer : Felix C. Stegerman <flx@obfusk.net>
|
5
|
+
# Date : 2014-06-17
|
6
|
+
#
|
7
|
+
# Copyright : Copyright (C) 2014 Felix C. Stegerman
|
8
|
+
# Licence : LGPLv3+
|
9
|
+
#
|
10
|
+
# -- ; }}}1
|
11
|
+
|
12
|
+
require 'obfusk/adt'
|
13
|
+
require 'obfusk/monads'
|
14
|
+
|
15
|
+
module Siresta
|
16
|
+
class Response < Obfusk::Monads::State
|
17
|
+
class ResponseState
|
18
|
+
include Obfusk::ADT
|
19
|
+
constructor :ResponseState,
|
20
|
+
:request, :response, :status, :api_obj
|
21
|
+
end
|
22
|
+
|
23
|
+
class RequestData
|
24
|
+
include Obfusk::ADT
|
25
|
+
constructor :RequestData, :headers, :body, :params # ...
|
26
|
+
end
|
27
|
+
|
28
|
+
class ResponseInfo
|
29
|
+
include Obfusk::ADT
|
30
|
+
constructor :ResponseInfo, :status, :headers, :data
|
31
|
+
end
|
32
|
+
|
33
|
+
class ResponseData
|
34
|
+
include Obfusk::ADT
|
35
|
+
constructor :ResponseEmpty
|
36
|
+
constructor :ResponseBody, :data
|
37
|
+
constructor :ResponseStream, :handle
|
38
|
+
end
|
39
|
+
|
40
|
+
class ResponseStatus
|
41
|
+
include Obfusk::ADT
|
42
|
+
constructor :ResponseContinue
|
43
|
+
constructor :ResponseDone
|
44
|
+
constructor :ResponseError, :message
|
45
|
+
end
|
46
|
+
|
47
|
+
[ ResponseState, RequestData, ResponseInfo ] \
|
48
|
+
.each { |x| x.import_constructors self, false }
|
49
|
+
|
50
|
+
[ ResponseData, ResponseStatus ] \
|
51
|
+
.each { |x| x.import_constructors self }
|
52
|
+
|
53
|
+
# -- constructor helpers --
|
54
|
+
|
55
|
+
# add error to state
|
56
|
+
def self._error(s, msg)
|
57
|
+
s.clone(status: ResponseError(msg))
|
58
|
+
end
|
59
|
+
|
60
|
+
# call block with state if error (to short-circuit)
|
61
|
+
def self._on_error(s, &b)
|
62
|
+
b[s] if s.status.is_a? ResponseError
|
63
|
+
end
|
64
|
+
|
65
|
+
# call block with error state if done
|
66
|
+
def self._on_done(s, &b)
|
67
|
+
b[_error(s, 'cannot continue when done')] \
|
68
|
+
if s.status.is_a? ResponseDone
|
69
|
+
end
|
70
|
+
|
71
|
+
# call block with error state if empty
|
72
|
+
def self._on_empty(s, &b)
|
73
|
+
b[_error(s, 'cannot use empty body')] \
|
74
|
+
if s.response.data.is_a? ResponseEmpty
|
75
|
+
end
|
76
|
+
|
77
|
+
# call block with error state if stream
|
78
|
+
def self._on_stream(s, &b)
|
79
|
+
b[_error(s, 'cannot use body response')] \
|
80
|
+
if s.response.data.is_a? ResponseStream
|
81
|
+
end
|
82
|
+
|
83
|
+
# -- constructors --
|
84
|
+
|
85
|
+
# everything ok
|
86
|
+
def self.ok(body, headers = {}, status = 200)
|
87
|
+
modify -> s {
|
88
|
+
_on_error(s) { |t| return t }
|
89
|
+
_on_done(s) { |t| return t }
|
90
|
+
_on_stream(s) { |t| return t }
|
91
|
+
s.clone(
|
92
|
+
response: s.response.clone(
|
93
|
+
status: status, headers: s.response.headers.merge(headers),
|
94
|
+
data: ResponseBody(body)
|
95
|
+
),
|
96
|
+
status: ResponseContinue()
|
97
|
+
)
|
98
|
+
}
|
99
|
+
end
|
100
|
+
|
101
|
+
# everyting ok, stream data
|
102
|
+
def self.stream(data, headers = {}, status = 200)
|
103
|
+
# TODO
|
104
|
+
end
|
105
|
+
|
106
|
+
# everything ok, stream data (keep open)
|
107
|
+
def self.stream_keep_open
|
108
|
+
# TODO
|
109
|
+
end
|
110
|
+
|
111
|
+
# error!
|
112
|
+
def self.error(msg)
|
113
|
+
modify { |s| _error s, msg }
|
114
|
+
end
|
115
|
+
|
116
|
+
# ...
|
117
|
+
|
118
|
+
# -- helpers --
|
119
|
+
|
120
|
+
# get atom data
|
121
|
+
def self.get_data(k, &b)
|
122
|
+
x = get >> -> s { mreturn s.api_obj.data[k]._ }; b ? x >> b : x
|
123
|
+
end
|
124
|
+
|
125
|
+
# set atom data
|
126
|
+
def self.set_data(k, v)
|
127
|
+
get >> -> s { s.api_obj.data[k].swap! { |_| v }; mreturn nil }
|
128
|
+
end
|
129
|
+
|
130
|
+
# authorization
|
131
|
+
def self.authorize(handler)
|
132
|
+
# TODO
|
133
|
+
end
|
134
|
+
|
135
|
+
# convert parameters
|
136
|
+
def self.convert_params(handler)
|
137
|
+
# TODO
|
138
|
+
end
|
139
|
+
|
140
|
+
# validate body
|
141
|
+
def self.validate_body(handler)
|
142
|
+
# TODO
|
143
|
+
end
|
144
|
+
|
145
|
+
# validate parameters
|
146
|
+
def self.validate_params(handler)
|
147
|
+
# TODO
|
148
|
+
end
|
149
|
+
|
150
|
+
# -- formats --
|
151
|
+
|
152
|
+
# convert request body from appropriate format
|
153
|
+
def self.choose_request_format(handler, formats)
|
154
|
+
modify -> s {
|
155
|
+
_on_error(s) { |t| return t }
|
156
|
+
_on_done(s) { |t| return t }
|
157
|
+
b = s.api_obj.convert_from(handler, formats, s.request.body)
|
158
|
+
s.clone(request: s.request.clone(body: b))
|
159
|
+
}
|
160
|
+
end
|
161
|
+
|
162
|
+
# convert response body to appropriate format
|
163
|
+
def self.choose_response_format(handler, formats)
|
164
|
+
modify -> s {
|
165
|
+
_on_error(s) { |t| return t }
|
166
|
+
_on_done(s) { |t| return t }
|
167
|
+
_on_empty(s) { |t| return t }
|
168
|
+
_on_stream(s) { |t| return t }
|
169
|
+
b = s.api_obj.convert_to(handler, formats, s.response.data.data)
|
170
|
+
s.clone(response: s.response.clone(data: ResponseBody(b)))
|
171
|
+
}
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# vim: set tw=70 sw=2 sts=2 et fdm=marker :
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# -- ; {{{1
|
2
|
+
#
|
3
|
+
# File : siresta/routes.rb
|
4
|
+
# Maintainer : Felix C. Stegerman <flx@obfusk.net>
|
5
|
+
# Date : 2014-06-13
|
6
|
+
#
|
7
|
+
# Copyright : Copyright (C) 2014 Felix C. Stegerman
|
8
|
+
# Licence : LGPLv3+
|
9
|
+
#
|
10
|
+
# -- ; }}}1
|
11
|
+
|
12
|
+
require 'siresta/spec'
|
13
|
+
|
14
|
+
module Siresta
|
15
|
+
# get routes from YAML description
|
16
|
+
def self.routes(opts = {})
|
17
|
+
routes = []
|
18
|
+
Spec.walk api_spec(opts), {
|
19
|
+
resource: -> (info) {
|
20
|
+
info[:methods].each do |m|
|
21
|
+
routes << [m.upcase, info[:path], info[:specs][m]['desc']]
|
22
|
+
end
|
23
|
+
nil
|
24
|
+
},
|
25
|
+
root: -> (_) {}, subresource: -> (_) {},
|
26
|
+
parametrized_subresource: -> (_) {},
|
27
|
+
}
|
28
|
+
routes
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# vim: set tw=70 sw=2 sts=2 et fdm=marker :
|
data/lib/siresta/spec.rb
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
# -- ; {{{1
|
2
|
+
#
|
3
|
+
# File : siresta/spec.rb
|
4
|
+
# Maintainer : Felix C. Stegerman <flx@obfusk.net>
|
5
|
+
# Date : 2014-06-17
|
6
|
+
#
|
7
|
+
# Copyright : Copyright (C) 2014 Felix C. Stegerman
|
8
|
+
# Licence : LGPLv3+
|
9
|
+
#
|
10
|
+
# -- ; }}}1
|
11
|
+
|
12
|
+
require 'yaml'
|
13
|
+
|
14
|
+
module Siresta
|
15
|
+
DEFAULT_API_YAML = 'config/api.yml'
|
16
|
+
|
17
|
+
module Spec
|
18
|
+
METHODS = %w{ post get put delete }
|
19
|
+
|
20
|
+
# walk spec
|
21
|
+
def self.walk(spec, opts)
|
22
|
+
name = spec['name']
|
23
|
+
version = spec['version']
|
24
|
+
sessions = spec['sessions']
|
25
|
+
formats = { request: spec['request_formats'] || [],
|
26
|
+
response: spec['response_formats'] || [] }
|
27
|
+
res = walk_resource spec['api'], '/', opts, formats
|
28
|
+
opts[:root][{
|
29
|
+
res: res, name: name, version: version, sessions: sessions
|
30
|
+
}]
|
31
|
+
end
|
32
|
+
|
33
|
+
# process resource when walking spec
|
34
|
+
def self.walk_resource(specs, path, opts, formats)
|
35
|
+
ms, ss = specs.inject([[],{}]) do |(ms,ss), spec|
|
36
|
+
chs = formats.merge({ request: spec['request_formats'],
|
37
|
+
response: spec['response_formats'] }
|
38
|
+
.reject { |k,v| !v })
|
39
|
+
(m = (METHODS & spec.keys).first) ?
|
40
|
+
[ms + [m], ss.merge(m => spec.merge(formats: chs))] : [ms,ss]
|
41
|
+
end
|
42
|
+
opts[:resource][{ methods: ms, specs: ss, path: path }] \
|
43
|
+
.tap { |res| walk_subresources res, specs, path, opts, formats }
|
44
|
+
end
|
45
|
+
|
46
|
+
# process subresources when walking spec
|
47
|
+
def self.walk_subresources(res, specs, path, opts, formats)
|
48
|
+
specs.each do |spec|
|
49
|
+
if (r = spec['resource'])
|
50
|
+
r_s = (p = Symbol === r) ? ":#{r}" : r
|
51
|
+
chs = formats.merge({ request: spec['request_formats'],
|
52
|
+
response: spec['response_formats'] }
|
53
|
+
.reject { |k,v| !v })
|
54
|
+
pth = (path == '/' ? '' : path) + '/' + r_s
|
55
|
+
sub = walk_resource spec['contains'], pth, opts, chs
|
56
|
+
opts[p ? :parametrized_subresource : :subresource][
|
57
|
+
{ res: res, sub: sub, parametrized: p, route: r,
|
58
|
+
route_s: r_s, path: path }
|
59
|
+
]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# get (cached) API spec
|
66
|
+
# @param [Hash] opts options
|
67
|
+
# @option opts [String] :data YAML data, or:
|
68
|
+
# @option opts [String] :file file name
|
69
|
+
def self.api_spec(opts = {})
|
70
|
+
if opts[:data]
|
71
|
+
YAML.load opts[:data]
|
72
|
+
else
|
73
|
+
file = opts[:file] || DEFAULT_API_YAML
|
74
|
+
(@api_spec ||= {})[file] ||= api_spec data: File.read(file)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# vim: set tw=70 sw=2 sts=2 et fdm=marker :
|
data/lib/siresta/xml.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
# -- ; {{{1
|
2
|
+
#
|
3
|
+
# File : siresta/xml.rb
|
4
|
+
# Maintainer : Felix C. Stegerman <flx@obfusk.net>
|
5
|
+
# Date : 2014-06-13
|
6
|
+
#
|
7
|
+
# Copyright : Copyright (C) 2014 Felix C. Stegerman
|
8
|
+
# Licence : LGPLv3+
|
9
|
+
#
|
10
|
+
# -- ; }}}1
|
11
|
+
|
12
|
+
# TODO: move to separate lib
|
13
|
+
|
14
|
+
require 'ox'
|
15
|
+
|
16
|
+
module Siresta
|
17
|
+
module XML
|
18
|
+
# parse XML
|
19
|
+
#
|
20
|
+
# ```ruby
|
21
|
+
# Siresta::XML.parse '<foo><bar id="99">hi!</bar></foo>'
|
22
|
+
# # => { tag: 'foo', attrs: {}, contents: [
|
23
|
+
# # { tag: 'bar', attrs: { id: 99 }, contents: ['hi!'] }
|
24
|
+
# # ] }
|
25
|
+
# ```
|
26
|
+
def self.parse(xml)
|
27
|
+
ox_elem = Ox.parse xml
|
28
|
+
_parse Ox::Document === ox_elem ? ox_elem.root : ox_elem
|
29
|
+
end
|
30
|
+
|
31
|
+
def self._parse(ox_elem)
|
32
|
+
if String === ox_elem
|
33
|
+
ox_elem
|
34
|
+
else
|
35
|
+
contents = ox_elem.nodes.map { |n| _parse n }
|
36
|
+
{ tag: ox_elem.name, attrs: ox_elem.attributes, contents: contents }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# emit XML
|
41
|
+
#
|
42
|
+
# ```ruby
|
43
|
+
# puts Siresta::XML.emit(
|
44
|
+
# { tag: 'foo', attrs: {}, contents: [
|
45
|
+
# { tag: 'bar', attrs: { id: 99 }, contents: ['hi!'] }
|
46
|
+
# ] }
|
47
|
+
# )
|
48
|
+
# # <?xml version="1.0"?>
|
49
|
+
# # <foo>
|
50
|
+
# # <bar id="99">hi!</bar>
|
51
|
+
# # </foo>
|
52
|
+
# ```
|
53
|
+
def self.emit(elem, opts = {})
|
54
|
+
ox_doc = Ox::Document.new version: '1.0'
|
55
|
+
_emit ox_doc, elem
|
56
|
+
Ox.dump ox_doc, { with_xml: true }.merge(opts)
|
57
|
+
end
|
58
|
+
|
59
|
+
def self._emit(ox_doc, elem)
|
60
|
+
if String === elem
|
61
|
+
ox_doc << elem
|
62
|
+
else
|
63
|
+
ox_elem = Ox::Element.new elem[:tag]
|
64
|
+
elem[:attrs].each_pair { |k,v| ox_elem[k] = v }
|
65
|
+
ox_doc << ox_elem
|
66
|
+
elem[:contents].each { |e| _emit ox_elem, e }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# vim: set tw=70 sw=2 sts=2 et fdm=marker :
|
data/siresta.gemspec
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require File.expand_path('../lib/siresta/version', __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = 'siresta'
|
5
|
+
s.homepage = 'https://github.com/obfusk/siresta'
|
6
|
+
s.summary = 'siRESTa - declarative REST APIs'
|
7
|
+
|
8
|
+
s.description = <<-END.gsub(/^ {4}/, '')
|
9
|
+
siRESTa is a DSL for declarative REST APIs. It can generate a
|
10
|
+
ruby API (w/ sinatra) and Client (w/ excon) for you, based on a
|
11
|
+
YAML file. Processing requests is done using a monad.
|
12
|
+
END
|
13
|
+
|
14
|
+
s.version = Siresta::VERSION
|
15
|
+
s.date = Siresta::DATE
|
16
|
+
|
17
|
+
s.authors = [ 'Felix C. Stegerman' ]
|
18
|
+
s.email = %w{ flx@obfusk.net }
|
19
|
+
|
20
|
+
s.licenses = %w{ LGPLv3+ }
|
21
|
+
|
22
|
+
s.files = %w{ .yardopts README.md Rakefile siresta.gemspec } \
|
23
|
+
+ Dir['lib/**/*.rb']
|
24
|
+
|
25
|
+
s.add_runtime_dependency 'excon'
|
26
|
+
# s.add_runtime_dependency 'hashie'
|
27
|
+
s.add_runtime_dependency 'obfusk', '>= 0.1.1'
|
28
|
+
s.add_runtime_dependency 'ox'
|
29
|
+
s.add_runtime_dependency 'sinatra'
|
30
|
+
|
31
|
+
s.add_development_dependency 'cucumber'
|
32
|
+
s.add_development_dependency 'rake'
|
33
|
+
s.add_development_dependency 'yard'
|
34
|
+
|
35
|
+
s.required_ruby_version = '>= 1.9.1'
|
36
|
+
end
|
metadata
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: siresta
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Felix C. Stegerman
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-06-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: excon
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: obfusk
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.1.1
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.1.1
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: ox
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: sinatra
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: cucumber
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rake
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: yard
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
description: |
|
112
|
+
siRESTa is a DSL for declarative REST APIs. It can generate a
|
113
|
+
ruby API (w/ sinatra) and Client (w/ excon) for you, based on a
|
114
|
+
YAML file. Processing requests is done using a monad.
|
115
|
+
email:
|
116
|
+
- flx@obfusk.net
|
117
|
+
executables: []
|
118
|
+
extensions: []
|
119
|
+
extra_rdoc_files: []
|
120
|
+
files:
|
121
|
+
- ".yardopts"
|
122
|
+
- README.md
|
123
|
+
- Rakefile
|
124
|
+
- lib/siresta.rb
|
125
|
+
- lib/siresta/api.rb
|
126
|
+
- lib/siresta/client.rb
|
127
|
+
- lib/siresta/env.rb
|
128
|
+
- lib/siresta/response.rb
|
129
|
+
- lib/siresta/routes.rb
|
130
|
+
- lib/siresta/spec.rb
|
131
|
+
- lib/siresta/version.rb
|
132
|
+
- lib/siresta/xml.rb
|
133
|
+
- siresta.gemspec
|
134
|
+
homepage: https://github.com/obfusk/siresta
|
135
|
+
licenses:
|
136
|
+
- LGPLv3+
|
137
|
+
metadata: {}
|
138
|
+
post_install_message:
|
139
|
+
rdoc_options: []
|
140
|
+
require_paths:
|
141
|
+
- lib
|
142
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
143
|
+
requirements:
|
144
|
+
- - ">="
|
145
|
+
- !ruby/object:Gem::Version
|
146
|
+
version: 1.9.1
|
147
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - ">="
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '0'
|
152
|
+
requirements: []
|
153
|
+
rubyforge_project:
|
154
|
+
rubygems_version: 2.2.2
|
155
|
+
signing_key:
|
156
|
+
specification_version: 4
|
157
|
+
summary: siRESTa - declarative REST APIs
|
158
|
+
test_files: []
|
159
|
+
has_rdoc:
|