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,269 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
|
|
5
|
+
module Tarsier
|
|
6
|
+
# Parameter object with validation and coercion support
|
|
7
|
+
# Provides type-safe access to request parameters
|
|
8
|
+
class Params
|
|
9
|
+
# Boolean type marker (Ruby doesn't have a Boolean class)
|
|
10
|
+
Boolean = Object.new.freeze
|
|
11
|
+
|
|
12
|
+
# Type coercion mappings
|
|
13
|
+
COERCIONS = {
|
|
14
|
+
String => ->(v) { v.to_s },
|
|
15
|
+
Integer => ->(v) { Integer(v) },
|
|
16
|
+
Float => ->(v) { Float(v) },
|
|
17
|
+
Boolean => ->(v) { !["false", "0", "", "no", "off"].include?(v.to_s.downcase) },
|
|
18
|
+
Array => ->(v) { Array(v) },
|
|
19
|
+
Hash => ->(v) { v.is_a?(Hash) ? v : {} },
|
|
20
|
+
Date => ->(v) { Date.parse(v.to_s) },
|
|
21
|
+
Time => ->(v) { Time.parse(v.to_s) },
|
|
22
|
+
DateTime => ->(v) { DateTime.parse(v.to_s) }
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
attr_reader :raw
|
|
26
|
+
|
|
27
|
+
# @param params [Hash] raw parameters
|
|
28
|
+
def initialize(params = {})
|
|
29
|
+
@raw = params.transform_keys(&:to_sym).freeze
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Get a parameter value
|
|
33
|
+
# @param key [Symbol, String] parameter key
|
|
34
|
+
# @return [Object, nil]
|
|
35
|
+
def [](key)
|
|
36
|
+
@raw[key.to_sym]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Check if parameter exists
|
|
40
|
+
# @param key [Symbol, String] parameter key
|
|
41
|
+
# @return [Boolean]
|
|
42
|
+
def key?(key)
|
|
43
|
+
@raw.key?(key.to_sym)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
alias has_key? key?
|
|
47
|
+
|
|
48
|
+
# Get parameter with default
|
|
49
|
+
# @param key [Symbol, String] parameter key
|
|
50
|
+
# @param default [Object] default value
|
|
51
|
+
# @return [Object]
|
|
52
|
+
def fetch(key, default = nil, &block)
|
|
53
|
+
@raw.fetch(key.to_sym, default, &block)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Get required parameter (raises if missing)
|
|
57
|
+
# @param key [Symbol, String] parameter key
|
|
58
|
+
# @param type [Class, nil] expected type for coercion
|
|
59
|
+
# @return [Object]
|
|
60
|
+
# @raise [MissingParameterError] if parameter is missing
|
|
61
|
+
def require(key, type: nil)
|
|
62
|
+
key = key.to_sym
|
|
63
|
+
raise MissingParameterError, key unless key?(key)
|
|
64
|
+
|
|
65
|
+
value = @raw[key]
|
|
66
|
+
type ? coerce(key, value, type) : value
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Get optional parameter with type coercion
|
|
70
|
+
# @param key [Symbol, String] parameter key
|
|
71
|
+
# @param type [Class, nil] expected type for coercion
|
|
72
|
+
# @param default [Object] default value if missing
|
|
73
|
+
# @return [Object, nil]
|
|
74
|
+
def optional(key, type: nil, default: nil)
|
|
75
|
+
key = key.to_sym
|
|
76
|
+
return default unless key?(key) && !@raw[key].nil?
|
|
77
|
+
|
|
78
|
+
value = @raw[key]
|
|
79
|
+
type ? coerce(key, value, type) : value
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Permit only specified keys
|
|
83
|
+
# @param keys [Array<Symbol>] allowed keys
|
|
84
|
+
# @return [Hash]
|
|
85
|
+
def permit(*keys)
|
|
86
|
+
keys = keys.flatten.map(&:to_sym)
|
|
87
|
+
@raw.slice(*keys)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Slice parameters
|
|
91
|
+
# @param keys [Array<Symbol>] keys to include
|
|
92
|
+
# @return [Hash]
|
|
93
|
+
def slice(*keys)
|
|
94
|
+
permit(*keys)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Except parameters
|
|
98
|
+
# @param keys [Array<Symbol>] keys to exclude
|
|
99
|
+
# @return [Hash]
|
|
100
|
+
def except(*keys)
|
|
101
|
+
keys = keys.flatten.map(&:to_sym)
|
|
102
|
+
@raw.reject { |k, _| keys.include?(k) }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Merge with another hash
|
|
106
|
+
# @param other [Hash] hash to merge
|
|
107
|
+
# @return [Params]
|
|
108
|
+
def merge(other)
|
|
109
|
+
Params.new(@raw.merge(other.transform_keys(&:to_sym)))
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Convert to hash
|
|
113
|
+
# @return [Hash]
|
|
114
|
+
def to_h
|
|
115
|
+
@raw.dup
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
alias to_hash to_h
|
|
119
|
+
|
|
120
|
+
# Iterate over parameters
|
|
121
|
+
def each(&block)
|
|
122
|
+
@raw.each(&block)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Check if empty
|
|
126
|
+
# @return [Boolean]
|
|
127
|
+
def empty?
|
|
128
|
+
@raw.empty?
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Parameter count
|
|
132
|
+
# @return [Integer]
|
|
133
|
+
def size
|
|
134
|
+
@raw.size
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
alias length size
|
|
138
|
+
|
|
139
|
+
# String representation
|
|
140
|
+
# @return [String]
|
|
141
|
+
def inspect
|
|
142
|
+
"#<Tarsier::Params #{@raw.inspect}>"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
def coerce(key, value, type)
|
|
148
|
+
# Boolean is a special marker object, not a real class
|
|
149
|
+
return value if type != Boolean && value.is_a?(type)
|
|
150
|
+
|
|
151
|
+
coercer = COERCIONS[type]
|
|
152
|
+
raise TypeCoercionError.new(key, type, value) unless coercer
|
|
153
|
+
|
|
154
|
+
coercer.call(value)
|
|
155
|
+
rescue ArgumentError, TypeError => e
|
|
156
|
+
raise TypeCoercionError.new(key, type, value)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Parameter schema for validation
|
|
161
|
+
class ParamSchema
|
|
162
|
+
attr_reader :rules
|
|
163
|
+
|
|
164
|
+
def initialize
|
|
165
|
+
@rules = {}
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Define a required parameter
|
|
169
|
+
# @param name [Symbol] parameter name
|
|
170
|
+
# @param type [Class] expected type
|
|
171
|
+
# @param options [Hash] validation options
|
|
172
|
+
def requires(name, type:, **options)
|
|
173
|
+
@rules[name.to_sym] = { required: true, type: type, **options }
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Define an optional parameter
|
|
177
|
+
# @param name [Symbol] parameter name
|
|
178
|
+
# @param type [Class] expected type
|
|
179
|
+
# @param options [Hash] validation options
|
|
180
|
+
def optional(name, type:, default: nil, **options)
|
|
181
|
+
@rules[name.to_sym] = { required: false, type: type, default: default, **options }
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Validate parameters against schema
|
|
185
|
+
# @param params [Params, Hash] parameters to validate
|
|
186
|
+
# @return [Hash] validated and coerced parameters
|
|
187
|
+
# @raise [ValidationError] if validation fails
|
|
188
|
+
def validate(params)
|
|
189
|
+
params = params.is_a?(Params) ? params : Params.new(params)
|
|
190
|
+
errors = {}
|
|
191
|
+
result = {}
|
|
192
|
+
|
|
193
|
+
@rules.each do |name, rule|
|
|
194
|
+
value = params[name]
|
|
195
|
+
|
|
196
|
+
if value.nil? || value == ""
|
|
197
|
+
if rule[:required]
|
|
198
|
+
errors[name] = ["is required"]
|
|
199
|
+
next
|
|
200
|
+
else
|
|
201
|
+
result[name] = rule[:default]
|
|
202
|
+
next
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Type coercion
|
|
207
|
+
begin
|
|
208
|
+
value = coerce_value(value, rule[:type])
|
|
209
|
+
rescue StandardError
|
|
210
|
+
errors[name] = ["must be a #{rule[:type]}"]
|
|
211
|
+
next
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Validations
|
|
215
|
+
validation_errors = validate_value(name, value, rule)
|
|
216
|
+
if validation_errors.any?
|
|
217
|
+
errors[name] = validation_errors
|
|
218
|
+
else
|
|
219
|
+
result[name] = value
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
raise ValidationError, errors if errors.any?
|
|
224
|
+
|
|
225
|
+
result
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
private
|
|
229
|
+
|
|
230
|
+
def coerce_value(value, type)
|
|
231
|
+
coercer = Params::COERCIONS[type]
|
|
232
|
+
coercer ? coercer.call(value) : value
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def validate_value(name, value, rule)
|
|
236
|
+
errors = []
|
|
237
|
+
|
|
238
|
+
# Length validations
|
|
239
|
+
if rule[:min] && value.respond_to?(:length) && value.length < rule[:min]
|
|
240
|
+
errors << "must be at least #{rule[:min]} characters"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
if rule[:max] && value.respond_to?(:length) && value.length > rule[:max]
|
|
244
|
+
errors << "must be at most #{rule[:max]} characters"
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Numeric validations
|
|
248
|
+
if rule[:greater_than] && value.respond_to?(:<) && value <= rule[:greater_than]
|
|
249
|
+
errors << "must be greater than #{rule[:greater_than]}"
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
if rule[:less_than] && value.respond_to?(:>) && value >= rule[:less_than]
|
|
253
|
+
errors << "must be less than #{rule[:less_than]}"
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Format validation
|
|
257
|
+
if rule[:format] && value.respond_to?(:match?) && !value.match?(rule[:format])
|
|
258
|
+
errors << "has invalid format"
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Inclusion validation
|
|
262
|
+
if rule[:in] && !rule[:in].include?(value)
|
|
263
|
+
errors << "must be one of: #{rule[:in].join(', ')}"
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
errors
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|