traim 0.1.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/.gitignore +3 -0
- data/LICENSE +19 -0
- data/README.md +272 -0
- data/lib/traim.rb +372 -0
- data/test/resource.rb +379 -0
- data/traim.gemspec +20 -0
- metadata +106 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 9a3cd06341db0633e95bded7bd7e6a45a5348511
|
4
|
+
data.tar.gz: 3ab56f6e5470c5fe0cb5693619a8251be0f3bf0c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a1d83bcf7a6c86db7f0d61ee147d1bcfebe3d4f913361443aca879d679b46cd3f7838cbcf1d6d84aa860c16c21f009afd6dda120fb5c544c8a26b93c9c1b38bc
|
7
|
+
data.tar.gz: f7afe6353577ad2ac32e698bd7d4cc6584ac4110bf80ac5c86b1bbd94584bfaaea04e9aa9482e3881278d60b960ceb6495c9545f7f55a08f177f467ead290f5a
|
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2017 Travis Liu
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,272 @@
|
|
1
|
+
# Traim
|
2
|
+
|
3
|
+
Traim is a microframework for Building a RESTful API service from your existing ActiveRecord models.
|
4
|
+
|
5
|
+
## Getting started
|
6
|
+
|
7
|
+
### Installation
|
8
|
+
``` ruby
|
9
|
+
gem install train
|
10
|
+
```
|
11
|
+
|
12
|
+
### Usage
|
13
|
+
Here's a simple application:
|
14
|
+
``` ruby
|
15
|
+
# cat hello_traim.rb
|
16
|
+
|
17
|
+
Traim.config do |app|
|
18
|
+
app.logger = Logger.new(STDOUT)
|
19
|
+
end
|
20
|
+
|
21
|
+
class User < ActiveRecord::Base
|
22
|
+
end
|
23
|
+
|
24
|
+
Train.application do
|
25
|
+
resource :users do
|
26
|
+
# Inject user model
|
27
|
+
model User
|
28
|
+
|
29
|
+
# Response json: {id: 1, name: “example"}
|
30
|
+
attribute :id
|
31
|
+
attribute :name
|
32
|
+
|
33
|
+
# POST /users
|
34
|
+
action :create
|
35
|
+
|
36
|
+
# GET /users/:id
|
37
|
+
action :show
|
38
|
+
|
39
|
+
# PUT /users/:id
|
40
|
+
action :update
|
41
|
+
|
42
|
+
# DELETE /users/:id
|
43
|
+
action :destory
|
44
|
+
end
|
45
|
+
end
|
46
|
+
```
|
47
|
+
put your activerecord config in
|
48
|
+
` config/database.yml `
|
49
|
+
|
50
|
+
To run it, you can create a config.ru file
|
51
|
+
``` ruby
|
52
|
+
# cat config.ru
|
53
|
+
require 'hello_traim.rb'
|
54
|
+
|
55
|
+
run Traim
|
56
|
+
```
|
57
|
+
|
58
|
+
Then run `rackup`.
|
59
|
+
|
60
|
+
Now, you already get basic CURD RESTful API from the user ActiveRecord model.
|
61
|
+
|
62
|
+
## Customizable action
|
63
|
+
By default, `action` can be easily used to create an endpoint for CRUD operations. you can write your own endpoint as well.
|
64
|
+
``` ruby
|
65
|
+
Traim.application do
|
66
|
+
resources :users do
|
67
|
+
model User
|
68
|
+
|
69
|
+
attribute :id
|
70
|
+
attribute :name
|
71
|
+
|
72
|
+
action :show do |params|
|
73
|
+
user = model.find_by_email params["payload"]["email"]
|
74
|
+
user.name = "[admin] #{user.name}"
|
75
|
+
user
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
```
|
80
|
+
|
81
|
+
Response
|
82
|
+
``` json
|
83
|
+
{"id": 1, "name": "[admin] travis"}
|
84
|
+
```
|
85
|
+
|
86
|
+
## Associations
|
87
|
+
create nestea json reponse with activerecord association
|
88
|
+
``` ruby
|
89
|
+
class User < ActiveRecord::Base
|
90
|
+
has_many :books
|
91
|
+
end
|
92
|
+
|
93
|
+
class Book < ActiveRecord::Base
|
94
|
+
belongs_to :user
|
95
|
+
end
|
96
|
+
|
97
|
+
Traim.application do
|
98
|
+
resources :users do
|
99
|
+
model User
|
100
|
+
|
101
|
+
attribute :id
|
102
|
+
attribute :name
|
103
|
+
|
104
|
+
action :show
|
105
|
+
|
106
|
+
has_many: books
|
107
|
+
end
|
108
|
+
|
109
|
+
resources :books do
|
110
|
+
model Book
|
111
|
+
|
112
|
+
attribute :isbn
|
113
|
+
end
|
114
|
+
end
|
115
|
+
```
|
116
|
+
|
117
|
+
Response
|
118
|
+
``` json
|
119
|
+
{
|
120
|
+
"id": 1,
|
121
|
+
"name": "travis"
|
122
|
+
"books": [
|
123
|
+
{"isbn": "978-1-61-729109-8"},
|
124
|
+
{"isbn": "561-6-28-209847-7"},
|
125
|
+
{"isbn": "527-3-83-394862-5"}
|
126
|
+
]
|
127
|
+
}
|
128
|
+
```
|
129
|
+
|
130
|
+
## Member
|
131
|
+
Member block can add actions to a specific record with id
|
132
|
+
``` Ruby
|
133
|
+
Traim.application do
|
134
|
+
resources :users do
|
135
|
+
model User
|
136
|
+
|
137
|
+
attribute :id
|
138
|
+
attribute :name
|
139
|
+
|
140
|
+
member :blurred do
|
141
|
+
|
142
|
+
# GET /users/1/blurred
|
143
|
+
show do |params|
|
144
|
+
record.name[1..2] = 'xx'
|
145
|
+
record
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
```
|
151
|
+
|
152
|
+
## Collection
|
153
|
+
Collection block can add actions to operate resources
|
154
|
+
``` Ruby
|
155
|
+
Traim.application do
|
156
|
+
resources :users do
|
157
|
+
model User
|
158
|
+
|
159
|
+
attribute :id
|
160
|
+
attribute :name
|
161
|
+
|
162
|
+
collection :admin do
|
163
|
+
|
164
|
+
# GET /users/admin
|
165
|
+
show do |params|
|
166
|
+
model.all
|
167
|
+
end
|
168
|
+
|
169
|
+
# POST /users/admin
|
170
|
+
create do |params|
|
171
|
+
model.create(params["payload"])
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
```
|
177
|
+
|
178
|
+
## Namespaces
|
179
|
+
Organize groups of resources under a namespace. Most commonly, you might arrange resources for versioning.
|
180
|
+
``` ruby
|
181
|
+
Traim.application do
|
182
|
+
namespace :api do
|
183
|
+
namespace :v1 do
|
184
|
+
resources :users do
|
185
|
+
model User
|
186
|
+
|
187
|
+
attribute :id
|
188
|
+
attribute :name
|
189
|
+
|
190
|
+
# endpoint: /api/v1/users
|
191
|
+
action :show
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
namespace :v2 do
|
196
|
+
resources :users do
|
197
|
+
model User
|
198
|
+
|
199
|
+
attribute :id
|
200
|
+
attribute :name
|
201
|
+
|
202
|
+
# endpoint: /api/v2/users
|
203
|
+
action :show
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
```
|
209
|
+
|
210
|
+
## Helpers
|
211
|
+
You can define helper methods that your endpoints can use with the helpers to deal with some common flow controls, like authentication or authorization.
|
212
|
+
``` ruby
|
213
|
+
Traim.application do
|
214
|
+
resources :users do
|
215
|
+
helpers do
|
216
|
+
def auth(user_id)
|
217
|
+
raise BadRequestError.new(message: "unauthenticated request") unless model.exists?(id: user_id)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
model User
|
222
|
+
|
223
|
+
attribute :id
|
224
|
+
attribute :name
|
225
|
+
|
226
|
+
action :show do |params|
|
227
|
+
auth(params["id"])
|
228
|
+
model.find params["id"]
|
229
|
+
user
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
```
|
234
|
+
|
235
|
+
## Visual attributes
|
236
|
+
Built-in attribute is generate response fields from model. Visual can help you present fields outside of model attributes.
|
237
|
+
``` ruby
|
238
|
+
Traim.application do
|
239
|
+
resources :users do
|
240
|
+
model User
|
241
|
+
|
242
|
+
attribute :id
|
243
|
+
attribute :name
|
244
|
+
|
245
|
+
attribute :vattr do |record|
|
246
|
+
"#{record.id} : #{record.email}"
|
247
|
+
end
|
248
|
+
|
249
|
+
action :show
|
250
|
+
end
|
251
|
+
end
|
252
|
+
```
|
253
|
+
|
254
|
+
Response
|
255
|
+
``` json
|
256
|
+
{"id": 1, "name": "travis", "vattr": "1 : travis"}
|
257
|
+
```
|
258
|
+
|
259
|
+
### Parameters whitelist
|
260
|
+
Built-in model operations are using mass assignment. For security concern, Parameters can be whitelisted with permit option.
|
261
|
+
``` ruby
|
262
|
+
Traim.application do
|
263
|
+
resources :users do
|
264
|
+
model User
|
265
|
+
|
266
|
+
attribute :id
|
267
|
+
attribute :name
|
268
|
+
|
269
|
+
action :create, permit: ["name"]
|
270
|
+
end
|
271
|
+
end
|
272
|
+
```
|
data/lib/traim.rb
ADDED
@@ -0,0 +1,372 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'rack'
|
3
|
+
require 'seg'
|
4
|
+
require 'yaml'
|
5
|
+
require 'logger'
|
6
|
+
require 'active_record'
|
7
|
+
|
8
|
+
class Traim
|
9
|
+
DEFAULT_HEADER = {"Content-Type" => 'application/json;charset=UTF-8'}
|
10
|
+
TRAIM_ENV = ENV['TRAIM_ENV'] || 'development'
|
11
|
+
|
12
|
+
def initialize(&block)
|
13
|
+
if self.class.logger == nil
|
14
|
+
self.class.logger = Logger.new(STDOUT)
|
15
|
+
self.class.logger.level = Logger::INFO
|
16
|
+
end
|
17
|
+
|
18
|
+
@app = Application.new
|
19
|
+
@app.compile(&block)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.settings; @settings ||= {} end
|
23
|
+
|
24
|
+
def self.application(&block)
|
25
|
+
@instance = new(&block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.logger=(logger); @logger = logger end
|
29
|
+
def self.logger; @logger end
|
30
|
+
|
31
|
+
def self.call(env)
|
32
|
+
@instance.dup.call(env)
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.config(&block)
|
36
|
+
config_file = YAML.load_file("#{Dir.pwd}/config/database.yml")
|
37
|
+
ActiveRecord::Base.establish_connection(config_file[TRAIM_ENV])
|
38
|
+
yield self
|
39
|
+
end
|
40
|
+
|
41
|
+
def logger; Traim.logger end
|
42
|
+
|
43
|
+
def call(env)
|
44
|
+
request = Rack::Request.new(env)
|
45
|
+
logger.info("#{request.request_method} #{request.path_info} from #{request.ip}")
|
46
|
+
logger.debug("Parameters: #{request.params}")
|
47
|
+
|
48
|
+
@app.route(request)
|
49
|
+
rescue Error => e
|
50
|
+
logger.error(e)
|
51
|
+
[e.status, e.header, [JSON.dump(e.body)]]
|
52
|
+
rescue Exception => e
|
53
|
+
logger.error(e)
|
54
|
+
error = Error.new
|
55
|
+
[error.status, error.header, [JSON.dump(error.body)]]
|
56
|
+
end
|
57
|
+
|
58
|
+
class Application
|
59
|
+
def logger; Traim.logger end
|
60
|
+
|
61
|
+
def initialize(name = :default)
|
62
|
+
@name = name
|
63
|
+
@resources = {}
|
64
|
+
@applications = {}
|
65
|
+
end
|
66
|
+
|
67
|
+
def resources(name, &block)
|
68
|
+
@resources[name] = Resource.new(block)
|
69
|
+
end
|
70
|
+
|
71
|
+
def namespace(name, &block)
|
72
|
+
logger.debug("application namespace #{name}")
|
73
|
+
application(name).compile(&block)
|
74
|
+
end
|
75
|
+
|
76
|
+
def application(name = :default)
|
77
|
+
logger.debug("Lunch application #{name}")
|
78
|
+
app = @applications[name] ||= Application.new(name)
|
79
|
+
end
|
80
|
+
|
81
|
+
def helpers(&block)
|
82
|
+
@helpers_block = block
|
83
|
+
end
|
84
|
+
|
85
|
+
def route(request, seg = nil)
|
86
|
+
inbox = {}
|
87
|
+
seg ||= Seg.new(request.path_info)
|
88
|
+
seg.capture(:segment, inbox)
|
89
|
+
segment = inbox[:segment].to_sym
|
90
|
+
|
91
|
+
if app = @applications[segment]
|
92
|
+
app.route(request, seg)
|
93
|
+
else
|
94
|
+
router = Router.new(@resources)
|
95
|
+
router.instance_eval(&@helpers_block) unless @helpers_block.nil?
|
96
|
+
router.run(seg, inbox)
|
97
|
+
router.render(request)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def compile(&block)
|
102
|
+
logger.debug("Compile application: #{@name}")
|
103
|
+
instance_eval(&block)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
class Error < StandardError
|
108
|
+
|
109
|
+
def initialize(options = {})
|
110
|
+
@message = options[:message] || error_message
|
111
|
+
@body = options[:body] || error_message
|
112
|
+
super(@message)
|
113
|
+
end
|
114
|
+
|
115
|
+
def status; 500 end
|
116
|
+
def error_message; 'Internal Server Error' end
|
117
|
+
def header; DEFAULT_HEADER end
|
118
|
+
def body; {message: @body} end
|
119
|
+
end
|
120
|
+
class NotImplementedError < Error
|
121
|
+
def error_message; "Not Implemented Error" end
|
122
|
+
def status; 501 end
|
123
|
+
end
|
124
|
+
class BadRequestError < Error
|
125
|
+
def error_message; "Bad Request Error" end
|
126
|
+
def status; 400 end
|
127
|
+
end
|
128
|
+
class NotFoundError < Error
|
129
|
+
def error_message; "Not Found Error" end
|
130
|
+
def status; 404 end
|
131
|
+
end
|
132
|
+
|
133
|
+
class Router
|
134
|
+
|
135
|
+
def status; @status || ok end
|
136
|
+
def logger; Traim.logger end
|
137
|
+
|
138
|
+
# status code sytax suger
|
139
|
+
def ok; @status = 200 end
|
140
|
+
def created; @status = 201 end
|
141
|
+
def no_cotent; @status = 204 end
|
142
|
+
|
143
|
+
def headers(key, value)
|
144
|
+
@headers[key] = value
|
145
|
+
end
|
146
|
+
|
147
|
+
def initialize(resources)
|
148
|
+
@status = nil
|
149
|
+
@resources = resources
|
150
|
+
@headers = {}
|
151
|
+
end
|
152
|
+
|
153
|
+
def self.resources; @resources ||= {} end
|
154
|
+
|
155
|
+
def resources(name)
|
156
|
+
@resources[name]
|
157
|
+
end
|
158
|
+
|
159
|
+
def show(&block); @resource.action(:show, &block) end
|
160
|
+
def create(&block); @resource.action(:create, &block) end
|
161
|
+
def update(&block); @resource.action(:update, &block) end
|
162
|
+
def destory(&block); @resource.action(:destory, &block) end
|
163
|
+
|
164
|
+
def run(seg, inbox)
|
165
|
+
begin
|
166
|
+
segment = inbox[:segment].to_sym
|
167
|
+
|
168
|
+
if @resource.nil?
|
169
|
+
raise BadRequestError unless @resource = resources(segment)
|
170
|
+
next
|
171
|
+
end
|
172
|
+
|
173
|
+
if @id.nil? && !defined?(@collection_name)
|
174
|
+
if collection = @resource.collections[segment]
|
175
|
+
@collection_name = segment
|
176
|
+
return instance_eval(&collection)
|
177
|
+
else
|
178
|
+
@id = segment.to_s.to_i
|
179
|
+
@record = @resource.model_delegator.show(@id)
|
180
|
+
next
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
if !defined?(@member_name)
|
185
|
+
if member = @resource.members[segment]
|
186
|
+
@member_name = segment
|
187
|
+
return instance_eval(&member)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
raise BadRequestError
|
192
|
+
end while seg.capture(:segment, inbox)
|
193
|
+
end
|
194
|
+
|
195
|
+
def to_json
|
196
|
+
if @result.kind_of?(ActiveRecord::Relation)
|
197
|
+
hash = @result.map do |object|
|
198
|
+
@resource.to_hash(object, @resources)
|
199
|
+
end
|
200
|
+
JSON.dump(hash)
|
201
|
+
else
|
202
|
+
new_hash = {}
|
203
|
+
if @result.errors.size == 0
|
204
|
+
new_hash = @resource.to_hash(@result, @resources)
|
205
|
+
else
|
206
|
+
new_hash = @result.errors.messages
|
207
|
+
end
|
208
|
+
JSON.dump(new_hash)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def action(name)
|
213
|
+
raise NotImplementedError unless action = @resource.actions[name]
|
214
|
+
|
215
|
+
action[:block] = default_actions[name] if action[:block].nil?
|
216
|
+
action
|
217
|
+
end
|
218
|
+
|
219
|
+
def default_actions
|
220
|
+
@default_actions ||= begin
|
221
|
+
actions = {}
|
222
|
+
delegator = @resource.model_delegator
|
223
|
+
actions["POST"] = lambda do |params|
|
224
|
+
delegator.create(params["payload"])
|
225
|
+
end
|
226
|
+
actions["GET"] = lambda do |params|
|
227
|
+
delegator.show(params["id"])
|
228
|
+
end
|
229
|
+
actions["PUT"] = lambda do |params|
|
230
|
+
result = delegator.update(params["id"], params["payload"])
|
231
|
+
result
|
232
|
+
end
|
233
|
+
actions["DELETE"] = lambda do |params|
|
234
|
+
delegator.delete(@id)
|
235
|
+
end
|
236
|
+
actions
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def model; @resource.model end
|
241
|
+
def record; @record; end
|
242
|
+
|
243
|
+
def render(request)
|
244
|
+
method_block = action(request.request_method)
|
245
|
+
payload = request.params
|
246
|
+
if (method_block[:options][:permit])
|
247
|
+
if not_permmited_payload = payload.detect { |key, value| !method_block[:options][:permit].include?(key) }
|
248
|
+
raise BadRequestError.new(message: "Not permitted payload: #{not_permmited_payload}")
|
249
|
+
end
|
250
|
+
end
|
251
|
+
params = {"payload" => payload}
|
252
|
+
params["id"] = @id unless @id.nil?
|
253
|
+
@result = @resource.execute(params, &method_block[:block])
|
254
|
+
|
255
|
+
[status, @headers, [to_json]]
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
class Resource
|
260
|
+
ACTION_METHODS = {create: 'POST', show: 'GET', update: 'PUT', destory: 'DELETE'}
|
261
|
+
|
262
|
+
|
263
|
+
def initialize(block)
|
264
|
+
instance_eval(&block)
|
265
|
+
end
|
266
|
+
|
267
|
+
def execute(params, &block)
|
268
|
+
yield params
|
269
|
+
end
|
270
|
+
|
271
|
+
def model(object = nil, options = {})
|
272
|
+
@model = object unless object.nil?
|
273
|
+
@model
|
274
|
+
end
|
275
|
+
|
276
|
+
def model_delegator
|
277
|
+
@model_delegator ||= Model.new(model)
|
278
|
+
end
|
279
|
+
|
280
|
+
def actions; @actions ||= {} end
|
281
|
+
def action(name, options = {}, &block)
|
282
|
+
actions[ACTION_METHODS[name]] = {block: block, options: options}
|
283
|
+
end
|
284
|
+
|
285
|
+
def logger; Traim.logger end
|
286
|
+
|
287
|
+
def resource(object = nil)
|
288
|
+
@resource = object unless object.nil?
|
289
|
+
@resource
|
290
|
+
end
|
291
|
+
|
292
|
+
def collections; @collections ||= {} end
|
293
|
+
|
294
|
+
def collection(name, &block)
|
295
|
+
collections[name] = block
|
296
|
+
end
|
297
|
+
|
298
|
+
def members; @members ||= {} end
|
299
|
+
|
300
|
+
def member(name, &block)
|
301
|
+
members[name] = block
|
302
|
+
end
|
303
|
+
|
304
|
+
def fields; @fields ||= [] end
|
305
|
+
def attribute(name, &block)
|
306
|
+
fields << {name: name, type: 'attribute', block: block}
|
307
|
+
end
|
308
|
+
|
309
|
+
def has_many(name)
|
310
|
+
fields << {name: name, type: 'association'}
|
311
|
+
end
|
312
|
+
|
313
|
+
def has_one(name)
|
314
|
+
fields << {name: name, type: 'connection'}
|
315
|
+
end
|
316
|
+
|
317
|
+
def to_hash(object, resources, nest_associations = [])
|
318
|
+
fields.inject({}) do | hash, attr|
|
319
|
+
name = attr[:name]
|
320
|
+
hash[name] = if attr[:type] == 'attribute'
|
321
|
+
if attr[:block].nil?
|
322
|
+
object.attributes[name.to_s]
|
323
|
+
else
|
324
|
+
execute(object, &attr[:block])
|
325
|
+
end
|
326
|
+
elsif attr[:type] == 'association'
|
327
|
+
raise Error if nest_associations.include?(name)
|
328
|
+
raise Error if object.class.reflections[name.to_s].blank?
|
329
|
+
nest_associations << name
|
330
|
+
object.send(name).map do |association|
|
331
|
+
resources[name].to_hash(association, nest_associations)
|
332
|
+
end
|
333
|
+
else
|
334
|
+
resource_name = name.to_s.pluralize.to_sym
|
335
|
+
raise Error.new(message: "Inifinite Association") if nest_associations.include?(resource_name)
|
336
|
+
raise Error if object.class.reflections[name.to_s].blank?
|
337
|
+
nest_associations << resource_name
|
338
|
+
resources[resource_name].to_hash(object.send(name), nest_associations)
|
339
|
+
end
|
340
|
+
hash
|
341
|
+
end
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
class Model
|
346
|
+
|
347
|
+
def initialize(model)
|
348
|
+
@model = model
|
349
|
+
end
|
350
|
+
|
351
|
+
def create(params)
|
352
|
+
resource = @model.new(params)
|
353
|
+
resource.save
|
354
|
+
resource
|
355
|
+
end
|
356
|
+
|
357
|
+
def show(id)
|
358
|
+
@model.find id
|
359
|
+
end
|
360
|
+
|
361
|
+
def delete(id)
|
362
|
+
show(id).delete
|
363
|
+
end
|
364
|
+
|
365
|
+
def update(id, params)
|
366
|
+
resource = show(id)
|
367
|
+
resource.assign_attributes(params)
|
368
|
+
resource.save
|
369
|
+
resource
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|
data/test/resource.rb
ADDED
@@ -0,0 +1,379 @@
|
|
1
|
+
require "cutest"
|
2
|
+
require_relative "../lib/traim"
|
3
|
+
|
4
|
+
Traim.config do |app|
|
5
|
+
app.logger = Logger.new(STDOUT)
|
6
|
+
app.logger.level = Logger::DEBUG
|
7
|
+
end
|
8
|
+
|
9
|
+
class User < ActiveRecord::Base
|
10
|
+
validates_presence_of :name
|
11
|
+
|
12
|
+
has_many :books
|
13
|
+
end
|
14
|
+
|
15
|
+
class Book < ActiveRecord::Base
|
16
|
+
belongs_to :user
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
def mock_request(app, url, method, payload = nil)
|
21
|
+
env = Rack::MockRequest.env_for( url,
|
22
|
+
"REQUEST_METHOD" => method,
|
23
|
+
:input => payload
|
24
|
+
)
|
25
|
+
|
26
|
+
app.call(env)
|
27
|
+
end
|
28
|
+
|
29
|
+
prepare do
|
30
|
+
end
|
31
|
+
|
32
|
+
setup do
|
33
|
+
User.create(name: "kolo", email: "kolo@gmail.com")
|
34
|
+
end
|
35
|
+
|
36
|
+
test "basic create, read, update and destory functionality" do |params|
|
37
|
+
app = Traim.application do
|
38
|
+
resources :users do
|
39
|
+
model User
|
40
|
+
|
41
|
+
attribute :id
|
42
|
+
attribute :name
|
43
|
+
|
44
|
+
action :create
|
45
|
+
action :show
|
46
|
+
action :update
|
47
|
+
action :destory
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
_, _, response = mock_request(app, "/users", "POST", "name=kolo&email=kolo@gmail.com")
|
52
|
+
user = JSON.parse(response.first)
|
53
|
+
|
54
|
+
_, _, response = mock_request(app, "/users/#{user["id"]}", "GET")
|
55
|
+
result = JSON.parse(response.first)
|
56
|
+
assert result["name"] == "kolo"
|
57
|
+
|
58
|
+
_, _, response = mock_request(app, "/users/#{user["id"]}?name=ivan", "PUT")
|
59
|
+
result = JSON.parse(response.first)
|
60
|
+
model = User.find(user["id"].to_i)
|
61
|
+
assert model.name == "ivan"
|
62
|
+
|
63
|
+
_, _, response = mock_request(app, "/users/#{user["id"]}?name=ivan", "DELETE")
|
64
|
+
result = JSON.parse(response.first)
|
65
|
+
assert !User.exists?(user["id"])
|
66
|
+
end
|
67
|
+
|
68
|
+
test "customize functionality" do |user|
|
69
|
+
app = Traim.application do
|
70
|
+
resources :users do
|
71
|
+
model User
|
72
|
+
|
73
|
+
attribute :id
|
74
|
+
attribute :name
|
75
|
+
|
76
|
+
action :show do |params|
|
77
|
+
user = model.find params["id"]
|
78
|
+
user.name = "[admin] #{user.name}"
|
79
|
+
user
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
_, _, response = mock_request(app, "/users/#{user.id}", "GET")
|
85
|
+
result = JSON.parse(response.first)
|
86
|
+
assert result["name"] == "[admin] kolo"
|
87
|
+
end
|
88
|
+
|
89
|
+
test "member create, read, update and destory functionality" do |user|
|
90
|
+
app = Traim.application do
|
91
|
+
resources :users do
|
92
|
+
model User
|
93
|
+
|
94
|
+
attribute :id
|
95
|
+
attribute :name
|
96
|
+
|
97
|
+
member :blurred do
|
98
|
+
show do |params|
|
99
|
+
logger.info("show blurred")
|
100
|
+
record.name[1..2] = "xx"
|
101
|
+
record
|
102
|
+
end
|
103
|
+
|
104
|
+
update do |params|
|
105
|
+
record.assign_attributes(params["payload"])
|
106
|
+
record.save
|
107
|
+
record
|
108
|
+
end
|
109
|
+
|
110
|
+
destory do |params|
|
111
|
+
record.delete
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
_, _, response = mock_request(app, "/users/#{user.id}/blurred", "GET")
|
118
|
+
result = JSON.parse(response.first)
|
119
|
+
assert result["name"] == "kxxo"
|
120
|
+
|
121
|
+
_, _, response = mock_request(app, "/users/#{user.id}/blurred?name=ivan", "PUT")
|
122
|
+
assert User.find(user.id).name == "ivan"
|
123
|
+
|
124
|
+
_, _, response = mock_request(app, "/users/#{user.id}/blurred", "DELETE")
|
125
|
+
assert !User.exists?(user.id)
|
126
|
+
|
127
|
+
end
|
128
|
+
|
129
|
+
test "collection create, read, update and destory functionality" do |user|
|
130
|
+
User.create(name: 'Keven', email: 'keven@gmail.com')
|
131
|
+
User.create(name: 'Ivan', email: 'ivan@gmail.com')
|
132
|
+
|
133
|
+
app = Traim.application do
|
134
|
+
resources :users do
|
135
|
+
model User
|
136
|
+
|
137
|
+
attribute :id
|
138
|
+
attribute :name
|
139
|
+
|
140
|
+
collection :admin do
|
141
|
+
show do |params|
|
142
|
+
model.all
|
143
|
+
end
|
144
|
+
|
145
|
+
create do |params|
|
146
|
+
model.create(params["payload"])
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
_, _, response = mock_request(app, "/users/admin?a=123", "GET")
|
153
|
+
result = JSON.parse(response.first)
|
154
|
+
assert result.size == User.all.size
|
155
|
+
|
156
|
+
_, _, response = mock_request(app, "/users/admin", "POST", "name=carol&email=shesee@gmail.com")
|
157
|
+
result = JSON.parse(response.first)
|
158
|
+
assert result["name"] == "carol"
|
159
|
+
end
|
160
|
+
|
161
|
+
test "error message" do |user|
|
162
|
+
app = Traim.application do
|
163
|
+
resources :users do
|
164
|
+
model User
|
165
|
+
|
166
|
+
attribute :id
|
167
|
+
attribute :name
|
168
|
+
|
169
|
+
action :create
|
170
|
+
action :show
|
171
|
+
action :update
|
172
|
+
action :destory
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
_, _, response = mock_request(app, "/users", "POST", "email=kolo@gmail.com")
|
177
|
+
result = JSON.parse(response.first)
|
178
|
+
assert result["name"].first == "can't be blank"
|
179
|
+
end
|
180
|
+
|
181
|
+
test "has many functionality" do |user|
|
182
|
+
book = Book.create(user: user, isbn: 'abc')
|
183
|
+
|
184
|
+
app = Traim.application do
|
185
|
+
resources :users do
|
186
|
+
model User
|
187
|
+
|
188
|
+
attribute :id
|
189
|
+
attribute :name
|
190
|
+
|
191
|
+
has_many :books
|
192
|
+
|
193
|
+
action :show
|
194
|
+
|
195
|
+
collection :admin do
|
196
|
+
show do |params|
|
197
|
+
model.all
|
198
|
+
end
|
199
|
+
|
200
|
+
create do |params|
|
201
|
+
# what to response?
|
202
|
+
model.create(params["payload"])
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
resources :books do
|
207
|
+
model Book
|
208
|
+
|
209
|
+
attribute :isbn
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
_, _, response = mock_request(app, "/users/#{user.id}", "GET")
|
214
|
+
result = JSON.parse(response.first)
|
215
|
+
assert result["books"].size == user.books.size
|
216
|
+
end
|
217
|
+
|
218
|
+
test "has one functionality" do |user|
|
219
|
+
book = Book.create(user: user, isbn: 'abc')
|
220
|
+
|
221
|
+
app = Traim.application do
|
222
|
+
resources :books do
|
223
|
+
model Book
|
224
|
+
attribute :isbn
|
225
|
+
|
226
|
+
action :show
|
227
|
+
|
228
|
+
has_one :user
|
229
|
+
end
|
230
|
+
|
231
|
+
resources :users do
|
232
|
+
model User
|
233
|
+
|
234
|
+
attribute :name
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
_, _, response = mock_request(app, "/books/#{book.id}", "GET")
|
239
|
+
result = JSON.parse(response.first)
|
240
|
+
assert result["user"]["name"] == user.name
|
241
|
+
end
|
242
|
+
|
243
|
+
test "namespace functionality" do |user|
|
244
|
+
book = Book.create(user: user, isbn: 'abc')
|
245
|
+
|
246
|
+
app = Traim.application do
|
247
|
+
namespace :api do
|
248
|
+
namespace :v1 do
|
249
|
+
resources :books do
|
250
|
+
model Book
|
251
|
+
attribute :isbn
|
252
|
+
|
253
|
+
has_one :user
|
254
|
+
|
255
|
+
action :show
|
256
|
+
end
|
257
|
+
|
258
|
+
resources :users do
|
259
|
+
model User
|
260
|
+
|
261
|
+
attribute :name
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
_, _, response = mock_request(app, "/api/v1/books/#{book.id}", "GET")
|
268
|
+
result = JSON.parse(response.first)
|
269
|
+
assert result["user"]["name"] == user.name
|
270
|
+
end
|
271
|
+
|
272
|
+
test "visual attributes functionality" do |user|
|
273
|
+
app = Traim.application do
|
274
|
+
resources :users do
|
275
|
+
model User
|
276
|
+
|
277
|
+
attribute :id
|
278
|
+
attribute :name
|
279
|
+
|
280
|
+
attribute :vattr do |record|
|
281
|
+
record.email + " : " + record.id.to_s
|
282
|
+
end
|
283
|
+
|
284
|
+
action :show
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
_, _, response = mock_request(app, "/users/#{user.id}", "GET")
|
289
|
+
result = JSON.parse(response.first)
|
290
|
+
assert result["name"] == "kolo"
|
291
|
+
end
|
292
|
+
|
293
|
+
test "strong parameters functionality" do |user|
|
294
|
+
app = Traim.application do
|
295
|
+
resources :users do
|
296
|
+
model User
|
297
|
+
|
298
|
+
attribute :id
|
299
|
+
attribute :name
|
300
|
+
|
301
|
+
action :create, permit: ["name"]
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
_, _, response = mock_request(app, "/users", "POST", "name=kolo&email=kolo@gmail.com")
|
306
|
+
result = JSON.parse(response.first)
|
307
|
+
assert result["message"] == "Bad Request Error"
|
308
|
+
end
|
309
|
+
|
310
|
+
test "helpers functionality" do |user|
|
311
|
+
app = Traim.application do
|
312
|
+
helpers do
|
313
|
+
def auth(params = nil)
|
314
|
+
logger.debug "auth: #{params}, all: #{model.all}"
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
resources :users do
|
319
|
+
model User
|
320
|
+
|
321
|
+
attribute :id
|
322
|
+
attribute :name
|
323
|
+
|
324
|
+
member :blurred do
|
325
|
+
show do |params|
|
326
|
+
auth('test')
|
327
|
+
record.name[1..2] = "xx"
|
328
|
+
record
|
329
|
+
end
|
330
|
+
|
331
|
+
update do |params|
|
332
|
+
record.assign_attributes(params["payload"])
|
333
|
+
record.save
|
334
|
+
record
|
335
|
+
end
|
336
|
+
|
337
|
+
destory do |params|
|
338
|
+
record.delete
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
_, _, response = mock_request(app, "/users/#{user.id}/blurred", "GET")
|
345
|
+
result = JSON.parse(response.first)
|
346
|
+
assert result["name"] == "kxxo"
|
347
|
+
|
348
|
+
_, _, response = mock_request(app, "/users/#{user.id}/blurred?name=ivan", "PUT")
|
349
|
+
assert User.find(user.id).name == "ivan"
|
350
|
+
|
351
|
+
_, _, response = mock_request(app, "/users/#{user.id}/blurred", "DELETE")
|
352
|
+
assert !User.exists?(user.id)
|
353
|
+
|
354
|
+
end
|
355
|
+
|
356
|
+
test "headers functionality" do |user|
|
357
|
+
app = Traim.application do
|
358
|
+
resources :users do
|
359
|
+
model User
|
360
|
+
|
361
|
+
attribute :id
|
362
|
+
attribute :name
|
363
|
+
|
364
|
+
member :headers do
|
365
|
+
show do |params|
|
366
|
+
headers("test", "yeah")
|
367
|
+
record
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
_, headers, response = mock_request(app, "/users/#{user.id}/headers", "GET")
|
374
|
+
assert headers["test"] == "yeah"
|
375
|
+
end
|
376
|
+
|
377
|
+
|
378
|
+
Book.delete_all
|
379
|
+
User.delete_all
|
data/traim.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "traim"
|
3
|
+
s.version = "0.1.2"
|
4
|
+
s.summary = %{Resource-oriented microframework for RESTful API}
|
5
|
+
s.description = %Q{Resource-oriented microframework for RESTful API}
|
6
|
+
s.authors = ["Travis Liu"]
|
7
|
+
s.email = ["travisliu.tw@gmail.com"]
|
8
|
+
s.homepage = "https://github.com/travisliu/traim"
|
9
|
+
s.license = "MIT"
|
10
|
+
|
11
|
+
s.files = `git ls-files`.split("\n")
|
12
|
+
|
13
|
+
s.rubyforge_project = "traim"
|
14
|
+
|
15
|
+
s.add_dependency "rack", "~> 2.0"
|
16
|
+
s.add_dependency "seg", "~> 1.2"
|
17
|
+
|
18
|
+
s.add_development_dependency "cutest", "1.2.3"
|
19
|
+
s.add_development_dependency "rack-test", "0.6.3"
|
20
|
+
end
|
metadata
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: traim
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Travis Liu
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-07-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rack
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.0'
|
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'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: seg
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.2'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: cutest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 1.2.3
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.2.3
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rack-test
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.6.3
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.6.3
|
69
|
+
description: Resource-oriented microframework for RESTful API
|
70
|
+
email:
|
71
|
+
- travisliu.tw@gmail.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- ".gitignore"
|
77
|
+
- LICENSE
|
78
|
+
- README.md
|
79
|
+
- lib/traim.rb
|
80
|
+
- test/resource.rb
|
81
|
+
- traim.gemspec
|
82
|
+
homepage: https://github.com/travisliu/traim
|
83
|
+
licenses:
|
84
|
+
- MIT
|
85
|
+
metadata: {}
|
86
|
+
post_install_message:
|
87
|
+
rdoc_options: []
|
88
|
+
require_paths:
|
89
|
+
- lib
|
90
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '0'
|
100
|
+
requirements: []
|
101
|
+
rubyforge_project: traim
|
102
|
+
rubygems_version: 2.6.8
|
103
|
+
signing_key:
|
104
|
+
specification_version: 4
|
105
|
+
summary: Resource-oriented microframework for RESTful API
|
106
|
+
test_files: []
|