kweerie 0.1.0 → 0.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 28a36928eab4b34cff2a0fbdcf7e4614264f501d7e75f0b08ae6c28f151cd641
4
- data.tar.gz: 8f0332f34bf28dc46c33cb671bb03e95330d08a9e15eed735be6ca64964d10db
3
+ metadata.gz: f11c69ed8356612d3a1df5dfb98b4751061db706908f7b2efe6ace375f53c73e
4
+ data.tar.gz: b33644c5b64bb941bf88bf6207ced00706e84a1c83c2b60fa9c7dddc680dd0fe
5
5
  SHA512:
6
- metadata.gz: 026f1786bbb42976b0461951005f448e7b2169b6962a5f06737a9cc2fbdd99af93c84ea5d5ee277067788e349c6e73df98096154bebea79f6a2b9b5f450f0234
7
- data.tar.gz: d3e8e06dcaa23ea12f205e623861ba08cd16b2be877e9008571efbe38a6f6a536efb717ed45ca977102c9b2fa2cf5ebec737a31faf6e8e4accf4e93f51412395
6
+ metadata.gz: 9f05d4db4050c840ac87c60b60e44dd27413fc3fee685cd81fd5320d6bb9d37f01ee5a1f29691989f1ec4f3de2a566d1a8fd0057696b54982bdd012170a0ea2b
7
+ data.tar.gz: 11560ae4acff559585472f020d29d0367ca431cd3f9398cfb75aa1e27b6a08057e8f6b1d5270b7f6ef043551c9f15d3b95434d586400390c512c75b410dc7661
data/README.md CHANGED
@@ -53,6 +53,132 @@ results = UserSearch.with(
53
53
  # => [{"id"=>9981, "name"=>"John Doe", "email"=>"johndoe@example.com"}]
54
54
  ```
55
55
 
56
+ ### Object Mapping
57
+
58
+ While `Kweerie::Base` returns plain hashes, you can use `Kweerie::BaseObjects` to get typed Ruby objects with proper attribute methods:
59
+
60
+ ```ruby
61
+ class UserSearch < Kweerie::BaseObjects
62
+ bind :name, as: '$1'
63
+ bind :created_at, as: '$2'
64
+ end
65
+
66
+ # Returns array of objects instead of hashes
67
+ users = UserSearch.with(
68
+ name: 'Claude',
69
+ created_at: '2024-01-01'
70
+ )
71
+
72
+ user = users.first
73
+ user.name # => "Claude"
74
+ user.created_at # => 2024-01-01 00:00:00 +0000 (Time object)
75
+ ```
76
+
77
+ ### Automatic Type Casting
78
+
79
+ BaseObjects automatically casts common PostgreSQL types to their Ruby equivalents:
80
+
81
+ ```ruby
82
+ # In your SQL file
83
+ SELECT
84
+ name,
85
+ created_at, -- timestamp
86
+ age::integer, -- integer
87
+ score::float, -- float
88
+ active::boolean, -- boolean
89
+ metadata::jsonb, -- jsonb
90
+ tags::jsonb -- jsonb array
91
+ FROM users;
92
+
93
+ # In your Ruby code
94
+ user = UserSearch.with(name: 'Claude').first
95
+
96
+ user.created_at # => Time object
97
+ user.age # => Integer
98
+ user.score # => Float
99
+ user.active # => true/false
100
+ user.metadata # => Hash with string keys
101
+ user.tags # => Array
102
+
103
+ # Nested JSONB data is properly accessible
104
+ user.metadata["role"] # => "admin"
105
+ user.metadata["preferences"]["theme"] # => "dark"
106
+ ```
107
+
108
+ ### Pattern Matching Support
109
+
110
+ BaseObjects support Ruby's pattern matching syntax:
111
+
112
+ ```ruby
113
+ case user
114
+ in { name:, metadata: { role: "admin" } }
115
+ puts "Admin user: #{name}"
116
+ in { name:, metadata: { role: "user" } }
117
+ puts "Regular user: #{name}"
118
+ end
119
+
120
+ # Nested pattern matching
121
+ case user
122
+ in { metadata: { preferences: { theme: "dark" } } }
123
+ puts "Dark theme user"
124
+ end
125
+ ```
126
+
127
+ ### Object Interface
128
+
129
+ BaseObjects provide several useful methods:
130
+
131
+ ```ruby
132
+ # Hash-like access
133
+ user[:name] # => "Claude"
134
+ user.fetch(:email, 'N/A') # => Returns 'N/A' if email is nil
135
+
136
+ # Serialization
137
+ user.to_h # => Hash with string keys
138
+ user.to_json # => JSON string
139
+
140
+ # Comparison
141
+ user1 == user2 # Compare all attributes
142
+ users.sort_by(&:created_at) # Sortable
143
+
144
+ # Change tracking
145
+ user.changed? # => Check if any attributes changed
146
+ user.changes # => Hash of changes with [old, new] values
147
+ user.original_attributes # => Original attributes from DB
148
+ ```
149
+
150
+ ## PostgreSQL Array Support
151
+
152
+ BaseObjects handles PostgreSQL arrays by converting them to Ruby arrays with proper type casting:
153
+
154
+ ```ruby
155
+ # In your PostgreSQL schema
156
+ create_table :users do |t|
157
+ t.integer :preferred_ordering, array: true, default: []
158
+ t.string :tags, array: true
159
+ t.float :scores, array: true
160
+ end
161
+
162
+ # In your query
163
+ user = UserSearch.with(name: 'Claude').first
164
+
165
+ user.preferred_ordering # => [1, 3, 2]
166
+ user.tags # => ["ruby", "rails"]
167
+ user.scores # => [98.5, 87.2, 92.0]
168
+ ```
169
+
170
+ ## Performance Considerations
171
+
172
+ BaseObjects creates a unique class for each query result set, with the following optimizations:
173
+
174
+ - Classes are cached and reused for subsequent queries
175
+ - Attribute readers are defined upfront
176
+ - Type casting happens once during initialization
177
+ - No method_missing or dynamic method definition per instance
178
+ - Efficient pattern matching support
179
+
180
+ For queries where you don't need the object interface, use `Kweerie::Base` instead for slightly better performance.
181
+
56
182
  ### Rails Generator
57
183
 
58
184
  If you're using Rails, you can use the generator to create new query files:
@@ -32,11 +32,11 @@ class KweerieGenerator < Rails::Generators::NamedBase
32
32
  -- Write your SQL query here
33
33
  -- Available parameters: #{parameters.map { |p| "$#{parameters.index(p) + 1} (#{p})" }.join(", ")}
34
34
 
35
- SELECT
35
+ -- SELECT
36
36
  -- your columns here
37
- FROM
37
+ -- FROM
38
38
  -- your tables here
39
- WHERE
39
+ -- WHERE
40
40
  -- your conditions here
41
41
  SQL
42
42
  end
@@ -0,0 +1,202 @@
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 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 :type_cast_value do |value|
33
+ case value
34
+ when /^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?$/ # DateTime check
35
+ Time.parse(value)
36
+ when /^\d+$/ # Integer check
37
+ value.to_i
38
+ when /^\d*\.\d+$/ # Float check
39
+ value.to_f
40
+ when /^(true|false)$/i # Boolean check
41
+ value.downcase == "true"
42
+ when /^{.*}$/ # Could be PG array or JSON
43
+ if value.start_with?("{") && value.end_with?("}") && !value.include?('"=>') && !value.include?(": ")
44
+ # PostgreSQL array (simple heuristic: no "=>" or ":" suggests it's not JSON)
45
+ parse_pg_array(value)
46
+ else
47
+ # Attempt JSON parse
48
+ begin
49
+ parsed = JSON.parse(value)
50
+ deep_stringify_keys(parsed)
51
+ rescue JSON::ParserError
52
+ value
53
+ end
54
+ end
55
+ when /^[\[{]/ # Pure JSON (arrays starting with [ or other JSON objects)
56
+ begin
57
+ parsed = JSON.parse(value)
58
+ deep_stringify_keys(parsed)
59
+ rescue JSON::ParserError
60
+ value
61
+ end
62
+ else
63
+ value
64
+ end
65
+ end
66
+
67
+ define_method :initialize do |attrs|
68
+ # Store original attributes with the same type casting
69
+ @_original_attributes = attrs.transform_keys(&:to_s).transform_values do |value|
70
+ type_cast_value(value)
71
+ end
72
+
73
+ attrs.each do |name, value|
74
+ casted_value = type_cast_value(value)
75
+ instance_variable_set("@#{name}", casted_value)
76
+ end
77
+ end
78
+ define_method :parse_pg_array do |value|
79
+ # Remove the curly braces
80
+ clean_value = value.gsub(/^{|}$/, "")
81
+ return [] if clean_value.empty?
82
+
83
+ # Split on comma, but not within quoted strings
84
+ elements = clean_value.split(/,(?=(?:[^"]*"[^"]*")*[^"]*$)/)
85
+
86
+ elements.map do |element|
87
+ case element
88
+ when /^\d+$/ # Integer
89
+ element.to_i
90
+ when /^\d*\.\d+$/ # Float
91
+ element.to_f
92
+ when /^(true|false)$/i # Boolean
93
+ element.downcase == "true"
94
+ when /^"(.*)"$/ # Quoted string
95
+ ::Regexp.last_match(1)
96
+ else
97
+ element
98
+ end
99
+ end
100
+ end
101
+
102
+ define_method :deep_symbolize_keys do |obj|
103
+ case obj
104
+ when Hash
105
+ obj.transform_keys(&:to_sym).transform_values { |v| deep_symbolize_keys(v) }
106
+ when Array
107
+ obj.map { |item| item.is_a?(Hash) ? deep_symbolize_keys(item) : item }
108
+ else
109
+ obj
110
+ end
111
+ end
112
+
113
+ # Nice inspect output
114
+ define_method :inspect do
115
+ attrs = attribute_names.map do |name|
116
+ "#{name}=#{instance_variable_get("@#{name}").inspect}"
117
+ end.join(" ")
118
+ "#<#{self.class.name || "Record"} #{attrs}>"
119
+ end
120
+
121
+ # Hash-like access
122
+ define_method :[] do |key|
123
+ instance_variable_get("@#{key}")
124
+ end
125
+
126
+ define_method :fetch do |key, default = nil|
127
+ instance_variable_defined?("@#{key}") ? instance_variable_get("@#{key}") : default
128
+ end
129
+
130
+ # Comparison methods
131
+ define_method :<=> do |other|
132
+ return nil unless other.is_a?(self.class)
133
+
134
+ to_h <=> other.to_h
135
+ end
136
+
137
+ define_method :== do |other|
138
+ return false unless other.is_a?(self.class)
139
+
140
+ to_h == other.to_h
141
+ end
142
+
143
+ define_method :eql? do |other|
144
+ self == other
145
+ end
146
+
147
+ define_method :hash do
148
+ to_h.hash
149
+ end
150
+
151
+ # Add helper method for deep string keys
152
+ define_method :deep_stringify_keys do |obj|
153
+ case obj
154
+ when Hash
155
+ obj.transform_keys(&:to_s).transform_values { |v| deep_stringify_keys(v) }
156
+ when Array
157
+ obj.map { |item| item.is_a?(Hash) ? deep_stringify_keys(item) : item }
158
+ else
159
+ obj
160
+ end
161
+ end
162
+
163
+ # Serialization
164
+ define_method :to_h do
165
+ attribute_names.each_with_object({}) do |name, hash|
166
+ value = instance_variable_get("@#{name}")
167
+ # Ensure string keys in output
168
+ hash[name.to_s] = value.is_a?(Hash) ? deep_stringify_keys(value) : value
169
+ end
170
+ end
171
+
172
+ define_method :to_json do |*args|
173
+ to_h.to_json(*args)
174
+ end
175
+
176
+ # Pattern matching support (Ruby 2.7+)
177
+ define_method :deconstruct_keys do |keys|
178
+ symbolized = deep_symbolize_keys(to_h)
179
+ keys ? symbolized.slice(*keys) : symbolized
180
+ end
181
+
182
+ # Original attributes access
183
+ define_method :original_attributes do
184
+ @_original_attributes
185
+ end
186
+
187
+ # ActiveModel-like changes tracking
188
+ define_method :changed? do
189
+ to_h != @_original_attributes
190
+ end
191
+
192
+ define_method :changes do
193
+ to_h.each_with_object({}) do |(key, value), changes|
194
+ original = @_original_attributes[key]
195
+ changes[key] = [original, value] if original != value
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end
202
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kweerie
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.2"
5
5
  end
data/lib/kweerie.rb CHANGED
@@ -26,3 +26,4 @@ end
26
26
 
27
27
  require_relative "kweerie/configuration"
28
28
  require_relative "kweerie/base"
29
+ require_relative "kweerie/base_objects"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kweerie
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Toby
@@ -11,33 +11,33 @@ cert_chain: []
11
11
  date: 2024-11-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: pg
14
+ name: minitest
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.0'
19
+ version: '5.0'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.0'
26
+ version: '5.0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: minitest
28
+ name: pg
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '5.0'
33
+ version: '1.0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '5.0'
40
+ version: '1.0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rails
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -67,6 +67,7 @@ files:
67
67
  - lib/generators/kweerie/kweerie_generator.rb
68
68
  - lib/kweerie.rb
69
69
  - lib/kweerie/base.rb
70
+ - lib/kweerie/base_objects.rb
70
71
  - lib/kweerie/configuration.rb
71
72
  - lib/kweerie/version.rb
72
73
  - sig/kweerie.rbs