fastrb 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.
data/lib/rubyapi.rb ADDED
@@ -0,0 +1,306 @@
1
+ require_relative "rubyapi/version"
2
+ require_relative "rubyapi/router"
3
+ require_relative "rubyapi/context"
4
+ require_relative "rubyapi/param_converters"
5
+ require_relative "rubyapi/serializer"
6
+ require_relative "rubyapi/openapi"
7
+ require_relative "rubyapi/schema"
8
+ require_relative "rubyapi/session"
9
+ require_relative "rubyapi/error_handler"
10
+ require_relative "rubyapi/config"
11
+ require_relative "rubyapi/di"
12
+ require_relative "rubyapi/plugin"
13
+ require_relative "rubyapi/websocket"
14
+ require_relative "rubyapi/sse"
15
+ require_relative "rubyapi/streaming"
16
+ require_relative "rubyapi/job"
17
+ require_relative "rubyapi/middleware/logging"
18
+ require_relative "rubyapi/plugins/cors"
19
+ require_relative "rubyapi/plugins/jwt"
20
+ require_relative "rubyapi/plugins/auth"
21
+ require_relative "rubyapi/plugins/cache"
22
+
23
+ module RubyAPI
24
+ class App
25
+ include ErrorHandler
26
+ include DI
27
+
28
+ attr_reader :router, :config, :container
29
+
30
+ def initialize(&block)
31
+ @router = Router.new
32
+ @config = Config.new
33
+ @container = Container.new
34
+ @middlewares = []
35
+ @before_hooks = []
36
+ @after_hooks = []
37
+ @error_handlers = {}
38
+ @plugins = []
39
+ instance_eval(&block) if block
40
+ end
41
+
42
+ def get(path, **opts, &block)
43
+ @router.add_route(:GET, path, **opts, &block)
44
+ end
45
+
46
+ def post(path, **opts, &block)
47
+ @router.add_route(:POST, path, **opts, &block)
48
+ end
49
+
50
+ def put(path, **opts, &block)
51
+ @router.add_route(:PUT, path, **opts, &block)
52
+ end
53
+
54
+ def patch(path, **opts, &block)
55
+ @router.add_route(:PATCH, path, **opts, &block)
56
+ end
57
+
58
+ def delete(path, **opts, &block)
59
+ @router.add_route(:DELETE, path, **opts, &block)
60
+ end
61
+
62
+ def options(path, **opts, &block)
63
+ @router.add_route(:OPTIONS, path, **opts, &block)
64
+ end
65
+
66
+ def head(path, **opts, &block)
67
+ @router.add_route(:HEAD, path, **opts, &block)
68
+ end
69
+
70
+ def route(method, path, **opts, &block)
71
+ @router.add_route(method, path, **opts, &block)
72
+ end
73
+
74
+ def websocket(path, &handler)
75
+ @router.add_route(:GET, path, websocket: true, block: handler)
76
+ end
77
+
78
+ def group(prefix, &block)
79
+ @router.push_prefix(prefix)
80
+ instance_eval(&block)
81
+ @router.pop_prefix(prefix)
82
+ end
83
+
84
+ def use(middleware, *args)
85
+ @middlewares << [middleware, args]
86
+ end
87
+
88
+ def before(&hook)
89
+ @before_hooks << hook
90
+ end
91
+
92
+ def after(&hook)
93
+ @after_hooks << hook
94
+ end
95
+
96
+ def plugin(name_or_klass, **opts)
97
+ klass = name_or_klass.is_a?(Class) ? name_or_klass : PluginRegistry.find(name_or_klass)
98
+ return unless klass
99
+
100
+ old_options = klass.options.dup
101
+ opts.each { |k, v| klass.option(k, v) }
102
+
103
+ @plugins << klass
104
+ klass.on_load(self)
105
+ rescue => e
106
+ klass.options.replace(old_options) if old_options
107
+ raise e
108
+ end
109
+
110
+ def register(name, &block)
111
+ @container.register(name, &block)
112
+ end
113
+
114
+ def call(env)
115
+ req = Rack::Request.new(env)
116
+ path = req.path_info
117
+ method = req.request_method.upcase
118
+
119
+ if path == "/openapi.json"
120
+ return serve_openapi
121
+ end
122
+
123
+ if path == "/docs"
124
+ return serve_swagger_ui
125
+ end
126
+
127
+ route = @router.find(method, path)
128
+
129
+ if route && route[:websocket] && env["HTTP_UPGRADE"]&.downcase == "websocket"
130
+ return handle_websocket(env, route)
131
+ end
132
+
133
+ ctx = Context.new(req, route || dummy_route(req))
134
+
135
+ @before_hooks.each do |hook|
136
+ hook.call(ctx)
137
+ if ctx.response_status
138
+ return error_response(ctx)
139
+ end
140
+ end
141
+
142
+ return [404, { "content-type" => "application/json" }, ['{"error":"Not Found"}']] unless route
143
+
144
+ ctx.apply_param_types!
145
+ return error_response(ctx) if ctx.response_status
146
+
147
+ if route[:body]
148
+ ctx.validate_body!(route[:body])
149
+ return error_response(ctx) if ctx.response_status
150
+ end
151
+
152
+ session = Session.new(req, secret_key: @config.get(:secret_key))
153
+ ctx.session = session
154
+
155
+ inject_deps(ctx, route)
156
+ return error_response(ctx) if ctx.response_status
157
+
158
+ begin
159
+ result = route[:block].call(ctx)
160
+ rescue => e
161
+ handler_status = find_error_handler(e)
162
+ if handler_status
163
+ set_session_cookie(ctx, session)
164
+ return [handler_status, { "content-type" => "application/json" }, [JSON.generate({ error: e.class.name, message: e.message })]]
165
+ end
166
+ raise e
167
+ end
168
+
169
+ @after_hooks.each { |hook| hook.call(ctx) }
170
+ ctx.response_body = result if result && !ctx.response_body
171
+
172
+ set_session_cookie(ctx, session)
173
+ serialize_response(ctx)
174
+ end
175
+
176
+ private
177
+
178
+ def dummy_route(req)
179
+ {
180
+ method: req.request_method.upcase,
181
+ path: req.path_info,
182
+ pattern: /\A#{Regexp.escape(req.path_info)}\z/,
183
+ param_names: [],
184
+ block: nil,
185
+ params: {},
186
+ body: nil,
187
+ inject: [],
188
+ depends: [],
189
+ websocket: false
190
+ }
191
+ end
192
+
193
+ def handle_websocket(env, route)
194
+ ws = WebSocket.new(env, &route[:block])
195
+ hijack_io = env["rack.hijack"]&.call
196
+ return [500, {}, ["rack.hijack not available"]] unless hijack_io
197
+
198
+ ws_thread = Thread.new do
199
+ ws.instance_variable_set(:@io, hijack_io)
200
+ ws.process!
201
+ end
202
+ ws_thread.join
203
+
204
+ [101, {}, []]
205
+ rescue => e
206
+ [500, { "content-type" => "application/json" }, [JSON.generate({ error: e.class.name, message: e.message })]]
207
+ end
208
+
209
+ def error_response(ctx)
210
+ headers = { "content-type" => "application/json" }.merge(ctx.response_headers)
211
+ body = case ctx.response_body
212
+ when Hash, Array then JSON.generate(ctx.response_body)
213
+ when String then ctx.response_body
214
+ else ctx.response_body.to_s
215
+ end
216
+ [ctx.response_status, headers, [body]]
217
+ end
218
+
219
+ def inject_deps(ctx, route)
220
+ deps = route[:injected_deps] || []
221
+ deps.each do |name|
222
+ resolved = @container.resolve(name)
223
+ ctx.set(name, resolved)
224
+ end
225
+
226
+ checks = route[:dependency_checks] || []
227
+ checks.each do |check_def|
228
+ check_val = check_def[:check]
229
+ status = check_def[:status] || 401
230
+
231
+ allowed = case check_val
232
+ when Proc
233
+ check_val.call(ctx)
234
+ when Symbol
235
+ dep = @container.resolve(check_val)
236
+ dep&.call(ctx)
237
+ when Class
238
+ check_val.new.call(ctx)
239
+ else
240
+ true
241
+ end
242
+
243
+ unless allowed
244
+ ctx.response_status = status
245
+ ctx.response_body = { error: "Forbidden" }
246
+ return
247
+ end
248
+ end
249
+ end
250
+
251
+ def serialize_response(ctx)
252
+ body = ctx.response_body
253
+ status = ctx.response_status || 200
254
+ headers = { "content-type" => "application/json" }.merge(ctx.response_headers)
255
+
256
+ if body.is_a?(StreamingBody)
257
+ [status, headers, body]
258
+ else
259
+ json = case body
260
+ when Hash, Array then JSON.generate(body)
261
+ when String then body
262
+ else body.to_s
263
+ end
264
+
265
+ [status, headers, [json]]
266
+ end
267
+ end
268
+
269
+ def set_session_cookie(ctx, session)
270
+ cookie_value = session.serialize
271
+ ctx.response_headers["set-cookie"] = "#{Session::SESSION_COOKIE}=#{cookie_value}; path=/; HttpOnly"
272
+ end
273
+
274
+ def find_error_handler(exception)
275
+ @error_handlers.each do |klass, status|
276
+ return status if exception.is_a?(klass)
277
+ end
278
+ nil
279
+ end
280
+
281
+ def serve_openapi
282
+ spec = OpenAPI.new(self)
283
+ [200, { "content-type" => "application/json" }, [spec.to_json]]
284
+ end
285
+
286
+ def serve_swagger_ui
287
+ html = <<~HTML
288
+ <!DOCTYPE html>
289
+ <html>
290
+ <head>
291
+ <title>RubyAPI Docs</title>
292
+ <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css">
293
+ </head>
294
+ <body>
295
+ <div id="swagger-ui"></div>
296
+ <script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script>
297
+ <script>
298
+ SwaggerUIBundle({ url: "/openapi.json", dom_id: "#swagger-ui" });
299
+ </script>
300
+ </body>
301
+ </html>
302
+ HTML
303
+ [200, { "content-type" => "text/html" }, [html]]
304
+ end
305
+ end
306
+ end
metadata ADDED
@@ -0,0 +1,148 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fastrb
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Bruno Queiroz
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: rack
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: bigdecimal
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '3.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: base64
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.2'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.2'
54
+ - !ruby/object:Gem::Dependency
55
+ name: jwt
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '2.7'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '2.7'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rspec
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '3.0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '3.0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: rack-test
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '2.0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '2.0'
96
+ description: FastRb is a modern web framework for Ruby, inspired by FastAPI, with
97
+ optional typing, automatic validation, and OpenAPI documentation.
98
+ executables:
99
+ - rubyapi
100
+ extensions: []
101
+ extra_rdoc_files: []
102
+ files:
103
+ - bin/rubyapi
104
+ - lib/rubyapi.rb
105
+ - lib/rubyapi/cli.rb
106
+ - lib/rubyapi/config.rb
107
+ - lib/rubyapi/context.rb
108
+ - lib/rubyapi/di.rb
109
+ - lib/rubyapi/error_handler.rb
110
+ - lib/rubyapi/job.rb
111
+ - lib/rubyapi/middleware/logging.rb
112
+ - lib/rubyapi/openapi.rb
113
+ - lib/rubyapi/param_converters.rb
114
+ - lib/rubyapi/plugin.rb
115
+ - lib/rubyapi/plugins/auth.rb
116
+ - lib/rubyapi/plugins/cache.rb
117
+ - lib/rubyapi/plugins/cors.rb
118
+ - lib/rubyapi/plugins/jwt.rb
119
+ - lib/rubyapi/router.rb
120
+ - lib/rubyapi/schema.rb
121
+ - lib/rubyapi/serializer.rb
122
+ - lib/rubyapi/session.rb
123
+ - lib/rubyapi/sse.rb
124
+ - lib/rubyapi/streaming.rb
125
+ - lib/rubyapi/version.rb
126
+ - lib/rubyapi/websocket.rb
127
+ homepage: https://github.com/bruno/rubyapi
128
+ licenses:
129
+ - MIT
130
+ metadata: {}
131
+ rdoc_options: []
132
+ require_paths:
133
+ - lib
134
+ required_ruby_version: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: 3.3.0
139
+ required_rubygems_version: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - ">="
142
+ - !ruby/object:Gem::Version
143
+ version: '0'
144
+ requirements: []
145
+ rubygems_version: 4.0.16
146
+ specification_version: 4
147
+ summary: A FastAPI-inspired web framework for Ruby
148
+ test_files: []