flon 0.2.0 → 0.3.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9d77f8f763e06566ba8fd67310e4bf2eb7cf25cf1a2927685973c8ab8310a673
4
- data.tar.gz: f693eedb16f5dbff5505c00408ff728b4dbf8911294dea690d36139b811027db
3
+ metadata.gz: dadeb3b4469a7ee38098b5e24548f525a32fa08d2577a8f67a64175f34d965e3
4
+ data.tar.gz: fe9c3d073b64a8bb443f1ce9b87d33889cb3c2750d255df3979b05563b3bfb42
5
5
  SHA512:
6
- metadata.gz: 1ff850c5ec62797dfba5deda611c90aa941110e824bc39bb3cb577fa04594e2f1b7e5210874a9e910b9d5b136d9c7ecb40b23077b800d1b3f39a3b82549a011f
7
- data.tar.gz: 179296c86b0de6ebca2629a1977a78159d1e4f261f03d3b397a834a9f41c89c5cb7cd129825aee22285d1d6ccf7421efa8f1909f834a3aac79a7ff8384d6a3b4
6
+ metadata.gz: 15663df435d9955f82ddc991955537919007a7e0c220dd380d435f8ba8cfc1593352155a5f7eed8dcb74efa2632ef295bda620dd8fc68e5d1beaa50f17f0c2a8
7
+ data.tar.gz: f10055e8e158a2a9a7f9c72c51a469d408558c9bec5c46d4ceee7a9c59b8af50dbdd359dc7b5fe008b69e86e86f1e2b58ca8348ada6f93c4ad1b735250b513c8
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Flon
2
2
 
3
3
  Flon, the API maker that uses JSON. It focuses on not doing any magic: it’s
4
- just a thin and convenient wrapper over Rack, with a hopefully obvious DSL.
4
+ just a thin and convenient wrapper over Rack, with a hopefully obvious DSL.
5
5
 
6
6
  ## Installation
7
7
 
@@ -13,11 +13,15 @@ gem 'flon'
13
13
 
14
14
  And then execute:
15
15
 
16
- $ bundle
16
+ ```sh
17
+ bundle
18
+ ```
17
19
 
18
20
  Or install it yourself as:
19
21
 
20
- $ gem install flon
22
+ ```sh
23
+ gem install flon
24
+ ```
21
25
 
22
26
  ## Usage
23
27
 
@@ -29,7 +33,7 @@ router DSL in your class scope.
29
33
  ```ruby
30
34
  require 'flon'
31
35
 
32
- class MyApi
36
+ class MyAPI
33
37
  extend Flon::DSL
34
38
 
35
39
  def initialize
@@ -41,35 +45,35 @@ class MyApi
41
45
  attr_reader :names
42
46
 
43
47
  post '/new'
44
- def add_name(_params, body)
45
- @names << body
48
+ def add_name(request)
49
+ @names << request.body
46
50
  end
47
51
 
48
52
  namespace '/:index' do
49
53
  get '/'
50
- def name_by_index(params)
51
- @names[params[:index].to_i]
54
+ def name_by_index(request)
55
+ @names[request.params[:index].to_i]
52
56
  end
53
57
 
54
58
  put '/edit'
55
- def change_name(params, body)
56
- @names[params[:index].to_i] = body
59
+ def change_name(request)
60
+ @names[request.params[:index].to_i] = request.body
57
61
  end
58
62
  end
59
63
  end
60
64
  end
61
65
  ```
62
66
 
63
- You can then create a `Flon::Api` using this:
67
+ You can then create a `Flon::API` using this:
64
68
 
65
69
  ```ruby
66
- api = Flon::Api.new(MyApi.new)
70
+ api = Flon::API.new(MyAPI.new)
67
71
  ```
68
72
 
69
- Note how you initialise the `MyApi` normally—you can use this for dependency
73
+ Note how you initialise everything normally—you can use this for dependency
70
74
  injection without a headache.
71
75
 
72
- `Flon::Api` is a bog-standard Rack app, so you can just use it in `config.ru`,
76
+ `Flon::API` is a bog-standard Rack app, so you can just use it in `config.ru`,
73
77
  or whatever method you use.
74
78
 
75
79
  ### Routing
@@ -114,12 +118,35 @@ pattern is the exact same as [Sinatra’s](https://github.com/sinatra/mustermann
114
118
  The bound method is called when an HTTP request matches that route with that
115
119
  HTTP method.
116
120
 
117
- The method will receive two arguments, in that order:
121
+ The method will receive one argument, the `Flon::Request` object describing the
122
+ HTTP request:
123
+
124
+ ```ruby
125
+ request.body # Gets the body; returns a String, or nil if no body
126
+
127
+ request.headers # Gets the headers; returns a Hash with String keys and
128
+ # String values
129
+
130
+ request.method # Gets the HTTP method; returns a Symbol with the name of
131
+ # the method lowercased
132
+
133
+ request.params # Gets the route and query parameters, merged, giving
134
+ # priority to route parameters; returns a Hash with Symbol
135
+ # keys
118
136
 
119
- 1. A parameters hash, containing the query string parameters and the route
120
- parameters;
121
- 2. A body object, containing the HTTP request’s body parsed with `JSON.parse`.
122
- For GETs, HEADs, and DELETEs, this will be `nil`.
137
+ request.path # Gets the path; returns a String
138
+
139
+ request.query_params # Gets the query parameters, specifically; returns a Hash
140
+ # with Symbol keys
141
+
142
+ request.route_params # Gets the route parameters, specifically; returns a Hash
143
+ # with Symbol keys
144
+
145
+ request.url # Gets the request’s URL; returns an URI object
146
+ ```
147
+
148
+ Note that the argument will not be passed if the method does not accept at
149
+ least one argument, so you can leave it out if you’re not gonna use it.
123
150
 
124
151
  #### Namespaces
125
152
 
@@ -131,19 +158,19 @@ namespace '/users' do
131
158
  attr_reader :users # GET /users
132
159
 
133
160
  post '/new'
134
- def add_user(_params, user) # /users/new
135
- @users << user
161
+ def add_user(request) # /users/new
162
+ @users << request.body
136
163
  end
137
164
 
138
165
  namespace '/:id' do
139
166
  get
140
- def user_by_id(params) # GET /users/:id
141
- @users[params[:id].to_i]
167
+ def user_by_id(request) # GET /users/:id
168
+ @users[request.params[:id].to_i]
142
169
  end
143
170
 
144
171
  put '/edit'
145
- def edit_user(params, body) # PUT /users/:id/edit
146
- @users[params[:id].to_i] = body
172
+ def edit_user(request) # PUT /users/:id/edit
173
+ @users[request.params[:id].to_i] = request.body
147
174
  end
148
175
  end
149
176
  end
@@ -168,7 +195,7 @@ get '/no'
168
195
  def no
169
196
  'no'
170
197
  end
171
- ```
198
+ ```
172
199
 
173
200
  Going to `/yes` will give you a 404, but going to `/v1/yes` will give you
174
201
  `"yes"`. The same thing happens with the `/no` route.
@@ -178,14 +205,14 @@ In reality, `version` is just an alias for `namespace`.
178
205
  ### Responding
179
206
 
180
207
  Whatever is returned by the route’s method is `#to_json`’d and sent back. If
181
- you want to return a custom status code, return a `Flon::Response` object,
182
- which takes the status code as the first argument and the body as the second
183
- argument of its constructor:
208
+ you want to return a custom response, return a `Flon::Response` object, which
209
+ takes the status code as the first argument, the body as the second argument,
210
+ and the headers as the third argument of its constructor:
184
211
 
185
212
  ```ruby
186
213
  get '/admin'
187
214
  def admin
188
- Flon::Response.new(403, 'No. Go away.')
215
+ Flon::Response.new(403, 'No. Go away.', 'X-Loser' => 'Yes')
189
216
  end
190
217
  ```
191
218
 
data/lib/flon/api.rb CHANGED
@@ -2,25 +2,59 @@
2
2
 
3
3
  require 'rack'
4
4
  require 'json'
5
+ require 'uri'
5
6
 
6
7
  module Flon
7
- # Represents a response, with an integer status and a body that will be
8
- # #to_json'd.
8
+ # Represents a request.
9
+ class Request
10
+ attr_reader :body, :headers, :method, :params, :path, :query_params,
11
+ :route_params, :url
12
+
13
+ def initialize(method, rack_request, route_params, body)
14
+ @body = body
15
+ @headers = rack_request.env
16
+ @method = method
17
+ @query_params = rack_request.params
18
+ @route_params = route_params
19
+ @params = coalesce_params(@query_params, @route_params)
20
+ @path = rack_request.path_info
21
+ @url = make_url(rack_request)
22
+ end
23
+
24
+ private
25
+
26
+ def coalesce_params(query_params, route_params)
27
+ query_params.transform_keys(&:to_sym).merge(route_params)
28
+ end
29
+
30
+ def make_url(request)
31
+ URI.parse(request.fullpath)
32
+ end
33
+ end
34
+
35
+ # Represents a response, with a status, body, and headers.
9
36
  #
10
37
  # @!attribute [r] status
11
38
  # @return [Integer] the status to use when responding
12
39
  #
13
40
  # @!attribute [r] body
14
41
  # @return [#to_json] the object to respond with that shall be #to_json'd
15
- Response = Struct.new(:status, :body)
42
+ #
43
+ # @!attribute [r] headers
44
+ # @return [Hash{String => #to_s}] the headers hash; each value will be
45
+ # converted with a String with #to_s
46
+ Response = Struct.new(:status, :body, :headers)
16
47
 
17
48
  # This class is Flon's Rack application.
18
- class Api
19
- # Creates a new {Api} object with the given API.
49
+ class API
50
+ DEFAULT_RESPONSE_HEADERS = { 'Content-Type' => 'application/json' }.freeze
51
+ private_constant :DEFAULT_RESPONSE_HEADERS
52
+
53
+ # Creates a new {API} object with the given API.
20
54
  #
21
55
  # @param [Object] api the API to use; its class must respond to #router and
22
56
  # the result must be a {Router}.
23
- # @return [Api] the newly constructed object
57
+ # @return [API] the newly constructed object
24
58
  def initialize(api)
25
59
  raise ArgumentError, "given API's class does not respond to #routes" unless valid_api?(api)
26
60
 
@@ -44,26 +78,21 @@ module Flon
44
78
  else dispatch(method, request, match)
45
79
  end
46
80
 
47
- [response.status, { 'Content-Type' => 'application/json' }, [response.body]]
81
+ rack_response(method, response)
48
82
  end
49
83
 
50
84
  private
51
85
 
52
- def dispatch(method, request, match)
53
- params = coalesce_params(request.params, match.params)
54
-
86
+ def dispatch(method, rack_request, match)
55
87
  if receives_body?(method)
56
- body = normalise_body(request)
88
+ body = normalise_body(rack_request)
57
89
  return Response.new(415, '"415 Unsupported Media Type"') if body == :fail
58
90
  else
59
91
  body = nil
60
92
  end
61
93
 
62
- call_action(match.action, params, body)
63
- end
64
-
65
- def coalesce_params(request_params, route_params)
66
- request_params.transform_keys(&:to_sym).merge(route_params)
94
+ request = Request.new(method, rack_request, match.params, body)
95
+ call_action(match.action, request)
67
96
  end
68
97
 
69
98
  def normalise_body(request)
@@ -83,8 +112,8 @@ module Flon
83
112
  http_method != :get && http_method != :delete
84
113
  end
85
114
 
86
- def call_action(action, params, body)
87
- responsify(call_respect_arity(action.bind(@api), params, body))
115
+ def call_action(action, request)
116
+ responsify(call_respect_arity(action.bind(@api), request))
88
117
  end
89
118
 
90
119
  def responsify(result)
@@ -100,6 +129,18 @@ module Flon
100
129
  arity.negative? ? method.call(*args) : method.call(*args[0...arity])
101
130
  end
102
131
 
132
+ def rack_response(method, response)
133
+ [
134
+ response.status || 200,
135
+ DEFAULT_RESPONSE_HEADERS.merge(stringify_values(response.headers || {})),
136
+ method == :head || !response.body ? [] : [response.body]
137
+ ]
138
+ end
139
+
140
+ def stringify_values(hash)
141
+ hash.transform_values(&:to_s)
142
+ end
143
+
103
144
  def http_method_to_symbol(http_method)
104
145
  http_method.downcase.to_sym
105
146
  end
data/lib/flon/dsl.rb CHANGED
@@ -6,7 +6,7 @@ module Flon
6
6
  # This module holds Flon's DSL. You should extend it in your class.
7
7
  #
8
8
  # @example
9
- # class BreadApi
9
+ # class BreadAPI
10
10
  # extend Flon::DSL
11
11
  #
12
12
  # get '/bread'
data/lib/flon/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Flon
4
4
  # Flon's version.
5
- VERSION = '0.2.0'
5
+ VERSION = '0.3.0'
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - unleashy
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-09-07 00:00:00.000000000 Z
11
+ date: 2019-09-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mustermann