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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +175 -0
- data/LICENSE.txt +21 -0
- data/README.md +984 -0
- data/exe/tarsier +7 -0
- data/lib/tarsier/application.rb +336 -0
- data/lib/tarsier/cli/commands/console.rb +87 -0
- data/lib/tarsier/cli/commands/generate.rb +85 -0
- data/lib/tarsier/cli/commands/help.rb +50 -0
- data/lib/tarsier/cli/commands/new.rb +59 -0
- data/lib/tarsier/cli/commands/routes.rb +139 -0
- data/lib/tarsier/cli/commands/server.rb +123 -0
- data/lib/tarsier/cli/commands/version.rb +14 -0
- data/lib/tarsier/cli/generators/app.rb +528 -0
- data/lib/tarsier/cli/generators/base.rb +93 -0
- data/lib/tarsier/cli/generators/controller.rb +91 -0
- data/lib/tarsier/cli/generators/middleware.rb +81 -0
- data/lib/tarsier/cli/generators/migration.rb +109 -0
- data/lib/tarsier/cli/generators/model.rb +109 -0
- data/lib/tarsier/cli/generators/resource.rb +27 -0
- data/lib/tarsier/cli/loader.rb +18 -0
- data/lib/tarsier/cli.rb +46 -0
- data/lib/tarsier/controller.rb +282 -0
- data/lib/tarsier/database.rb +588 -0
- data/lib/tarsier/errors.rb +77 -0
- data/lib/tarsier/middleware/base.rb +47 -0
- data/lib/tarsier/middleware/compression.rb +113 -0
- data/lib/tarsier/middleware/cors.rb +101 -0
- data/lib/tarsier/middleware/csrf.rb +88 -0
- data/lib/tarsier/middleware/logger.rb +74 -0
- data/lib/tarsier/middleware/rate_limit.rb +110 -0
- data/lib/tarsier/middleware/stack.rb +143 -0
- data/lib/tarsier/middleware/static.rb +124 -0
- data/lib/tarsier/model.rb +590 -0
- data/lib/tarsier/params.rb +269 -0
- data/lib/tarsier/query.rb +495 -0
- data/lib/tarsier/request.rb +274 -0
- data/lib/tarsier/response.rb +282 -0
- data/lib/tarsier/router/compiler.rb +173 -0
- data/lib/tarsier/router/node.rb +97 -0
- data/lib/tarsier/router/route.rb +119 -0
- data/lib/tarsier/router.rb +272 -0
- data/lib/tarsier/version.rb +5 -0
- data/lib/tarsier/websocket.rb +275 -0
- data/lib/tarsier.rb +167 -0
- data/sig/tarsier.rbs +485 -0
- metadata +230 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarsier
|
|
4
|
+
class CLI
|
|
5
|
+
module Generators
|
|
6
|
+
class Middleware < Base
|
|
7
|
+
def generate
|
|
8
|
+
create_file(middleware_path, middleware_content)
|
|
9
|
+
create_file(spec_path, spec_content)
|
|
10
|
+
puts "\nMiddleware generated successfully!"
|
|
11
|
+
puts "Add to your application:"
|
|
12
|
+
puts " use #{camelize(name)}Middleware"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def middleware_path
|
|
18
|
+
"app/middleware/#{underscore(name)}_middleware.rb"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def spec_path
|
|
22
|
+
"spec/middleware/#{underscore(name)}_middleware_spec.rb"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def middleware_content
|
|
26
|
+
<<~RUBY
|
|
27
|
+
# frozen_string_literal: true
|
|
28
|
+
|
|
29
|
+
class #{camelize(name)}Middleware < Tarsier::Middleware::Base
|
|
30
|
+
def call(request, response)
|
|
31
|
+
# Before request processing
|
|
32
|
+
before(request, response)
|
|
33
|
+
|
|
34
|
+
# Call next middleware/app
|
|
35
|
+
@app.call(request, response)
|
|
36
|
+
|
|
37
|
+
# After request processing
|
|
38
|
+
after(request, response)
|
|
39
|
+
|
|
40
|
+
response
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def before(request, response)
|
|
46
|
+
# Add your before logic here
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def after(request, response)
|
|
50
|
+
# Add your after logic here
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
RUBY
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def spec_content
|
|
57
|
+
<<~RUBY
|
|
58
|
+
# frozen_string_literal: true
|
|
59
|
+
|
|
60
|
+
require "spec_helper"
|
|
61
|
+
|
|
62
|
+
RSpec.describe #{camelize(name)}Middleware do
|
|
63
|
+
let(:app) { ->(req, res) { res } }
|
|
64
|
+
let(:middleware) { described_class.new(app) }
|
|
65
|
+
|
|
66
|
+
describe "#call" do
|
|
67
|
+
it "passes request to next middleware" do
|
|
68
|
+
request = double("request")
|
|
69
|
+
response = double("response")
|
|
70
|
+
|
|
71
|
+
expect(app).to receive(:call).with(request, response)
|
|
72
|
+
middleware.call(request, response)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
RUBY
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarsier
|
|
4
|
+
class CLI
|
|
5
|
+
module Generators
|
|
6
|
+
class Migration < Base
|
|
7
|
+
def initialize(name, columns = [])
|
|
8
|
+
super(name)
|
|
9
|
+
@columns = parse_columns(columns)
|
|
10
|
+
@timestamp = Time.now.strftime("%Y%m%d%H%M%S")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def generate
|
|
14
|
+
create_file(migration_path, migration_content)
|
|
15
|
+
puts "\nMigration generated successfully!"
|
|
16
|
+
puts "Run: tarsier db:migrate"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def parse_columns(cols)
|
|
22
|
+
cols.map do |col|
|
|
23
|
+
name, type = col.split(":")
|
|
24
|
+
type ||= "string"
|
|
25
|
+
{ name: name, type: type }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def migration_path
|
|
30
|
+
"db/migrations/#{@timestamp}_#{underscore(name)}.rb"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def migration_content
|
|
34
|
+
if name.start_with?("create_")
|
|
35
|
+
create_table_migration
|
|
36
|
+
elsif name.start_with?("add_")
|
|
37
|
+
add_column_migration
|
|
38
|
+
else
|
|
39
|
+
generic_migration
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def create_table_migration
|
|
44
|
+
table_name = name.sub(/^create_/, "")
|
|
45
|
+
<<~RUBY
|
|
46
|
+
# frozen_string_literal: true
|
|
47
|
+
|
|
48
|
+
class #{camelize(name)} < Tarsier::Migration
|
|
49
|
+
def up
|
|
50
|
+
create_table :#{table_name} do |t|
|
|
51
|
+
#{column_definitions}
|
|
52
|
+
t.timestamps
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def down
|
|
57
|
+
drop_table :#{table_name}
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
RUBY
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def add_column_migration
|
|
64
|
+
match = name.match(/^add_(.+)_to_(.+)$/)
|
|
65
|
+
column_name = match ? match[1] : "column"
|
|
66
|
+
table_name = match ? match[2] : "table"
|
|
67
|
+
|
|
68
|
+
<<~RUBY
|
|
69
|
+
# frozen_string_literal: true
|
|
70
|
+
|
|
71
|
+
class #{camelize(name)} < Tarsier::Migration
|
|
72
|
+
def up
|
|
73
|
+
add_column :#{table_name}, :#{column_name}, :string
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def down
|
|
77
|
+
remove_column :#{table_name}, :#{column_name}
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
RUBY
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def generic_migration
|
|
84
|
+
<<~RUBY
|
|
85
|
+
# frozen_string_literal: true
|
|
86
|
+
|
|
87
|
+
class #{camelize(name)} < Tarsier::Migration
|
|
88
|
+
def up
|
|
89
|
+
# Add your migration code here
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def down
|
|
93
|
+
# Add rollback code here
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
RUBY
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def column_definitions
|
|
100
|
+
return " # Add columns here" if @columns.empty?
|
|
101
|
+
|
|
102
|
+
@columns.map do |col|
|
|
103
|
+
" t.#{col[:type]} :#{col[:name]}"
|
|
104
|
+
end.join("\n")
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarsier
|
|
4
|
+
class CLI
|
|
5
|
+
module Generators
|
|
6
|
+
class Model < Base
|
|
7
|
+
TYPES = {
|
|
8
|
+
"string" => ":string",
|
|
9
|
+
"text" => ":text",
|
|
10
|
+
"integer" => ":integer",
|
|
11
|
+
"float" => ":float",
|
|
12
|
+
"boolean" => ":boolean",
|
|
13
|
+
"datetime" => ":datetime",
|
|
14
|
+
"date" => ":date",
|
|
15
|
+
"json" => ":json"
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
def initialize(name, attributes = [])
|
|
19
|
+
super(name)
|
|
20
|
+
@attributes = parse_attributes(attributes)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def generate
|
|
24
|
+
create_file(model_path, model_content)
|
|
25
|
+
create_file(spec_path, spec_content)
|
|
26
|
+
puts "\nModel generated successfully!"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def parse_attributes(attrs)
|
|
32
|
+
attrs.map do |attr|
|
|
33
|
+
name, type = attr.split(":")
|
|
34
|
+
type ||= "string"
|
|
35
|
+
{ name: name, type: TYPES[type] || ":string" }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def model_path
|
|
40
|
+
"app/models/#{underscore(name)}.rb"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def spec_path
|
|
44
|
+
"spec/models/#{underscore(name)}_spec.rb"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def model_content
|
|
48
|
+
<<~RUBY
|
|
49
|
+
# frozen_string_literal: true
|
|
50
|
+
|
|
51
|
+
class #{camelize(name)} < Tarsier::Model
|
|
52
|
+
table_name "#{pluralize(underscore(name))}"
|
|
53
|
+
|
|
54
|
+
#{attribute_definitions}
|
|
55
|
+
#{validation_definitions}
|
|
56
|
+
end
|
|
57
|
+
RUBY
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def attribute_definitions
|
|
61
|
+
return " # Add attributes here" if @attributes.empty?
|
|
62
|
+
|
|
63
|
+
@attributes.map do |attr|
|
|
64
|
+
" attribute :#{attr[:name]}, #{attr[:type]}"
|
|
65
|
+
end.join("\n")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def validation_definitions
|
|
69
|
+
return "" if @attributes.empty?
|
|
70
|
+
|
|
71
|
+
"\n" + @attributes.map do |attr|
|
|
72
|
+
" # validates :#{attr[:name]}, presence: true"
|
|
73
|
+
end.join("\n")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def spec_content
|
|
77
|
+
<<~RUBY
|
|
78
|
+
# frozen_string_literal: true
|
|
79
|
+
|
|
80
|
+
require "spec_helper"
|
|
81
|
+
|
|
82
|
+
RSpec.describe #{camelize(name)} do
|
|
83
|
+
describe "validations" do
|
|
84
|
+
#{validation_specs}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
describe "associations" do
|
|
88
|
+
pending "add association tests"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
RUBY
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def validation_specs
|
|
95
|
+
return ' pending "add validation tests"' if @attributes.empty?
|
|
96
|
+
|
|
97
|
+
@attributes.map do |attr|
|
|
98
|
+
<<~RUBY.chomp
|
|
99
|
+
it "has #{attr[:name]} attribute" do
|
|
100
|
+
model = described_class.new(#{attr[:name]}: "test")
|
|
101
|
+
expect(model.#{attr[:name]}).to eq("test")
|
|
102
|
+
end
|
|
103
|
+
RUBY
|
|
104
|
+
end.join("\n\n")
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarsier
|
|
4
|
+
class CLI
|
|
5
|
+
module Generators
|
|
6
|
+
class Resource < Base
|
|
7
|
+
ACTIONS = %w[index show new create edit update destroy].freeze
|
|
8
|
+
|
|
9
|
+
def initialize(name, attributes = [])
|
|
10
|
+
super(name)
|
|
11
|
+
@attributes = attributes
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def generate
|
|
15
|
+
puts "Generating resource: #{name}"
|
|
16
|
+
puts
|
|
17
|
+
|
|
18
|
+
Model.new(name, @attributes).generate
|
|
19
|
+
Controller.new(name, ACTIONS).generate
|
|
20
|
+
|
|
21
|
+
puts "\nAdd to config/routes.rb:"
|
|
22
|
+
puts " resources :#{pluralize(underscore(name))}"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Load all CLI components
|
|
4
|
+
require_relative "../cli"
|
|
5
|
+
require_relative "commands/help"
|
|
6
|
+
require_relative "commands/version"
|
|
7
|
+
require_relative "commands/new"
|
|
8
|
+
require_relative "commands/generate"
|
|
9
|
+
require_relative "commands/server"
|
|
10
|
+
require_relative "commands/console"
|
|
11
|
+
require_relative "commands/routes"
|
|
12
|
+
require_relative "generators/base"
|
|
13
|
+
require_relative "generators/controller"
|
|
14
|
+
require_relative "generators/model"
|
|
15
|
+
require_relative "generators/resource"
|
|
16
|
+
require_relative "generators/middleware"
|
|
17
|
+
require_relative "generators/migration"
|
|
18
|
+
require_relative "generators/app"
|
data/lib/tarsier/cli.rb
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Tarsier
|
|
7
|
+
# Command-line interface for Tarsier framework
|
|
8
|
+
class CLI
|
|
9
|
+
COMMANDS = %w[new generate server console routes version help].freeze
|
|
10
|
+
ALIASES = { "g" => "generate", "s" => "server", "c" => "console", "r" => "routes", "n" => "new", "v" => "version" }.freeze
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def start(args = ARGV)
|
|
14
|
+
new(args).run
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize(args)
|
|
19
|
+
@args = args
|
|
20
|
+
@command = resolve_command(args.shift)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def run
|
|
24
|
+
case @command
|
|
25
|
+
when "new" then Commands::New.run(@args)
|
|
26
|
+
when "generate" then Commands::Generate.run(@args)
|
|
27
|
+
when "server" then Commands::Server.run(@args)
|
|
28
|
+
when "console" then Commands::Console.run(@args)
|
|
29
|
+
when "routes" then Commands::Routes.run(@args)
|
|
30
|
+
when "version" then Commands::Version.run(@args)
|
|
31
|
+
when "help", nil then Commands::Help.run(@args)
|
|
32
|
+
else
|
|
33
|
+
puts "Unknown command: #{@command}"
|
|
34
|
+
Commands::Help.run([])
|
|
35
|
+
exit 1
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def resolve_command(cmd)
|
|
42
|
+
return nil if cmd.nil?
|
|
43
|
+
ALIASES[cmd] || cmd
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarsier
|
|
4
|
+
# Base controller class with filters, parameter validation, and rendering
|
|
5
|
+
class Controller
|
|
6
|
+
class << self
|
|
7
|
+
# Define parameter schema for actions
|
|
8
|
+
# @yield [ParamSchema] schema builder
|
|
9
|
+
def params(&block)
|
|
10
|
+
@param_schema ||= ParamSchema.new
|
|
11
|
+
@param_schema.instance_eval(&block) if block_given?
|
|
12
|
+
@param_schema
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Get parameter schema
|
|
16
|
+
# @return [ParamSchema, nil]
|
|
17
|
+
def param_schema
|
|
18
|
+
@param_schema
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Define a before filter
|
|
22
|
+
# @param method_name [Symbol] filter method name
|
|
23
|
+
# @param only [Array<Symbol>] actions to apply to
|
|
24
|
+
# @param except [Array<Symbol>] actions to exclude
|
|
25
|
+
def before_action(method_name, only: nil, except: nil)
|
|
26
|
+
filters[:before] << { method: method_name, only: only, except: except }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Define an after filter
|
|
30
|
+
# @param method_name [Symbol] filter method name
|
|
31
|
+
# @param only [Array<Symbol>] actions to apply to
|
|
32
|
+
# @param except [Array<Symbol>] actions to exclude
|
|
33
|
+
def after_action(method_name, only: nil, except: nil)
|
|
34
|
+
filters[:after] << { method: method_name, only: only, except: except }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Define an around filter
|
|
38
|
+
# @param method_name [Symbol] filter method name
|
|
39
|
+
# @param only [Array<Symbol>] actions to apply to
|
|
40
|
+
# @param except [Array<Symbol>] actions to exclude
|
|
41
|
+
def around_action(method_name, only: nil, except: nil)
|
|
42
|
+
filters[:around] << { method: method_name, only: only, except: except }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Get all filters
|
|
46
|
+
# @return [Hash]
|
|
47
|
+
def filters
|
|
48
|
+
@filters ||= { before: [], after: [], around: [] }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Rescue from specific exceptions
|
|
52
|
+
# @param exception_class [Class] exception class to rescue
|
|
53
|
+
# @param with [Symbol] handler method name
|
|
54
|
+
def rescue_from(exception_class, with:)
|
|
55
|
+
rescue_handlers[exception_class] = with
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Get rescue handlers
|
|
59
|
+
# @return [Hash]
|
|
60
|
+
def rescue_handlers
|
|
61
|
+
@rescue_handlers ||= {}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Skip a before filter
|
|
65
|
+
# @param method_name [Symbol] filter method name
|
|
66
|
+
# @param only [Array<Symbol>] actions to skip for
|
|
67
|
+
# @param except [Array<Symbol>] actions to not skip for
|
|
68
|
+
def skip_before_action(method_name, only: nil, except: nil)
|
|
69
|
+
skipped_filters[:before] << { method: method_name, only: only, except: except }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Get skipped filters
|
|
73
|
+
# @return [Hash]
|
|
74
|
+
def skipped_filters
|
|
75
|
+
@skipped_filters ||= { before: [], after: [], around: [] }
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
attr_reader :request, :response, :params, :action_name
|
|
80
|
+
|
|
81
|
+
# @param request [Request] the request object
|
|
82
|
+
# @param response [Response] the response object
|
|
83
|
+
# @param action [Symbol] the action to execute
|
|
84
|
+
def initialize(request, response, action)
|
|
85
|
+
@request = request
|
|
86
|
+
@response = response
|
|
87
|
+
@action_name = action.to_sym
|
|
88
|
+
@params = request.params
|
|
89
|
+
@halted = false
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Execute the controller action with filters
|
|
93
|
+
# @return [Response]
|
|
94
|
+
def dispatch
|
|
95
|
+
run_filters(:before)
|
|
96
|
+
return @response if halted?
|
|
97
|
+
|
|
98
|
+
run_around_filters do
|
|
99
|
+
validate_params! if self.class.param_schema
|
|
100
|
+
send(@action_name)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
run_filters(:after) unless halted?
|
|
104
|
+
@response
|
|
105
|
+
rescue StandardError => e
|
|
106
|
+
handle_exception(e)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Get validated parameters
|
|
110
|
+
# @return [Hash]
|
|
111
|
+
def validated_params
|
|
112
|
+
@validated_params ||= {}
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Render a response
|
|
116
|
+
# @param options [Hash] render options
|
|
117
|
+
def render(json: nil, html: nil, text: nil, status: 200, **options)
|
|
118
|
+
@response.status = status
|
|
119
|
+
|
|
120
|
+
if json
|
|
121
|
+
@response.json(json, status: status)
|
|
122
|
+
elsif html
|
|
123
|
+
@response.html(html, status: status)
|
|
124
|
+
elsif text
|
|
125
|
+
@response.text(text, status: status)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Redirect to another URL
|
|
130
|
+
# @param url [String] redirect URL
|
|
131
|
+
# @param status [Integer] HTTP status code
|
|
132
|
+
def redirect_to(url, status: 302)
|
|
133
|
+
@response.redirect(url, status: status)
|
|
134
|
+
halt!
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Send a file
|
|
138
|
+
# @param path [String] file path
|
|
139
|
+
# @param options [Hash] options
|
|
140
|
+
def send_file(path, content_type: nil, filename: nil, disposition: "attachment")
|
|
141
|
+
raise Errno::ENOENT, path unless File.exist?(path)
|
|
142
|
+
|
|
143
|
+
content_type ||= guess_content_type(path)
|
|
144
|
+
filename ||= File.basename(path)
|
|
145
|
+
|
|
146
|
+
@response.set_header("Content-Type", content_type)
|
|
147
|
+
@response.set_header("Content-Disposition", "#{disposition}; filename=\"#{filename}\"")
|
|
148
|
+
@response.body = File.read(path)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Stream response
|
|
152
|
+
# @yield [StreamWriter] stream writer
|
|
153
|
+
def stream(&block)
|
|
154
|
+
@response.stream(&block)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Head-only response
|
|
158
|
+
# @param status [Integer] HTTP status code
|
|
159
|
+
# @param headers [Hash] additional headers
|
|
160
|
+
def head(status, **headers)
|
|
161
|
+
@response.status = status
|
|
162
|
+
headers.each { |k, v| @response.set_header(k.to_s, v) }
|
|
163
|
+
@response.body = ""
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Halt request processing
|
|
167
|
+
def halt!
|
|
168
|
+
@halted = true
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Check if processing is halted
|
|
172
|
+
# @return [Boolean]
|
|
173
|
+
def halted?
|
|
174
|
+
@halted
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Access session (if middleware is loaded)
|
|
178
|
+
# @return [Hash]
|
|
179
|
+
def session
|
|
180
|
+
@request.env["tarsier.session"] ||= {}
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Access flash messages
|
|
184
|
+
# @return [Hash]
|
|
185
|
+
def flash
|
|
186
|
+
session[:flash] ||= {}
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
protected
|
|
190
|
+
|
|
191
|
+
# Override in subclasses for custom authentication
|
|
192
|
+
def authenticate
|
|
193
|
+
# Default: no-op
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Override in subclasses for custom authorization
|
|
197
|
+
def authorize
|
|
198
|
+
# Default: no-op
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
private
|
|
202
|
+
|
|
203
|
+
def run_filters(type)
|
|
204
|
+
applicable_filters(type).each do |filter|
|
|
205
|
+
break if halted?
|
|
206
|
+
send(filter[:method])
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def run_around_filters(&block)
|
|
211
|
+
around_filters = applicable_filters(:around)
|
|
212
|
+
|
|
213
|
+
if around_filters.empty?
|
|
214
|
+
yield
|
|
215
|
+
else
|
|
216
|
+
chain = around_filters.reverse.reduce(block) do |next_filter, filter|
|
|
217
|
+
-> { send(filter[:method]) { next_filter.call } }
|
|
218
|
+
end
|
|
219
|
+
chain.call
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def applicable_filters(type)
|
|
224
|
+
all_filters = self.class.filters[type]
|
|
225
|
+
skipped = self.class.skipped_filters[type]
|
|
226
|
+
|
|
227
|
+
all_filters.select do |filter|
|
|
228
|
+
applies_to_action?(filter) && !skipped_for_action?(filter, skipped)
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def applies_to_action?(filter)
|
|
233
|
+
return false if filter[:only] && !filter[:only].include?(@action_name)
|
|
234
|
+
return false if filter[:except] && filter[:except].include?(@action_name)
|
|
235
|
+
true
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def skipped_for_action?(filter, skipped)
|
|
239
|
+
skipped.any? do |skip|
|
|
240
|
+
skip[:method] == filter[:method] && applies_to_action?(skip)
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def validate_params!
|
|
245
|
+
@validated_params = self.class.param_schema.validate(@params)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def handle_exception(exception)
|
|
249
|
+
handler = find_rescue_handler(exception.class)
|
|
250
|
+
|
|
251
|
+
if handler
|
|
252
|
+
send(handler, exception)
|
|
253
|
+
@response
|
|
254
|
+
else
|
|
255
|
+
raise exception
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def find_rescue_handler(exception_class)
|
|
260
|
+
self.class.rescue_handlers.each do |klass, handler|
|
|
261
|
+
return handler if exception_class <= klass
|
|
262
|
+
end
|
|
263
|
+
nil
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def guess_content_type(path)
|
|
267
|
+
ext = File.extname(path).downcase
|
|
268
|
+
{
|
|
269
|
+
".html" => "text/html",
|
|
270
|
+
".css" => "text/css",
|
|
271
|
+
".js" => "application/javascript",
|
|
272
|
+
".json" => "application/json",
|
|
273
|
+
".xml" => "application/xml",
|
|
274
|
+
".png" => "image/png",
|
|
275
|
+
".jpg" => "image/jpeg",
|
|
276
|
+
".gif" => "image/gif",
|
|
277
|
+
".svg" => "image/svg+xml",
|
|
278
|
+
".pdf" => "application/pdf"
|
|
279
|
+
}[ext] || "application/octet-stream"
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|