kweerie 0.1.0 → 0.1.1

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: 13712e8746dc90c472a92aba0ee7fc7897069d669a20e64f7c57f898a7ff0012
4
+ data.tar.gz: 9593d6a93cb80e8b8949764c91c49a669bb96d9828530eb876b5c2d04ad80fbd
5
5
  SHA512:
6
- metadata.gz: 026f1786bbb42976b0461951005f448e7b2169b6962a5f06737a9cc2fbdd99af93c84ea5d5ee277067788e349c6e73df98096154bebea79f6a2b9b5f450f0234
7
- data.tar.gz: d3e8e06dcaa23ea12f205e623861ba08cd16b2be877e9008571efbe38a6f6a536efb717ed45ca977102c9b2fa2cf5ebec737a31faf6e8e4accf4e93f51412395
6
+ metadata.gz: 713a3d4d3e044f560ba9a1278d011c131b699d5805d40f95caf2e43eabf80865ae7ba4a2099d37f1d7b9e517f8c0cc16c35ca25cb18ddb1fea188455c1f019f0
7
+ data.tar.gz: e5007711e45dabe31bc20a7780eb503a7f4114e21a47ad711b2327ca99a5923dce68359fa1229cb4cc488eb7c3c02cb8692b89189825931175a7eb4440adf075
data/README.md CHANGED
@@ -53,6 +53,112 @@ 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
+ ### Performance Considerations
151
+
152
+ BaseObjects creates a unique class for each query result set, with the following optimizations:
153
+
154
+ - Classes are cached and reused for subsequent queries
155
+ - Attribute readers are defined upfront
156
+ - Type casting happens once during initialization
157
+ - No method_missing or dynamic method definition per instance
158
+ - Efficient pattern matching support
159
+
160
+ For queries where you don't need the object interface, use `Kweerie::Base` instead for slightly better performance.
161
+
56
162
  ### Rails Generator
57
163
 
58
164
  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,167 @@
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 /^[\[{]/ # JSON/JSONB check
43
+ begin
44
+ parsed = JSON.parse(value)
45
+ # Use string keys for consistency in output
46
+ deep_stringify_keys(parsed)
47
+ rescue JSON::ParserError
48
+ value
49
+ end
50
+ else
51
+ value
52
+ end
53
+ end
54
+
55
+ define_method :initialize do |attrs|
56
+ # Store original attributes with the same type casting
57
+ @_original_attributes = attrs.transform_keys(&:to_s).transform_values do |value|
58
+ type_cast_value(value)
59
+ end
60
+
61
+ attrs.each do |name, value|
62
+ casted_value = type_cast_value(value)
63
+ instance_variable_set("@#{name}", casted_value)
64
+ end
65
+ end
66
+
67
+ define_method :deep_symbolize_keys do |obj|
68
+ case obj
69
+ when Hash
70
+ obj.transform_keys(&:to_sym).transform_values { |v| deep_symbolize_keys(v) }
71
+ when Array
72
+ obj.map { |item| item.is_a?(Hash) ? deep_symbolize_keys(item) : item }
73
+ else
74
+ obj
75
+ end
76
+ end
77
+
78
+ # Nice inspect output
79
+ define_method :inspect do
80
+ attrs = attribute_names.map do |name|
81
+ "#{name}=#{instance_variable_get("@#{name}").inspect}"
82
+ end.join(" ")
83
+ "#<#{self.class.name || "Record"} #{attrs}>"
84
+ end
85
+
86
+ # Hash-like access
87
+ define_method :[] do |key|
88
+ instance_variable_get("@#{key}")
89
+ end
90
+
91
+ define_method :fetch do |key, default = nil|
92
+ instance_variable_defined?("@#{key}") ? instance_variable_get("@#{key}") : default
93
+ end
94
+
95
+ # Comparison methods
96
+ define_method :<=> do |other|
97
+ return nil unless other.is_a?(self.class)
98
+
99
+ to_h <=> other.to_h
100
+ end
101
+
102
+ define_method :== do |other|
103
+ return false unless other.is_a?(self.class)
104
+
105
+ to_h == other.to_h
106
+ end
107
+
108
+ define_method :eql? do |other|
109
+ self == other
110
+ end
111
+
112
+ define_method :hash do
113
+ to_h.hash
114
+ end
115
+
116
+ # Add helper method for deep string keys
117
+ define_method :deep_stringify_keys do |obj|
118
+ case obj
119
+ when Hash
120
+ obj.transform_keys(&:to_s).transform_values { |v| deep_stringify_keys(v) }
121
+ when Array
122
+ obj.map { |item| item.is_a?(Hash) ? deep_stringify_keys(item) : item }
123
+ else
124
+ obj
125
+ end
126
+ end
127
+
128
+ # Serialization
129
+ define_method :to_h do
130
+ attribute_names.each_with_object({}) do |name, hash|
131
+ value = instance_variable_get("@#{name}")
132
+ # Ensure string keys in output
133
+ hash[name.to_s] = value.is_a?(Hash) ? deep_stringify_keys(value) : value
134
+ end
135
+ end
136
+
137
+ define_method :to_json do |*args|
138
+ to_h.to_json(*args)
139
+ end
140
+
141
+ # Pattern matching support (Ruby 2.7+)
142
+ define_method :deconstruct_keys do |keys|
143
+ symbolized = deep_symbolize_keys(to_h)
144
+ keys ? symbolized.slice(*keys) : symbolized
145
+ end
146
+
147
+ # Original attributes access
148
+ define_method :original_attributes do
149
+ @_original_attributes
150
+ end
151
+
152
+ # ActiveModel-like changes tracking
153
+ define_method :changed? do
154
+ to_h != @_original_attributes
155
+ end
156
+
157
+ define_method :changes do
158
+ to_h.each_with_object({}) do |(key, value), changes|
159
+ original = @_original_attributes[key]
160
+ changes[key] = [original, value] if original != value
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
167
+ 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.1"
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.1
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