kweerie 0.1.0 → 0.1.1

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