traim 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (8) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/LICENSE +19 -0
  4. data/README.md +272 -0
  5. data/lib/traim.rb +372 -0
  6. data/test/resource.rb +379 -0
  7. data/traim.gemspec +20 -0
  8. 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
@@ -0,0 +1,3 @@
1
+ .DS_Store
2
+ *.swp
3
+ *.swo
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: []