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
@@ -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