gr_api_manager 0.1.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.
Files changed (4) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +345 -0
  3. data/lib/gr_api_manager.rb +149 -0
  4. metadata +75 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f5f6ec1afb3c1aed64ba13f1d41198f656b52918b061bb0e9fffbb28145ec681
4
+ data.tar.gz: eb578c753e0a5f662dcdc5198855e3f063c6241d3b41f531e3eaaebfb2c66dd4
5
+ SHA512:
6
+ metadata.gz: 840d926e759021f66b36e4148a41dc46a6e26bc74aad62aeab1cfef669620dcb3f344febba2bfe3ed287568844c141b92e3004e193d90d2c9d3d95faa117aa7d
7
+ data.tar.gz: 3e65530682e1ae7bded34147ef2df0f75e345bcaf702ed46f54d59fb746ed373d2e1d59344be796723f26af3c8cb26db54cdb42cee2a940c70b9cdd4b983aad7
data/README.md ADDED
@@ -0,0 +1,345 @@
1
+ # GR API Manager
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/gr-api-manager.svg)](https://rubygems.org/gems/gr-api-manager)
4
+
5
+ A minimal, opinionated Ruby wrapper around Sinatra that eliminates boilerplate from REST API development. Authentication, parameter validation, type casting, CORS, and error handling are handled at the framework level — you write only the business logic.
6
+
7
+ ---
8
+
9
+ ## Features
10
+
11
+ - **Available on RubyGems!** Install globally and use it in any project.
12
+ - Route-level Bearer Token authentication (opt-in/opt-out per endpoint).
13
+ - Declarative required parameter validation with automatic `400 Bad Request` responses.
14
+ - Smart type casting for query string values (`"10"` -> `Integer`, `"true"` -> `TrueClass`, `"9.5"` -> `Float`).
15
+ - Global JSON error responses for `404` and `500`.
16
+ - Broad CORS and `OPTIONS` preflight handling out of the box (Frontend ready).
17
+ - API versioning via configurable route prefix (e.g. `/api/v1`).
18
+
19
+ ---
20
+
21
+ ## Installation
22
+
23
+ Add this line to your application's Gemfile:
24
+
25
+ ```ruby
26
+ gem 'gr-api-manager'
27
+ ```
28
+
29
+ And then execute:
30
+ ```bash
31
+ bundle install
32
+ ```
33
+
34
+ Or install it yourself directly via RubyGems:
35
+ ```bash
36
+ gem install gr-api-manager
37
+ ```
38
+
39
+ ---
40
+
41
+ ## Quick Start
42
+
43
+ ```ruby
44
+ require 'gr_api_manager'
45
+
46
+ api = GRApiManager::Server.new(
47
+ port: 4567,
48
+ bearer_token: "your_secret_token",
49
+ prefix: "/api/v1"
50
+ )
51
+
52
+ # Public endpoint
53
+ api.get('/health', auth: false) do
54
+ { status: 'online', time: Time.now.to_s }
55
+ end
56
+
57
+ api.run!
58
+ ```
59
+
60
+ ```bash
61
+ curl http://localhost:4567/api/v1/health
62
+ # => {"status":"online","time":"..."}
63
+ ```
64
+
65
+ ---
66
+
67
+ ## Configuration
68
+
69
+ All parameters are optional. If omitted, the manager falls back to environment variables loaded from a `.env` file at the project root (via `dotenv`). If neither is provided, defaults are used.
70
+
71
+ ```ruby
72
+ GRApiManager::Server.new(
73
+ port: 4567, # Default: ENV['PORT'] || 4000
74
+ bearer_token: "secret", # Default: ENV['API_TOKEN']
75
+ permitted_hosts: ["example.com"], # Host allowlist — empty means allow all
76
+ prefix: "/api/v1" # Route prefix applied to all endpoints
77
+ )
78
+ ```
79
+
80
+ ### Using a .env file (Recommended)
81
+
82
+ Create a `.env` file at the root of your project:
83
+
84
+ ```env
85
+ PORT=4567
86
+ API_TOKEN=your_secret_token
87
+ ```
88
+
89
+ Then initialize the manager with no arguments and it will pick everything up automatically:
90
+
91
+ ```ruby
92
+ require 'gr_api_manager'
93
+
94
+ api = GRApiManager::Server.new
95
+ # Reads PORT and API_TOKEN from .env
96
+ ```
97
+
98
+ This is the recommended approach for production — keep secrets out of source code and out of version control. Add `.env` to your `.gitignore`.
99
+
100
+ ```text
101
+ # .gitignore
102
+ .env
103
+ ```
104
+
105
+ ### Priority order
106
+
107
+ When a value is provided both in code and in `.env`, the explicit argument always wins:
108
+
109
+ ```text
110
+ new(bearer_token: "hardcoded") > ENV['API_TOKEN'] > nil (auth disabled)
111
+ new(port: 4567) > ENV['PORT'] > 4000
112
+ ```
113
+
114
+ ---
115
+
116
+ ## Defining Routes
117
+
118
+ The manager exposes `get`, `post`, `put`, and `delete` methods. Each route receives a merged `params` hash containing both URL/query parameters (type-cast) and the parsed JSON body.
119
+
120
+ ```ruby
121
+ api.get('/users', auth: true, requires: [:role, :age]) do |params|
122
+ # params is a merged, type-cast hash of all inputs
123
+ {
124
+ requested_role: params[:role],
125
+ is_adult: params[:age] >= 18 # age is safely cast to Integer
126
+ }
127
+ end
128
+ ```
129
+
130
+ ### Options
131
+
132
+ | Option | Type | Default | Description |
133
+ |------------|---------|---------|--------------------------------------------------|
134
+ | `auth` | Boolean | `true` | Require Bearer Token for this route |
135
+ | `requires` | Array | `[]` | List of required parameter keys (symbols/strings)|
136
+
137
+ ---
138
+
139
+ ## Authentication
140
+
141
+ All routes require a valid Bearer Token by default. Pass the token in the `Authorization` header:
142
+
143
+ ```bash
144
+ curl -H "Authorization: Bearer your_secret_token" http://localhost:4567/api/v1/users
145
+ ```
146
+
147
+ To make a route public, set `auth: false`:
148
+
149
+ ```ruby
150
+ api.get('/health', auth: false) do
151
+ { status: 'online' }
152
+ end
153
+ ```
154
+
155
+ **Automatic Error Responses:**
156
+
157
+ ```json
158
+ // Missing header (401)
159
+ { "error": "Token required. Format: 'Bearer <token>'" }
160
+
161
+ // Wrong token (403)
162
+ { "error": "Invalid token" }
163
+ ```
164
+
165
+ ---
166
+
167
+ ## Parameter Validation
168
+
169
+ Declare required parameters at the route level. If any are missing or blank, the framework responds with `400 Bad Request` automatically — no conditional logic needed in your handler.
170
+
171
+ ```ruby
172
+ api.post('/users', requires: [:name, :email]) do |params|
173
+ status 201
174
+ { message: "User created", user: params }
175
+ end
176
+ ```
177
+
178
+ ```bash
179
+ curl -X POST http://localhost:4567/api/v1/users \
180
+ -H "Authorization: Bearer secret" \
181
+ -H "Content-Type: application/json" \
182
+ -d '{"name": "Gabo"}'
183
+
184
+ # => 400 { "error": "Missing required parameters", "required": ["email"] }
185
+ ```
186
+
187
+ ---
188
+
189
+ ## Smart Type Casting
190
+
191
+ Query string parameters are automatically cast to native Ruby types before reaching your handler.
192
+
193
+ | Input string | Ruby type | Value |
194
+ |--------------|-----------|----------|
195
+ | `"42"` | Integer | `42` |
196
+ | `"3.14"` | Float | `3.14` |
197
+ | `"true"` | TrueClass | `true` |
198
+ | `"false"` | FalseClass| `false` |
199
+ | `"hello"` | String | `"hello"`|
200
+
201
+ ```bash
202
+ curl "http://localhost:4567/api/v1/test/types?age=18&active=true&score=9.5" \
203
+ -H "Authorization: Bearer secret"
204
+
205
+ # => { "age": 18, "active": true, "score": 9.5 }
206
+ ```
207
+
208
+ ---
209
+
210
+ ## Error Handling
211
+
212
+ Global handlers return consistent JSON for unmatched routes and unhandled exceptions.
213
+
214
+ ```json
215
+ // 404
216
+ { "error": "Endpoint not found", "path": "/api/v1/missing" }
217
+
218
+ // 500
219
+ { "error": "Internal Server Error", "details": "..." }
220
+ ```
221
+
222
+ You can also set status codes and short-circuit responses manually inside any handler:
223
+
224
+ ```ruby
225
+ api.get('/users/:id') do |params|
226
+ if params[:id] == 0
227
+ status 404
228
+ next { error: "Invalid user ID" }
229
+ end
230
+
231
+ { id: params[:id], name: "User_#{params[:id]}" }
232
+ end
233
+ ```
234
+
235
+ ---
236
+
237
+ ## Request Logging
238
+
239
+ Every request is logged to stdout with a timestamp and color-coded status:
240
+
241
+ ```text
242
+ [14:32:01] GET /api/v1/health - 200
243
+ [14:32:05] POST /api/v1/users - 400
244
+ ```
245
+
246
+ Green for 2xx, red for everything else.
247
+
248
+ ---
249
+
250
+ ## Running the Server
251
+
252
+ ```ruby
253
+ api.run!
254
+ ```
255
+
256
+ ```text
257
+ =============================================
258
+ GR API MANAGER STARTED
259
+ Port : 4567
260
+ Auth : Enabled
261
+ Prefix : /api/v1
262
+ =============================================
263
+ ```
264
+
265
+ ---
266
+
267
+ ## Full Usage Example
268
+
269
+ Below is a complete working API covering the most common patterns.
270
+
271
+ ```ruby
272
+ require 'gr_api_manager'
273
+
274
+ api = GRApiManager::Server.new(
275
+ port: 4567,
276
+ bearer_token: "secret123",
277
+ prefix: "/api/v1"
278
+ )
279
+
280
+ # 1. Public health check — no token required
281
+ api.get('/health', auth: false) do
282
+ { status: 'online', time: Time.now.strftime("%Y-%m-%d %H:%M:%S") }
283
+ end
284
+
285
+ # 2. List users — token required (default)
286
+ api.get('/users') do |params|
287
+ users = [
288
+ { id: 1, name: "Alice", role: "admin" },
289
+ { id: 2, name: "Bob", role: "viewer" }
290
+ ]
291
+ { users: users, total: users.length }
292
+ end
293
+
294
+ # 3. Get single user by ID — :id is cast to Integer automatically
295
+ api.get('/users/:id') do |params|
296
+ if params[:id] == 0
297
+ status 404
298
+ next { error: "User not found" }
299
+ end
300
+ { id: params[:id], name: "User_#{params[:id]}" }
301
+ end
302
+
303
+ # 4. Create user — validates required fields before reaching the block
304
+ api.post('/users', requires: [:name, :role]) do |params|
305
+ status 201
306
+ { message: "User created", user: { name: params[:name], role: params[:role] } }
307
+ end
308
+
309
+ # 5. Update user
310
+ api.put('/users/:id', requires: [:name]) do |params|
311
+ { message: "User #{params[:id]} updated", name: params[:name] }
312
+ end
313
+
314
+ # 6. Delete user
315
+ api.delete('/users/:id') do |params|
316
+ { message: "User #{params[:id]} deleted" }
317
+ end
318
+
319
+ api.run!
320
+ ```
321
+
322
+ ### Minimal setup using only .env
323
+
324
+ ```env
325
+ # .env
326
+ PORT=4567
327
+ API_TOKEN=secret123
328
+ ```
329
+
330
+ ```ruby
331
+ # api_server.rb
332
+ require 'gr_api_manager'
333
+
334
+ api = GRApiManager::Server.new # reads everything from .env
335
+
336
+ api.get('/ping', auth: false) { { pong: true } }
337
+
338
+ api.run!
339
+ ```
340
+
341
+ ---
342
+
343
+ ## License
344
+
345
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,149 @@
1
+ require 'sinatra/base'
2
+ require 'json'
3
+ require 'dotenv/load'
4
+
5
+ module GRApiManager
6
+ class Server
7
+ attr_reader :app_class
8
+
9
+ # Initializes the server configuration.
10
+ def initialize(port: nil, bearer_token: nil, permitted_hosts: [], prefix: '')
11
+ @port = port || ENV['PORT'] || 4000
12
+ @token = bearer_token || ENV['API_TOKEN']
13
+ @permitted_hosts = permitted_hosts.empty? ? [] : permitted_hosts
14
+ @prefix = prefix
15
+
16
+ @app_class = Class.new(Sinatra::Base) do
17
+
18
+ # Logs HTTP requests with status-based color coding.
19
+ def log_request(method, path, params, status_code)
20
+ color = status_code.between?(200, 299) ? "\e[32m" : "\e[31m"
21
+ puts "[#{Time.now.strftime('%H:%M:%S')}] #{color}#{method} #{path} - #{status_code}\e[0m"
22
+ end
23
+
24
+ # Casts string URL parameters to native Ruby types (Integer, Float, Boolean).
25
+ def smart_parse(hash)
26
+ hash.transform_values do |val|
27
+ case val
28
+ when 'true' then true
29
+ when 'false' then false
30
+ when /^[0-9]+$/ then val.to_i
31
+ when /^[0-9]+\.[0-9]+$/ then val.to_f
32
+ else val
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ configure_app
39
+ end
40
+
41
+ private
42
+
43
+ # Sets up Sinatra environment, CORS policies, and global error handlers.
44
+ def configure_app
45
+ app = @app_class
46
+ app.set :port, @port
47
+ app.set :bind, '0.0.0.0'
48
+ app.set :token, @token
49
+ app.set :show_exceptions, false
50
+ app.set :host_authorization, { permitted_hosts: @permitted_hosts }
51
+
52
+ # Enable broad CORS and handle preflight requests.
53
+ app.before do
54
+ headers 'Access-Control-Allow-Origin' => '*',
55
+ 'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, OPTIONS',
56
+ 'Access-Control-Allow-Headers' => 'Content-Type, Authorization'
57
+ content_type :json
58
+ end
59
+
60
+ app.options '*' do
61
+ halt 200
62
+ end
63
+
64
+ # JSON formatted 404 response.
65
+ app.not_found do
66
+ status 404
67
+ { error: "Endpoint not found", path: request.path_info }.to_json
68
+ end
69
+
70
+ # JSON formatted 500 response.
71
+ app.error do
72
+ e = env['sinatra.error']
73
+ status 500
74
+ { error: "Internal Server Error", details: e.message }.to_json
75
+ end
76
+ end
77
+
78
+ public
79
+
80
+ # Dynamically generate routing methods (get, post, put, delete).
81
+ %w[get post put delete].each do |verb|
82
+ define_method(verb) do |path, options = {}, &block|
83
+ register_route(verb, path, options, &block)
84
+ end
85
+ end
86
+
87
+ # Core routing logic: auth validation, param parsing, and block execution.
88
+ def register_route(verb, path, options = {}, &block)
89
+ verb = verb.to_s.upcase
90
+ require_auth = options.fetch(:auth, true)
91
+ required_params = options.fetch(:requires, [])
92
+
93
+ # Construct the full path with the optional prefix.
94
+ full_path = File.join('/', @prefix.to_s, path.to_s).gsub(%r{/+}, '/')
95
+
96
+ handler = proc do
97
+
98
+ # 1. Authentication check
99
+ if require_auth
100
+ auth_header = request.env["HTTP_AUTHORIZATION"]
101
+ halt 401, { error: "Token required. Format: 'Bearer <token>'" }.to_json if auth_header.nil?
102
+ halt 403, { error: "Invalid token" }.to_json if auth_header.split(" ").last != settings.token
103
+ end
104
+
105
+ # 2. Body parsing (for POST/PUT requests)
106
+ parsed_body = {}
107
+ if ['POST', 'PUT'].include?(verb)
108
+ body_data = request.body.read.to_s
109
+ unless body_data.empty?
110
+ begin
111
+ parsed_body = JSON.parse(body_data, symbolize_names: true)
112
+ rescue JSON::ParserError
113
+ halt 400, { error: "Invalid JSON body" }.to_json
114
+ end
115
+ end
116
+ end
117
+
118
+ # 3. Merge query parameters with parsed JSON body
119
+ all_params = smart_parse(params).merge(parsed_body)
120
+
121
+ # 4. Declarative parameter validation
122
+ missing = required_params.select { |p| all_params[p.to_sym].nil? || all_params[p.to_sym].to_s.strip.empty? }
123
+ if missing.any?
124
+ status 400
125
+ log_request(verb, full_path, all_params, 400)
126
+ next { error: "Missing required parameters", required: missing }.to_json
127
+ end
128
+
129
+ # 5. Execute user-defined block
130
+ result = instance_exec(all_params, &block)
131
+ log_request(verb, full_path, all_params, response.status)
132
+ result.to_json
133
+ end
134
+
135
+ @app_class.send(verb.downcase, full_path, &handler)
136
+ end
137
+
138
+ # Starts the Sinatra server with a custom GR banner.
139
+ def run!
140
+ puts "============================================="
141
+ puts " GR API MANAGER STARTED"
142
+ puts " Port : #{@port}"
143
+ puts " Auth : #{@token ? 'Enabled' : 'Public'}"
144
+ puts " Prefix : #{@prefix.empty? ? '/' : @prefix}"
145
+ puts "============================================="
146
+ @app_class.run!
147
+ end
148
+ end
149
+ end
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gr_api_manager
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Razo
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: sinatra
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '3.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '3.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: dotenv
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.8'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.8'
40
+ description: Eliminates boilerplate from REST API development. Handles Auth, CORS,
41
+ parameter validation, and type casting.
42
+ email:
43
+ - garabatoangelopolis@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - README.md
49
+ - lib/gr_api_manager.rb
50
+ homepage: https://github.com/Gabo-Razo/sinatra-api-manager
51
+ licenses:
52
+ - MIT
53
+ metadata:
54
+ allowed_push_host: https://rubygems.org
55
+ source_code_uri: https://github.com/Gabo-Razo/sinatra-api-manager
56
+ changelog_uri: https://github.com/Gabo-Razo/sinatra-api-manager/blob/main/README.md
57
+ post_install_message: Thanks for installing GR API Manager! Ready to eliminate boilerplate?
58
+ rdoc_options: []
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '3.0'
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ requirements: []
72
+ rubygems_version: 3.6.7
73
+ specification_version: 4
74
+ summary: A minimal, opinionated Ruby wrapper around Sinatra.
75
+ test_files: []