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.
@@ -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
@@ -0,0 +1 @@
1
+ --markup markdown
@@ -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 : )
@@ -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
@@ -0,0 +1,5 @@
1
+ require 'siresta/api'
2
+ require 'siresta/client'
3
+ require 'siresta/env'
4
+ require 'siresta/routes'
5
+ require 'siresta/version'
@@ -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 :
@@ -0,0 +1,6 @@
1
+ module Siresta
2
+ # environment
3
+ def self.env
4
+ ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'development'
5
+ end
6
+ end
@@ -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 :
@@ -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 :
@@ -0,0 +1,4 @@
1
+ module Siresta
2
+ VERSION = '0.0.2'
3
+ DATE = '2014-06-18'
4
+ end
@@ -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 :
@@ -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: