siresta 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 +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
|
+
[](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:
|