kweerie 0.1.0 → 0.1.2

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