kweerie 0.1.5 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b47a2f9363e6fb03e19af88b8d34ab9d727e45e2ed7f05e95d41d23835f93693
4
- data.tar.gz: cb868fa5858008ce6e2ec14188e81c41fb0ad554e85e491b71ebe625d357257e
3
+ metadata.gz: 8ee037d0d27c788d915bb3a17c5cdc9fb4dc00dd172ae8f32023df00fd70515d
4
+ data.tar.gz: '0950c053e7d14f261923f74c5987fa177fa6ffb1f9b2e8213e132f411722d877'
5
5
  SHA512:
6
- metadata.gz: 3f9927adf42ec5fed6e65ea6c5a83a3166ea10ccadbe9d802a2d2bb6cc02647365df44865052566a6b255fa8dbb91be74f406a0fc61993982589222b2724724a
7
- data.tar.gz: '08acf2f917e887a25e0c22aebd493aa861c1bc0fa827e80983ee577f352d1048efabb2285b152cb1d141ccf8846e34071828f4b77960d0b1b51909fb279fcd85'
6
+ metadata.gz: 75e8dee13094014cefe169804ee050424fe0a946b8055d7f92423a819d578c375817bc580ae1b57b658d513786efc3fa731d3dad3fde3b022ec95cafa2701264
7
+ data.tar.gz: 8c7066bd2b2d1b2812889c116518d87be3b4868e57443fd54bc67b6980772999c4db8b853e98453331d364caf289ec8005c5ebf3d619ab2330d2b8dab781f7a0
data/.rubocop.yml CHANGED
@@ -41,3 +41,6 @@ Gemspec/RequiredRubyVersion:
41
41
 
42
42
  Metrics/BlockLength:
43
43
  Enabled: false
44
+
45
+ Naming/AccessorMethodName:
46
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,4 +1,14 @@
1
- ## [Unreleased]
1
+ ## [0.3.0] - 2024-11-21
2
+
3
+ - changed sql_paths to default_path.
4
+ - fixed generate to handle defined path
5
+
6
+ ## [0.2.0] - 2024-11-12
7
+
8
+ - changed BaseObjects to BaseObject.
9
+ - remove automatic type inference.
10
+ - added cast_select.
11
+ - added tiny amound of custom types.
2
12
 
3
13
  ## [0.1.0] - 2024-11-06
4
14
 
data/README.md CHANGED
@@ -55,10 +55,10 @@ results = UserSearch.with(
55
55
 
56
56
  ### Object Mapping
57
57
 
58
- While `Kweerie::Base` returns plain hashes, you can use `Kweerie::BaseObjects` to get typed Ruby objects with proper attribute methods:
58
+ While `Kweerie::Base` returns plain hashes, you can use `Kweerie::BaseObject` to get typed Ruby objects with proper attribute methods:
59
59
 
60
60
  ```ruby
61
- class UserSearch < Kweerie::BaseObjects
61
+ class UserSearch < Kweerie::BaseObject
62
62
  bind :name, as: '$1'
63
63
  bind :created_at, as: '$2'
64
64
  end
@@ -148,7 +148,7 @@ UsersByStatus.all # ✗ Raises ArgumentError
148
148
  UsersByStatus.with(status: 'active') # ✓ Correct usage
149
149
  ```
150
150
 
151
- Both methods work with `Kweerie::Base` and `Kweerie::BaseObjects`, returning arrays of hashes or objects respectively:
151
+ Both methods work with `Kweerie::Base` and `Kweerie::BaseObject`, returning arrays of hashes or objects respectively:
152
152
 
153
153
  ```ruby
154
154
  # Returns array of hashes
@@ -158,65 +158,15 @@ users = AllUsers.all
158
158
  # => [{"id" => 1, "name" => "Eclipsoid"}, ...]
159
159
 
160
160
  # Returns array of objects
161
- class AllUsers < Kweerie::BaseObjects
161
+ class AllUsers < Kweerie::BaseObject
162
162
  end
163
163
  users = AllUsers.all
164
164
  # => [#<AllUsers id=1 name="Eclipsoid">, ...]
165
165
  ```
166
166
 
167
- ### Automatic Type Casting
168
-
169
- BaseObjects automatically casts common PostgreSQL types to their Ruby equivalents:
170
-
171
- ```ruby
172
- # In your SQL file
173
- SELECT
174
- name,
175
- created_at, -- timestamp
176
- age::integer, -- integer
177
- score::float, -- float
178
- active::boolean, -- boolean
179
- metadata::jsonb, -- jsonb
180
- tags::jsonb -- jsonb array
181
- FROM users;
182
-
183
- # In your Ruby code
184
- user = UserSearch.with(name: 'Eclipsoid').first
185
-
186
- user.created_at # => Time object
187
- user.age # => Integer
188
- user.score # => Float
189
- user.active # => true/false
190
- user.metadata # => Hash with string keys
191
- user.tags # => Array
192
-
193
- # Nested JSONB data is properly accessible
194
- user.metadata["role"] # => "admin"
195
- user.metadata["preferences"]["theme"] # => "dark"
196
- ```
197
-
198
- ### Pattern Matching Support
199
-
200
- BaseObjects support Ruby's pattern matching syntax:
201
-
202
- ```ruby
203
- case user
204
- in { name:, metadata: { role: "admin" } }
205
- puts "Admin user: #{name}"
206
- in { name:, metadata: { role: "user" } }
207
- puts "Regular user: #{name}"
208
- end
209
-
210
- # Nested pattern matching
211
- case user
212
- in { metadata: { preferences: { theme: "dark" } } }
213
- puts "Dark theme user"
214
- end
215
- ```
216
-
217
167
  ### Object Interface
218
168
 
219
- BaseObjects provide several useful methods:
169
+ BaseObject provide several useful methods:
220
170
 
221
171
  ```ruby
222
172
  # Hash-like access
@@ -230,31 +180,6 @@ user.to_json # => JSON string
230
180
  # Comparison
231
181
  user1 == user2 # Compare all attributes
232
182
  users.sort_by(&:created_at) # Sortable
233
-
234
- # Change tracking
235
- user.changed? # => Check if any attributes changed
236
- user.changes # => Hash of changes with [old, new] values
237
- user.original_attributes # => Original attributes from DB
238
- ```
239
-
240
- ### PostgreSQL Array Support
241
-
242
- BaseObjects handles PostgreSQL arrays by converting them to Ruby arrays with proper type casting:
243
-
244
- ```ruby
245
- # In your PostgreSQL schema
246
- create_table :users do |t|
247
- t.integer :preferred_ordering, array: true, default: []
248
- t.string :tags, array: true
249
- t.float :scores, array: true
250
- end
251
-
252
- # In your query
253
- user = UserSearch.with(name: 'Eclipsoid').first
254
-
255
- user.preferred_ordering # => [1, 3, 2]
256
- user.tags # => ["ruby", "rails"]
257
- user.scores # => [98.5, 87.2, 92.0]
258
183
  ```
259
184
 
260
185
  ### SQL File Location
@@ -282,9 +207,90 @@ class UserSearch < Kweerie::Base
282
207
  end
283
208
  ```
284
209
 
210
+ ## Type Casting
211
+
212
+ `cast_select` provides flexible, explicit type casting for your query result fields. Instead of relying on automatic type inference, you can specify exactly how each field should be cast. By default it will return as the string from the database.
213
+
214
+ ### Basic Usage
215
+
216
+ ```ruby
217
+ class UserQuery < Kweerie::BaseObjects
218
+ cast_select :age, as: ->(val) { val.to_i }
219
+ cast_select :active, as: Types::Boolean
220
+ cast_select :metadata, as: Types::PgJsonb
221
+ end
222
+ ```
223
+
224
+ ### Casting Options
225
+
226
+ #### Built-in Type Classes
227
+ The gem includes a few built-in type classes for specific scenarios, you'll find a comprehensive amount of them in rails:
228
+
229
+ ```ruby
230
+ # Boolean casting (handles various boolean representations)
231
+ cast_select :active, as: Types::Boolean
232
+
233
+ # JSONB casting (handles both objects and arrays)
234
+ cast_select :metadata, as: Types::PgJsonb
235
+
236
+ # Postgres Array casting
237
+ cast_select :tags, as: Types::PgArray
238
+ ```
239
+
240
+ #### Lambda/Proc Casting
241
+ For simple transformations, you can use a lambda or proc:
242
+
243
+ ```ruby
244
+ class ProductQuery < Kweerie::BaseObjects
245
+ cast_select :price, as: ->(val) { val.to_f }
246
+ cast_select :quantity, as: ->(val) { val.to_i }
247
+ cast_select :sku, as: ->(val) { val.upcase }
248
+ end
249
+ ```
250
+
251
+ #### Method Reference Casting
252
+ You can reference an instance method for complex casting logic:
253
+
254
+ ```ruby
255
+ class OrderQuery < Kweerie::BaseObjects
256
+ cast_select :total, as: :calculate_total
257
+
258
+ def calculate_total(val)
259
+ return if val.nil?
260
+
261
+ Money.new(val)
262
+ end
263
+ end
264
+ ```
265
+
266
+ ### Custom Type Classes
267
+
268
+ For reusable, complex casting logic, you can create custom type classes, or use the ones provided by rails:
269
+
270
+ ```ruby
271
+ class Types::Money
272
+ def cast(value)
273
+ return nil if value.nil?
274
+
275
+ (value.to_f * 100).to_i # Store as cents
276
+ end
277
+ end
278
+
279
+ class Types::PGIntArray < Types::PgArray
280
+ def cast(value)
281
+ super.map(&:to_i) # Convert array elements to integers
282
+ end
283
+ end
284
+
285
+ class InvoiceQuery < Kweerie::BaseObjects
286
+ cast_select :amount, as: Types::Money
287
+ cast_select :line_items, as: Types::PGIntArray
288
+ end
289
+ ```
290
+
285
291
  ### Performance Considerations
286
292
 
287
- BaseObjects creates a unique class for each query result set, with the following optimizations:
293
+ BaseObject creates a unique class for each query result set, with the following optimizations:
288
294
 
289
295
  - Classes are cached and reused for subsequent queries
290
296
  - Attribute readers are defined upfront
@@ -318,16 +324,16 @@ Kweerie.configure do |config|
318
324
  # Use a custom connection provider
319
325
  config.connection_provider = -> { MyCustomConnectionPool.connection }
320
326
 
321
- # Configure where to look for SQL files
322
- config.sql_paths = -> { ['db/queries', 'app/sql'] }
327
+ # Configure where to look for SQL files. Generator also uses this path
328
+ config.default_path = 'app/sql'
323
329
  end
324
330
  ```
325
331
 
326
332
  ## Requirements
327
333
 
328
- - Ruby 2.7 or higher
334
+ - Ruby 3 or higher
329
335
  - PostgreSQL (this gem is PostgreSQL-specific and uses the `pg` gem)
330
- - Rails 6+ (optional, needed for the generator and default ActiveRecord integration)
336
+ - Rails 7+ (optional, needed for the generator and default ActiveRecord integration)
331
337
 
332
338
  ## Features
333
339
 
@@ -25,10 +25,10 @@ class KweerieGenerator < Rails::Generators::NamedBase
25
25
  FileUtils.mkdir_p("app/queries")
26
26
 
27
27
  # Create the Ruby file
28
- create_file "app/queries/#{file_name}.rb", template_content
28
+ create_file "#{default_path}/#{file_name}.rb", template_content
29
29
 
30
30
  # Create the SQL file
31
- create_file "app/queries/#{file_name}.sql", <<~SQL
31
+ create_file "#{default_path}/#{file_name}.sql", <<~SQL
32
32
  -- Write your SQL query here
33
33
  -- Available parameters: #{parameters.map { |p| "$#{parameters.index(p) + 1} (#{p})" }.join(", ")}
34
34
 
@@ -50,4 +50,8 @@ class KweerieGenerator < Rails::Generators::NamedBase
50
50
  def class_name
51
51
  name.classify
52
52
  end
53
+
54
+ def default_path
55
+ Kweerie.configuration.default_path
56
+ end
53
57
  end
data/lib/kweerie/base.rb CHANGED
@@ -100,40 +100,7 @@ module Kweerie
100
100
  end
101
101
 
102
102
  def sql_path
103
- @sql_path ||=
104
- if @sql_file_location&.key?(:root)
105
- raise ConfigurationError, "Root path requires Rails to be defined" unless defined?(Rails)
106
-
107
- path = Rails.root.join(@sql_file_location[:root]).to_s
108
- raise SQLFileNotFound, "Could not find SQL file at #{path}" unless File.exist?(path)
109
-
110
- path
111
-
112
- elsif @sql_file_location&.key?(:relative)
113
- configured_paths = Kweerie.configuration.sql_paths.call
114
- sql_file = configured_paths.map do |path|
115
- full_path = File.join(path, @sql_file_location[:relative])
116
- full_path if File.exist?(full_path)
117
- end.compact.first
118
- unless sql_file
119
- raise SQLFileNotFound,
120
- "Could not find SQL file #{@sql_file_location[:relative]}"
121
- end
122
-
123
- sql_file
124
- else # default behavior
125
- sql_filename = "#{name.underscore}.sql"
126
- configured_paths = Kweerie.configuration.sql_paths.call
127
-
128
- sql_file = configured_paths.map do |path|
129
- full_path = File.join(path, sql_filename)
130
- full_path if File.exist?(full_path)
131
- end.compact.first
132
-
133
- raise SQLFileNotFound, "SQL file not found for #{name}" unless sql_file
134
-
135
- sql_file
136
- end
103
+ @sql_path ||= SQLPathResolver.new(@sql_file_location, name).resolve
137
104
  end
138
105
 
139
106
  def sql_content
@@ -151,7 +118,7 @@ module Kweerie
151
118
  #
152
119
  # === Returns
153
120
  #
154
- # Array of hashes representing the query results. When using Kweerie::BaseObjects,
121
+ # Array of hashes representing the query results. When using Kweerie::BaseObject,
155
122
  # returns array of typed objects instead.
156
123
  #
157
124
  # === Examples
@@ -163,7 +130,7 @@ module Kweerie
163
130
  # )
164
131
  # # => [{"id"=>1, "name"=>"Eclipsoid", "email"=>"eclipsoid@example.com"}]
165
132
  #
166
- # # With type casting (BaseObjects)
133
+ # # With type casting (BaseObject)
167
134
  # UserSearch.with(created_after: '2024-01-01')
168
135
  # # => [#<UserSearch id=1 created_at=2024-01-01 00:00:00 +0000>]
169
136
  #
@@ -205,15 +172,6 @@ module Kweerie
205
172
  ordered_params = bindings.transform_values { |position| params[bindings.key(position)] }
206
173
  ordered_params.values
207
174
  end
208
-
209
- def root_path
210
- defined?(Rails) ? Rails.root : Dir.pwd
211
- end
212
-
213
- def using_activerecord?
214
- defined?(ActiveRecord::Base) &&
215
- Kweerie.configuration.connection_provider == Kweerie::Configuration.new.connection_provider
216
- end
217
175
  end
218
176
  end
219
177
  end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "object_methods/accessors"
4
+ require_relative "object_methods/comparison"
5
+ require_relative "object_methods/key_transformation"
6
+ require_relative "object_methods/serialization"
7
+ require_relative "object_methods/type_casting"
8
+
9
+ module Kweerie
10
+ class BaseObject < Base
11
+ class << self
12
+ def with(params = {})
13
+ results = super
14
+ return [] if results.empty?
15
+
16
+ # Create a unique result class for this query
17
+ result_class = generate(results.first.keys)
18
+ # Map results to objects
19
+ results.map { |row| result_class.new(row) }
20
+ end
21
+
22
+ def cast_select(field, as: nil)
23
+ cast_definitions[field] = as if as
24
+ end
25
+
26
+ def cast_definitions
27
+ @cast_definitions ||= {}
28
+ end
29
+
30
+ def generate(attribute_names)
31
+ cast_definitions = self.cast_definitions
32
+ Class.new(self) do
33
+ include Comparable
34
+ include ObjectMethods::Accessors
35
+ include ObjectMethods::Comparison
36
+ include ObjectMethods::KeyTransformation
37
+ include ObjectMethods::Serialization
38
+ include ObjectMethods::TypeCasting
39
+
40
+ # Define attr_readers for all columns
41
+ attribute_names.each { |name| attr_reader name }
42
+
43
+ define_method :initialize do |attrs|
44
+ # Store both raw and casted versions
45
+ @_raw_original_attributes = attrs.dup
46
+ @_original_attributes = attrs.each_with_object({}) do |(key, value), hash|
47
+ type_definition = cast_definitions[key.to_sym]
48
+ casted_value = type_cast_value(value, type_definition)
49
+ hash[key.to_s] = casted_value
50
+ instance_variable_set("@#{key}", casted_value)
51
+ end
52
+
53
+ super() if defined?(super)
54
+ end
55
+
56
+ # Nice inspect output
57
+ define_method :inspect do
58
+ attrs = attribute_names.map do |name|
59
+ "#{name}=#{instance_variable_get("@#{name}").inspect}"
60
+ end.join(" ")
61
+ "#<#{self.class.superclass.name} #{attrs}>"
62
+ end
63
+
64
+ # Make attribute_names available to instance methods
65
+ define_method(:attribute_names) { attribute_names }
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Kweerie
4
4
  class Configuration
5
- attr_accessor :connection_provider, :sql_paths
5
+ attr_accessor :connection_provider, :sql_paths, :default_path
6
6
 
7
7
  def initialize
8
8
  # Default to using ActiveRecord's connection if available
@@ -20,6 +20,8 @@ module Kweerie
20
20
  paths.unshift("lib/queries") unless defined?(Rails)
21
21
  paths
22
22
  }
23
+
24
+ @default_path = defined?(Rails) ? "app/queries" : "lib/queries"
23
25
  end
24
26
  end
25
27
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ObjectMethods
4
+ module Accessors
5
+ def [](key)
6
+ instance_variable_get("@#{key}")
7
+ end
8
+
9
+ def fetch(key, default = nil)
10
+ instance_variable_defined?("@#{key}") ? instance_variable_get("@#{key}") : default
11
+ end
12
+
13
+ def original_attributes
14
+ @_original_attributes
15
+ end
16
+
17
+ def raw_original_attributes
18
+ @_raw_original_attributes
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ObjectMethods
4
+ module Comparison
5
+ def <=>(other)
6
+ return nil unless other.is_a?(self.class)
7
+
8
+ to_h <=> other.to_h
9
+ end
10
+
11
+ def ==(other)
12
+ return false unless other.is_a?(self.class)
13
+
14
+ to_h == other.to_h
15
+ end
16
+
17
+ def eql?(other)
18
+ self == other
19
+ end
20
+
21
+ def hash
22
+ to_h.hash
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ObjectMethods
4
+ module KeyTransformation
5
+ def deep_stringify_keys(obj)
6
+ case obj
7
+ when Hash
8
+ obj.transform_keys(&:to_s)
9
+ .transform_values { |v| deep_stringify_keys(v) }
10
+ when Array
11
+ obj.map { |item| item.is_a?(Hash) ? deep_stringify_keys(item) : item }
12
+ else
13
+ obj
14
+ end
15
+ end
16
+
17
+ def deep_symbolize_keys(obj)
18
+ case obj
19
+ when Hash
20
+ obj.transform_keys(&:to_sym)
21
+ .transform_values { |v| deep_symbolize_keys(v) }
22
+ when Array
23
+ obj.map { |item| item.is_a?(Hash) ? deep_symbolize_keys(item) : item }
24
+ else
25
+ obj
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ObjectMethods
4
+ module Serialization
5
+ def to_h
6
+ attribute_names.each_with_object({}) do |name, hash|
7
+ value = instance_variable_get("@#{name}")
8
+ hash[name.to_s] = serialize_value(value)
9
+ end
10
+ end
11
+
12
+ def to_json(*args)
13
+ to_h.to_json(*args)
14
+ end
15
+
16
+ def deconstruct_keys(keys)
17
+ symbolized = deep_symbolize_keys(to_h)
18
+ keys ? symbolized.slice(*keys) : symbolized
19
+ end
20
+
21
+ private
22
+
23
+ def serialize_value(value)
24
+ value.is_a?(Hash) ? deep_stringify_keys(value) : value
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ObjectMethods
4
+ module TypeCasting
5
+ def type_cast_value(value, type_definition)
6
+ return value if type_definition.nil?
7
+
8
+ case type_definition
9
+ when Symbol
10
+ public_send(type_definition, value)
11
+ when Proc
12
+ type_definition.call(value)
13
+ when Class
14
+ type_definition.new.cast(value)
15
+ else
16
+ raise ArgumentError, "Unsupported type definition: #{type_definition}"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SQLPathResolver
4
+ class ConfigurationError < StandardError; end
5
+ class SQLFileNotFound < StandardError; end
6
+
7
+ def initialize(sql_file_location, name = nil)
8
+ @sql_file_location = sql_file_location
9
+ @name = name
10
+ end
11
+
12
+ def resolve
13
+ return resolve_root_path if root_path?
14
+ return resolve_relative_path if relative_path?
15
+
16
+ resolve_default_path
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :sql_file_location, :name
22
+
23
+ def root_path?
24
+ sql_file_location&.key?(:root)
25
+ end
26
+
27
+ def relative_path?
28
+ sql_file_location&.key?(:relative)
29
+ end
30
+
31
+ def resolve_root_path
32
+ raise ConfigurationError, "Root path requires Rails to be defined" unless defined?(Rails)
33
+
34
+ path = Rails.root.join(sql_file_location[:root]).to_s
35
+ validate_file_exists!(path)
36
+ path
37
+ end
38
+
39
+ def resolve_relative_path
40
+ relative_file = sql_file_location[:relative]
41
+ find_in_configured_paths(relative_file) or
42
+ raise SQLFileNotFound, "Could not find SQL file #{relative_file}"
43
+ end
44
+
45
+ def resolve_default_path
46
+ raise ArgumentError, "Name must be provided for default path resolution" unless name
47
+
48
+ sql_filename = "#{name.underscore}.sql"
49
+ find_in_configured_paths(sql_filename) or
50
+ raise SQLFileNotFound, "SQL file not found for #{name}"
51
+ end
52
+
53
+ def find_in_configured_paths(filename)
54
+ full_path = File.join(path, filename)
55
+
56
+ full_path if File.exist?(full_path)
57
+ end
58
+
59
+ def path
60
+ Kweerie.configuration.default_path
61
+ end
62
+
63
+ def validate_file_exists!(path)
64
+ raise SQLFileNotFound, "Could not find SQL file at #{path}" unless File.exist?(path)
65
+ end
66
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Types
4
+ class Boolean
5
+ TRUTHY = [true, 1, "1", "t", "T", "true", "TRUE"].freeze
6
+ FALSEY = [false, 0, "0", "f", "F", "false", "FALSE"].freeze
7
+
8
+ def cast(value)
9
+ return nil if value.nil?
10
+ return value if value.is_a?(Boolean)
11
+
12
+ if TRUTHY.include?(value)
13
+ true
14
+ elsif FALSEY.include?(value)
15
+ false
16
+ else
17
+ raise ArgumentError, "Invalid boolean value: #{value}"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Types
4
+ class PgArray
5
+ def cast(value)
6
+ clean_value = value.gsub(/^{|}$/, "")
7
+ return [] if clean_value.empty?
8
+
9
+ elements = clean_value.split(/,(?=(?:[^"]*"[^"]*")*[^"]*$)/)
10
+ elements.map { |element| cast_array_element(element) }
11
+ end
12
+
13
+ def cast_array_element(element)
14
+ case element
15
+ when /^\d+$/ then element.to_i
16
+ when /^\d*\.\d+$/ then element.to_f
17
+ when /^(true|false)$/i then element.downcase == "true"
18
+ when /^"(.*)"$/ then ::Regexp.last_match(1)
19
+ else element
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Types
4
+ class PgJsonb
5
+ def cast(value)
6
+ return {} if value.nil?
7
+ return value if value.is_a?(Hash)
8
+
9
+ JSON.parse(value.to_s)
10
+ rescue JSON::ParserError
11
+ raise ArgumentError, "Invalid JSON value: #{value}"
12
+ end
13
+ end
14
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kweerie
4
- VERSION = "0.1.5"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/kweerie.rb CHANGED
@@ -26,4 +26,8 @@ end
26
26
 
27
27
  require_relative "kweerie/configuration"
28
28
  require_relative "kweerie/base"
29
- require_relative "kweerie/base_objects"
29
+ require_relative "kweerie/base_object"
30
+ require_relative "kweerie/sql_path_resolver"
31
+ require_relative "kweerie/types/boolean"
32
+ require_relative "kweerie/types/pg_array"
33
+ require_relative "kweerie/types/pg_jsonb"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kweerie
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Toby
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-11-08 00:00:00.000000000 Z
11
+ date: 2024-11-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -68,8 +68,17 @@ files:
68
68
  - lib/generators/kweerie/kweerie_generator.rb
69
69
  - lib/kweerie.rb
70
70
  - lib/kweerie/base.rb
71
- - lib/kweerie/base_objects.rb
71
+ - lib/kweerie/base_object.rb
72
72
  - lib/kweerie/configuration.rb
73
+ - lib/kweerie/object_methods/accessors.rb
74
+ - lib/kweerie/object_methods/comparison.rb
75
+ - lib/kweerie/object_methods/key_transformation.rb
76
+ - lib/kweerie/object_methods/serialization.rb
77
+ - lib/kweerie/object_methods/type_casting.rb
78
+ - lib/kweerie/sql_path_resolver.rb
79
+ - lib/kweerie/types/boolean.rb
80
+ - lib/kweerie/types/pg_array.rb
81
+ - lib/kweerie/types/pg_jsonb.rb
73
82
  - lib/kweerie/version.rb
74
83
  - sig/kweerie.rbs
75
84
  homepage: https://github.com/tobyond/kweerie
@@ -1,210 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "active_support/core_ext/string"
4
- require "json"
5
-
6
- module Kweerie
7
- class BaseObjects < Base
8
- class << self
9
- def with(params = {})
10
- results = super
11
- return [] if results.empty?
12
-
13
- # Create a unique result class for this query
14
- result_class = generate_result_class(results.first.keys)
15
-
16
- # Map results to objects
17
- results.map { |row| result_class.new(row) }
18
- end
19
-
20
- private
21
-
22
- def generate_result_class(attribute_names)
23
- @generate_result_class ||= Class.new(self) do
24
- # Include comparison and serialization modules
25
- include Comparable
26
-
27
- # Define attr_readers for all columns
28
- attribute_names.each do |name|
29
- attr_reader name
30
- end
31
-
32
- define_method :initialize do |attrs|
33
- # Store both raw and casted versions
34
- @_raw_original_attributes = attrs.dup
35
- @_original_attributes = attrs.transform_keys(&:to_s).transform_values do |value|
36
- type_cast_value(value)
37
- end
38
-
39
- attrs.each do |name, value|
40
- casted_value = type_cast_value(value)
41
- instance_variable_set("@#{name}", casted_value)
42
- end
43
- super() if defined?(super)
44
- end
45
-
46
- define_method :type_cast_value do |value|
47
- case value
48
- when /^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?$/ # DateTime check
49
- Time.parse(value)
50
- when /^\d+$/ # Integer check
51
- value.to_i
52
- when /^\d*\.\d+$/ # Float check
53
- value.to_f
54
- when /^(true|false)$/i # Boolean check
55
- value.downcase == "true"
56
- when /^{.*}$/ # Could be PG array or JSON
57
- if value.start_with?("{") && value.end_with?("}") && !value.include?('"=>') && !value.include?(": ")
58
- # PostgreSQL array (simple heuristic: no "=>" or ":" suggests it's not JSON)
59
- parse_pg_array(value)
60
- else
61
- # Attempt JSON parse
62
- begin
63
- parsed = JSON.parse(value)
64
- deep_stringify_keys(parsed)
65
- rescue JSON::ParserError
66
- value
67
- end
68
- end
69
- when /^[\[{]/ # Pure JSON (arrays starting with [ or other JSON objects)
70
- begin
71
- parsed = JSON.parse(value)
72
- deep_stringify_keys(parsed)
73
- rescue JSON::ParserError
74
- value
75
- end
76
- else
77
- value
78
- end
79
- end
80
-
81
- define_method :parse_pg_array do |value|
82
- # Remove the curly braces
83
- clean_value = value.gsub(/^{|}$/, "")
84
- return [] if clean_value.empty?
85
-
86
- # Split on comma, but not within quoted strings
87
- elements = clean_value.split(/,(?=(?:[^"]*"[^"]*")*[^"]*$)/)
88
-
89
- elements.map do |element|
90
- case element
91
- when /^\d+$/ # Integer
92
- element.to_i
93
- when /^\d*\.\d+$/ # Float
94
- element.to_f
95
- when /^(true|false)$/i # Boolean
96
- element.downcase == "true"
97
- when /^"(.*)"$/ # Quoted string
98
- ::Regexp.last_match(1)
99
- else
100
- element
101
- end
102
- end
103
- end
104
-
105
- define_method :deep_symbolize_keys do |obj|
106
- case obj
107
- when Hash
108
- obj.transform_keys(&:to_sym).transform_values { |v| deep_symbolize_keys(v) }
109
- when Array
110
- obj.map { |item| item.is_a?(Hash) ? deep_symbolize_keys(item) : item }
111
- else
112
- obj
113
- end
114
- end
115
-
116
- # Nice inspect output
117
- define_method :inspect do
118
- attrs = attribute_names.map do |name|
119
- "#{name}=#{instance_variable_get("@#{name}").inspect}"
120
- end.join(" ")
121
- "#<#{self.class.superclass.name} #{attrs}>"
122
- end
123
-
124
- # Hash-like access
125
- define_method :[] do |key|
126
- instance_variable_get("@#{key}")
127
- end
128
-
129
- define_method :fetch do |key, default = nil|
130
- instance_variable_defined?("@#{key}") ? instance_variable_get("@#{key}") : default
131
- end
132
-
133
- # Comparison methods
134
- define_method :<=> do |other|
135
- return nil unless other.is_a?(self.class)
136
-
137
- to_h <=> other.to_h
138
- end
139
-
140
- define_method :== do |other|
141
- return false unless other.is_a?(self.class)
142
-
143
- to_h == other.to_h
144
- end
145
-
146
- define_method :eql? do |other|
147
- self == other
148
- end
149
-
150
- define_method :hash do
151
- to_h.hash
152
- end
153
-
154
- # Add helper method for deep string keys
155
- define_method :deep_stringify_keys do |obj|
156
- case obj
157
- when Hash
158
- obj.transform_keys(&:to_s).transform_values { |v| deep_stringify_keys(v) }
159
- when Array
160
- obj.map { |item| item.is_a?(Hash) ? deep_stringify_keys(item) : item }
161
- else
162
- obj
163
- end
164
- end
165
-
166
- # Serialization
167
- define_method :to_h do
168
- attribute_names.each_with_object({}) do |name, hash|
169
- value = instance_variable_get("@#{name}")
170
- # Ensure string keys in output
171
- hash[name.to_s] = value.is_a?(Hash) ? deep_stringify_keys(value) : value
172
- end
173
- end
174
-
175
- define_method :to_json do |*args|
176
- to_h.to_json(*args)
177
- end
178
-
179
- # Pattern matching support (Ruby 2.7+)
180
- define_method :deconstruct_keys do |keys|
181
- symbolized = deep_symbolize_keys(to_h)
182
- keys ? symbolized.slice(*keys) : symbolized
183
- end
184
-
185
- # Original attributes access
186
- define_method :original_attributes do
187
- @_original_attributes
188
- end
189
-
190
- # Raw attributes access
191
- define_method :raw_original_attributes do
192
- @_raw_original_attributes
193
- end
194
-
195
- # ActiveModel-like changes tracking
196
- define_method :changed? do
197
- to_h != @_original_attributes
198
- end
199
-
200
- define_method :changes do
201
- to_h.each_with_object({}) do |(key, value), changes|
202
- original = @_original_attributes[key]
203
- changes[key] = [original, value] if original != value
204
- end
205
- end
206
- end
207
- end
208
- end
209
- end
210
- end