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.
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: []