tarsier 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 (47) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +175 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +984 -0
  5. data/exe/tarsier +7 -0
  6. data/lib/tarsier/application.rb +336 -0
  7. data/lib/tarsier/cli/commands/console.rb +87 -0
  8. data/lib/tarsier/cli/commands/generate.rb +85 -0
  9. data/lib/tarsier/cli/commands/help.rb +50 -0
  10. data/lib/tarsier/cli/commands/new.rb +59 -0
  11. data/lib/tarsier/cli/commands/routes.rb +139 -0
  12. data/lib/tarsier/cli/commands/server.rb +123 -0
  13. data/lib/tarsier/cli/commands/version.rb +14 -0
  14. data/lib/tarsier/cli/generators/app.rb +528 -0
  15. data/lib/tarsier/cli/generators/base.rb +93 -0
  16. data/lib/tarsier/cli/generators/controller.rb +91 -0
  17. data/lib/tarsier/cli/generators/middleware.rb +81 -0
  18. data/lib/tarsier/cli/generators/migration.rb +109 -0
  19. data/lib/tarsier/cli/generators/model.rb +109 -0
  20. data/lib/tarsier/cli/generators/resource.rb +27 -0
  21. data/lib/tarsier/cli/loader.rb +18 -0
  22. data/lib/tarsier/cli.rb +46 -0
  23. data/lib/tarsier/controller.rb +282 -0
  24. data/lib/tarsier/database.rb +588 -0
  25. data/lib/tarsier/errors.rb +77 -0
  26. data/lib/tarsier/middleware/base.rb +47 -0
  27. data/lib/tarsier/middleware/compression.rb +113 -0
  28. data/lib/tarsier/middleware/cors.rb +101 -0
  29. data/lib/tarsier/middleware/csrf.rb +88 -0
  30. data/lib/tarsier/middleware/logger.rb +74 -0
  31. data/lib/tarsier/middleware/rate_limit.rb +110 -0
  32. data/lib/tarsier/middleware/stack.rb +143 -0
  33. data/lib/tarsier/middleware/static.rb +124 -0
  34. data/lib/tarsier/model.rb +590 -0
  35. data/lib/tarsier/params.rb +269 -0
  36. data/lib/tarsier/query.rb +495 -0
  37. data/lib/tarsier/request.rb +274 -0
  38. data/lib/tarsier/response.rb +282 -0
  39. data/lib/tarsier/router/compiler.rb +173 -0
  40. data/lib/tarsier/router/node.rb +97 -0
  41. data/lib/tarsier/router/route.rb +119 -0
  42. data/lib/tarsier/router.rb +272 -0
  43. data/lib/tarsier/version.rb +5 -0
  44. data/lib/tarsier/websocket.rb +275 -0
  45. data/lib/tarsier.rb +167 -0
  46. data/sig/tarsier.rbs +485 -0
  47. metadata +230 -0
data/exe/tarsier ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "tarsier"
5
+ require "tarsier/cli/loader"
6
+
7
+ Tarsier::CLI.start(ARGV)
@@ -0,0 +1,336 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tarsier
4
+ # Main application class - the entry point for Tarsier apps
5
+ #
6
+ # Tarsier::Application is Rack-compatible and provides both a minimal
7
+ # Flask-style API and a more structured Rails-style API.
8
+ #
9
+ # @example Minimal (Flask-style)
10
+ # app = Tarsier.app do
11
+ # get('/') { { message: 'Hello!' } }
12
+ # post('/users') { |req| User.create(req.params) }
13
+ # end
14
+ #
15
+ # @example Structured (Rails-style)
16
+ # Tarsier.application do
17
+ # configure { self.secret_key = ENV['SECRET'] }
18
+ # use Tarsier::Middleware::Logger
19
+ # routes do
20
+ # resources :users
21
+ # end
22
+ # end
23
+ #
24
+ # @since 0.1.0
25
+ class Application
26
+ # @return [Router] the application router
27
+ attr_reader :router
28
+
29
+ # @return [Middleware::Stack] the middleware stack
30
+ attr_reader :middleware_stack
31
+
32
+ # @return [Configuration] application configuration
33
+ attr_reader :config
34
+
35
+ # Create a new application
36
+ def initialize
37
+ @router = Router.new
38
+ @middleware_stack = Middleware::Stack.new
39
+ @config = Configuration.new
40
+ @booted = false
41
+ end
42
+
43
+ # Configure the application
44
+ #
45
+ # @yield [Configuration] configuration object
46
+ # @return [self]
47
+ #
48
+ # @example
49
+ # configure do
50
+ # self.secret_key = 'my-secret'
51
+ # self.log_level = :debug
52
+ # end
53
+ def configure(&block)
54
+ @config.instance_eval(&block) if block_given?
55
+ self
56
+ end
57
+
58
+ # Define routes using the router DSL
59
+ #
60
+ # @yield [Router] router instance
61
+ # @return [self]
62
+ #
63
+ # @example
64
+ # routes do
65
+ # root to: 'home#index'
66
+ # resources :users
67
+ # end
68
+ def routes(&block)
69
+ @router.draw(&block)
70
+ self
71
+ end
72
+
73
+ # Add middleware to the stack
74
+ #
75
+ # @param middleware [Class] middleware class
76
+ # @param args [Array] middleware arguments
77
+ # @param options [Hash] middleware options
78
+ # @return [self]
79
+ #
80
+ # @example
81
+ # use Tarsier::Middleware::Logger
82
+ # use Tarsier::Middleware::CORS, origins: ['*']
83
+ def use(middleware, *args, **options)
84
+ @middleware_stack.use(middleware, *args, **options)
85
+ self
86
+ end
87
+
88
+ # Define a GET route (Flask-style)
89
+ #
90
+ # @param path [String] route path
91
+ # @param to [String, Proc, nil] route handler
92
+ # @yield request handler block
93
+ # @return [self]
94
+ #
95
+ # @example
96
+ # get('/') { { status: 'ok' } }
97
+ # get('/users/:id') { |req| User.find(req.params[:id]) }
98
+ def get(path, to: nil, **options, &block)
99
+ add_inline_route(:get, path, to, **options, &block)
100
+ end
101
+
102
+ # Define a POST route (Flask-style)
103
+ #
104
+ # @param path [String] route path
105
+ # @param to [String, Proc, nil] route handler
106
+ # @yield request handler block
107
+ # @return [self]
108
+ def post(path, to: nil, **options, &block)
109
+ add_inline_route(:post, path, to, **options, &block)
110
+ end
111
+
112
+ # Define a PUT route (Flask-style)
113
+ #
114
+ # @param path [String] route path
115
+ # @param to [String, Proc, nil] route handler
116
+ # @yield request handler block
117
+ # @return [self]
118
+ def put(path, to: nil, **options, &block)
119
+ add_inline_route(:put, path, to, **options, &block)
120
+ end
121
+
122
+ # Define a PATCH route (Flask-style)
123
+ #
124
+ # @param path [String] route path
125
+ # @param to [String, Proc, nil] route handler
126
+ # @yield request handler block
127
+ # @return [self]
128
+ def patch(path, to: nil, **options, &block)
129
+ add_inline_route(:patch, path, to, **options, &block)
130
+ end
131
+
132
+ # Define a DELETE route (Flask-style)
133
+ #
134
+ # @param path [String] route path
135
+ # @param to [String, Proc, nil] route handler
136
+ # @yield request handler block
137
+ # @return [self]
138
+ def delete(path, to: nil, **options, &block)
139
+ add_inline_route(:delete, path, to, **options, &block)
140
+ end
141
+
142
+ # Boot the application (compile routes, etc.)
143
+ #
144
+ # @return [self]
145
+ def boot!
146
+ return self if @booted
147
+
148
+ @router.compile!
149
+ @booted = true
150
+ self
151
+ end
152
+
153
+ # Check if application is booted
154
+ #
155
+ # @return [Boolean]
156
+ def booted?
157
+ @booted
158
+ end
159
+
160
+ # Rack interface
161
+ #
162
+ # @param env [Hash] Rack environment
163
+ # @return [Array] Rack response tuple [status, headers, body]
164
+ def call(env)
165
+ boot! unless @booted
166
+
167
+ request = Request.new(env)
168
+ response = Response.new
169
+
170
+ begin
171
+ @middleware_stack.call(request, response, method(:dispatch))
172
+ rescue RouteNotFoundError => e
173
+ handle_not_found(request, response, e)
174
+ rescue StandardError => e
175
+ handle_error(request, response, e)
176
+ end
177
+
178
+ response.finish
179
+ end
180
+
181
+ # Direct dispatch (bypasses Rack for performance)
182
+ #
183
+ # @param request [Request] request object
184
+ # @param response [Response] response object
185
+ # @return [Response]
186
+ def dispatch(request, response)
187
+ match = @router.match(request.method, request.path)
188
+
189
+ unless match
190
+ raise RouteNotFoundError.new(request.method, request.path)
191
+ end
192
+
193
+ route, params = match
194
+ request = Request.new(request.env, params)
195
+
196
+ execute_route(route, request, response)
197
+ end
198
+
199
+ # Get a named route path
200
+ #
201
+ # @param name [Symbol] route name
202
+ # @param params [Hash] route parameters
203
+ # @return [String]
204
+ def path_for(name, params = {})
205
+ @router.path_for(name, params)
206
+ end
207
+
208
+ # Get all routes (for debugging/inspection)
209
+ #
210
+ # @return [Array<Route>]
211
+ def routes_list
212
+ @router.routes
213
+ end
214
+
215
+ private
216
+
217
+ def add_inline_route(method, path, to, **options, &block)
218
+ handler = block || to
219
+ @router.send(method, path, to: handler, **options)
220
+ self
221
+ end
222
+
223
+ def execute_route(route, request, response)
224
+ if route.proc_handler?
225
+ execute_proc_handler(route, request, response)
226
+ else
227
+ execute_controller_action(route, request, response)
228
+ end
229
+
230
+ response
231
+ end
232
+
233
+ def execute_proc_handler(route, request, response)
234
+ handler = route.handler
235
+ result = case handler.arity
236
+ when 0 then handler.call
237
+ when 1 then handler.call(request)
238
+ else handler.call(request, response)
239
+ end
240
+
241
+ # Auto-render hash as JSON
242
+ response.json(result) if result.is_a?(Hash) && !response.sent?
243
+ end
244
+
245
+ def execute_controller_action(route, request, response)
246
+ controller_class = resolve_controller_class(route.controller_class)
247
+ action = route.action_name
248
+
249
+ unless controller_class.method_defined?(action)
250
+ raise ActionNotFoundError.new(controller_class, action)
251
+ end
252
+
253
+ controller = controller_class.new(request, response, action)
254
+ controller.dispatch
255
+ end
256
+
257
+ def resolve_controller_class(class_name)
258
+ return class_name if class_name.is_a?(Class)
259
+
260
+ class_name.split("::").reduce(Object) do |mod, name|
261
+ mod.const_get(name)
262
+ end
263
+ end
264
+
265
+ def handle_not_found(request, response, error)
266
+ response.status = 404
267
+ response.content_type = "application/json"
268
+ response.body = JSON.generate({ error: "Not Found", path: error.path })
269
+ end
270
+
271
+ def handle_error(request, response, error)
272
+ response.status = 500
273
+ response.content_type = "application/json"
274
+
275
+ body = { error: error.class.name, message: error.message }
276
+ body[:backtrace] = error.backtrace&.first(10) if Tarsier.development?
277
+
278
+ response.body = JSON.generate(body)
279
+ log_error(error) if Tarsier.production?
280
+ end
281
+
282
+ def log_error(error)
283
+ warn "[Tarsier] #{error.class}: #{error.message}"
284
+ warn error.backtrace&.first(5)&.join("\n")
285
+ end
286
+ end
287
+
288
+ # Application configuration
289
+ #
290
+ # @since 0.1.0
291
+ class Configuration
292
+ # @return [String] secret key for sessions and CSRF
293
+ attr_accessor :secret_key
294
+
295
+ # @return [Symbol] session storage type
296
+ attr_accessor :session_store
297
+
298
+ # @return [Symbol] log level
299
+ attr_accessor :log_level
300
+
301
+ # @return [Boolean] serve static files
302
+ attr_accessor :static_files
303
+
304
+ # @return [Symbol] default response format
305
+ attr_accessor :default_format
306
+
307
+ # @return [Boolean] force SSL
308
+ attr_accessor :force_ssl
309
+
310
+ # @return [String] server host
311
+ attr_accessor :host
312
+
313
+ # @return [Integer] server port
314
+ attr_accessor :port
315
+
316
+ def initialize
317
+ @secret_key = ENV["TARSIER_SECRET_KEY"]
318
+ @session_store = :cookie
319
+ @log_level = :info
320
+ @static_files = true
321
+ @default_format = :json
322
+ @force_ssl = false
323
+ @host = "0.0.0.0"
324
+ @port = 7827
325
+ end
326
+
327
+ # Set configuration from hash
328
+ #
329
+ # @param options [Hash] configuration options
330
+ def set(options)
331
+ options.each do |key, value|
332
+ send("#{key}=", value) if respond_to?("#{key}=")
333
+ end
334
+ end
335
+ end
336
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tarsier
4
+ class CLI
5
+ module Commands
6
+ class Console
7
+ HELP = <<~HELP
8
+ Usage: tarsier console [options]
9
+
10
+ Start an interactive console with your application loaded.
11
+
12
+ Options:
13
+ -e, --env ENV Environment to load (default: development)
14
+
15
+ Examples:
16
+ tarsier console
17
+ tarsier console -e production
18
+ HELP
19
+
20
+ def self.show_help
21
+ puts HELP
22
+ end
23
+
24
+ def self.run(args)
25
+ options = parse_options(args)
26
+ ENV["TARSIER_ENV"] = options[:env]
27
+
28
+ puts "Tarsier Console (#{options[:env]})"
29
+ puts "Type 'exit' to quit"
30
+ puts
31
+
32
+ load_application
33
+ start_repl
34
+ end
35
+
36
+ def self.parse_options(args)
37
+ options = { env: "development" }
38
+
39
+ i = 0
40
+ while i < args.length
41
+ case args[i]
42
+ when "-e", "--env"
43
+ options[:env] = args[i + 1]
44
+ i += 2
45
+ else
46
+ i += 1
47
+ end
48
+ end
49
+
50
+ options
51
+ end
52
+
53
+ def self.load_application
54
+ if File.exist?("config/application.rb")
55
+ require "./config/application"
56
+ elsif File.exist?("app.rb")
57
+ require "./app"
58
+ end
59
+ rescue LoadError => e
60
+ puts "Warning: Could not load application: #{e.message}"
61
+ end
62
+
63
+ def self.start_repl
64
+ require "irb"
65
+ IRB.start
66
+ rescue LoadError
67
+ simple_repl
68
+ end
69
+
70
+ def self.simple_repl
71
+ loop do
72
+ print "tarsier> "
73
+ input = gets&.chomp
74
+ break if input.nil? || input == "exit"
75
+
76
+ begin
77
+ result = eval(input) # rubocop:disable Security/Eval
78
+ puts "=> #{result.inspect}"
79
+ rescue StandardError => e
80
+ puts "Error: #{e.message}"
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tarsier
4
+ class CLI
5
+ module Commands
6
+ class Generate
7
+ HELP = <<~HELP
8
+ Usage: tarsier generate <type> <name> [options]
9
+
10
+ Generate various components for your Tarsier application.
11
+
12
+ Types:
13
+ controller NAME [actions] Generate a controller with optional actions
14
+ model NAME [attributes] Generate a model with optional attributes
15
+ resource NAME [attrs] Generate model, controller, and routes
16
+ middleware NAME Generate a middleware class
17
+ migration NAME Generate a database migration
18
+
19
+ Examples:
20
+ tarsier generate controller users index show create
21
+ tarsier generate model user name:string email:string
22
+ tarsier generate resource post title:string body:text
23
+ tarsier generate middleware authentication
24
+ HELP
25
+
26
+ def self.show_help
27
+ puts HELP
28
+ end
29
+
30
+ def self.run(args)
31
+ type = args.shift
32
+ name = args.shift
33
+
34
+ if type.nil?
35
+ puts "Error: Please specify what to generate"
36
+ show_help
37
+ exit 1
38
+ end
39
+
40
+ if name.nil?
41
+ puts "Error: Please provide a name"
42
+ exit 1
43
+ end
44
+
45
+ case type
46
+ when "controller", "c"
47
+ generate_controller(name, args)
48
+ when "model", "m"
49
+ generate_model(name, args)
50
+ when "resource", "r"
51
+ generate_resource(name, args)
52
+ when "middleware"
53
+ generate_middleware(name)
54
+ when "migration"
55
+ generate_migration(name, args)
56
+ else
57
+ puts "Unknown generator: #{type}"
58
+ show_help
59
+ exit 1
60
+ end
61
+ end
62
+
63
+ def self.generate_controller(name, actions)
64
+ Generators::Controller.new(name, actions).generate
65
+ end
66
+
67
+ def self.generate_model(name, attributes)
68
+ Generators::Model.new(name, attributes).generate
69
+ end
70
+
71
+ def self.generate_resource(name, attributes)
72
+ Generators::Resource.new(name, attributes).generate
73
+ end
74
+
75
+ def self.generate_middleware(name)
76
+ Generators::Middleware.new(name).generate
77
+ end
78
+
79
+ def self.generate_migration(name, columns)
80
+ Generators::Migration.new(name, columns).generate
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tarsier
4
+ class CLI
5
+ module Commands
6
+ class Help
7
+ BANNER = <<~BANNER
8
+ Tarsier - A modern, high-performance Ruby web framework
9
+
10
+ Usage: tarsier <command> [options]
11
+
12
+ Commands:
13
+ new APP_NAME Create a new Tarsier application
14
+ generate (g) Generate controllers, models, etc.
15
+ server (s) Start the development server
16
+ console (c) Start an interactive console
17
+ routes (r) Display all routes
18
+ version (v) Show Tarsier version
19
+ help Show this help message
20
+
21
+ Run 'tarsier <command> --help' for more information on a specific command.
22
+ BANNER
23
+
24
+ def self.run(args)
25
+ if args.first
26
+ show_command_help(args.first)
27
+ else
28
+ puts BANNER
29
+ end
30
+ end
31
+
32
+ def self.show_command_help(command)
33
+ case command
34
+ when "new", "n"
35
+ New.show_help
36
+ when "generate", "g"
37
+ Generate.show_help
38
+ when "server", "s"
39
+ Server.show_help
40
+ when "routes", "r"
41
+ Routes.show_help
42
+ else
43
+ puts "Unknown command: #{command}"
44
+ puts BANNER
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tarsier
4
+ class CLI
5
+ module Commands
6
+ class New
7
+ HELP = <<~HELP
8
+ Usage: tarsier new APP_NAME [options]
9
+
10
+ Create a new Tarsier application with the given name.
11
+
12
+ Options:
13
+ --api Create an API-only application (no views)
14
+ --minimal Create a minimal application
15
+ --skip-git Skip git initialization
16
+ --skip-bundle Skip bundle install
17
+
18
+ Examples:
19
+ tarsier new my_app
20
+ tarsier new my_api --api
21
+ tarsier new my_app --minimal
22
+ HELP
23
+
24
+ def self.show_help
25
+ puts HELP
26
+ end
27
+
28
+ def self.run(args)
29
+ options = parse_options(args)
30
+ app_name = args.first
31
+
32
+ if app_name.nil? || app_name.empty?
33
+ puts "Error: Please provide an application name"
34
+ puts "Usage: tarsier new APP_NAME"
35
+ exit 1
36
+ end
37
+
38
+ Generator.new(app_name, options).generate
39
+ end
40
+
41
+ def self.parse_options(args)
42
+ options = { api: false, minimal: false, skip_git: false, skip_bundle: false }
43
+
44
+ args.delete_if do |arg|
45
+ case arg
46
+ when "--api" then options[:api] = true
47
+ when "--minimal" then options[:minimal] = true
48
+ when "--skip-git" then options[:skip_git] = true
49
+ when "--skip-bundle" then options[:skip_bundle] = true
50
+ else false
51
+ end
52
+ end
53
+
54
+ options
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end