dorm 0.1.0
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 +7 -0
- data/.rubocop.yml +8 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/README.md +226 -0
- data/Rakefile +92 -0
- data/examples/connection_pool_example.rb +88 -0
- data/examples/query_builder_examples.rb +202 -0
- data/lib/dorm/connection_pool.rb +218 -0
- data/lib/dorm/database.rb +142 -0
- data/lib/dorm/functional_helpers.rb +141 -0
- data/lib/dorm/query_builder.rb +434 -0
- data/lib/dorm/repository.rb +338 -0
- data/lib/dorm/result.rb +77 -0
- data/lib/dorm/version.rb +5 -0
- data/lib/dorm.rb +25 -0
- data/sig/Dorm.rbs +4 -0
- metadata +159 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dorm
|
|
4
|
+
# Metaprogramming module to generate repository methods
|
|
5
|
+
module Repository
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def for(data_class, table_name: nil, validations: {})
|
|
9
|
+
table_name ||= pluralize(data_class.name.downcase)
|
|
10
|
+
|
|
11
|
+
Module.new do
|
|
12
|
+
extend self
|
|
13
|
+
|
|
14
|
+
# Store metadata about this repository
|
|
15
|
+
define_singleton_method(:data_class) { data_class }
|
|
16
|
+
define_singleton_method(:table_name) { table_name }
|
|
17
|
+
define_singleton_method(:validations) { validations }
|
|
18
|
+
define_singleton_method(:columns) { data_class.members }
|
|
19
|
+
define_singleton_method(:db_columns) { columns - [:id] }
|
|
20
|
+
|
|
21
|
+
# Helper method to get the correct placeholder syntax for the current adapter
|
|
22
|
+
define_singleton_method(:placeholder) do |index|
|
|
23
|
+
case Database.adapter
|
|
24
|
+
when :postgresql
|
|
25
|
+
"$#{index}"
|
|
26
|
+
when :sqlite3
|
|
27
|
+
'?'
|
|
28
|
+
else
|
|
29
|
+
'?'
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Helper method to generate placeholders for multiple values
|
|
34
|
+
define_singleton_method(:placeholders) do |count, start_index: 1|
|
|
35
|
+
case Database.adapter
|
|
36
|
+
when :postgresql
|
|
37
|
+
(start_index...(start_index + count)).map { |i| "$#{i}" }
|
|
38
|
+
when :sqlite3
|
|
39
|
+
Array.new(count, '?')
|
|
40
|
+
else
|
|
41
|
+
Array.new(count, '?')
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Helper method to handle RETURNING clause differences
|
|
46
|
+
define_singleton_method(:returning_clause) do |column = 'id'|
|
|
47
|
+
case Database.adapter
|
|
48
|
+
when :postgresql
|
|
49
|
+
"RETURNING #{column}"
|
|
50
|
+
when :sqlite3
|
|
51
|
+
"RETURNING #{column}"
|
|
52
|
+
else
|
|
53
|
+
"RETURNING #{column}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Helper method to check if result is empty (adapter-specific)
|
|
58
|
+
define_singleton_method(:result_empty?) do |result|
|
|
59
|
+
case Database.adapter
|
|
60
|
+
when :postgresql
|
|
61
|
+
result.ntuples == 0
|
|
62
|
+
when :sqlite3
|
|
63
|
+
result.empty?
|
|
64
|
+
else
|
|
65
|
+
result.empty?
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Generate standard CRUD methods
|
|
70
|
+
# Find by ID
|
|
71
|
+
define_singleton_method(:find) do |id|
|
|
72
|
+
Result.try do
|
|
73
|
+
result = Database.query("SELECT * FROM #{table_name} WHERE id = #{placeholder(1)}", [id])
|
|
74
|
+
raise 'Record not found' if result_empty?(result)
|
|
75
|
+
|
|
76
|
+
row_to_data(result[0])
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Find all records
|
|
81
|
+
define_singleton_method(:find_all) do
|
|
82
|
+
Result.try do
|
|
83
|
+
result = Database.query("SELECT * FROM #{table_name} ORDER BY id")
|
|
84
|
+
result.map { |row| row_to_data(row) }
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Create new record
|
|
89
|
+
define_singleton_method(:create) do |attrs|
|
|
90
|
+
Result.try do
|
|
91
|
+
validate_attrs(attrs)
|
|
92
|
+
|
|
93
|
+
now = Time.now
|
|
94
|
+
attrs_with_timestamps = attrs.merge(created_at: now, updated_at: now)
|
|
95
|
+
|
|
96
|
+
# Ensure we don't have an id in the attributes for creation
|
|
97
|
+
attrs_with_timestamps.delete(:id) if attrs_with_timestamps.key?(:id)
|
|
98
|
+
|
|
99
|
+
record = data_class.new(id: nil, **attrs_with_timestamps)
|
|
100
|
+
|
|
101
|
+
columns_list = db_columns.join(', ')
|
|
102
|
+
placeholder_list = placeholders(db_columns.length).join(', ')
|
|
103
|
+
values = db_columns.map { |col| serialize_value(record.send(col)) }
|
|
104
|
+
|
|
105
|
+
result = Database.query(
|
|
106
|
+
"INSERT INTO #{table_name} (#{columns_list}) VALUES (#{placeholder_list}) #{returning_clause}",
|
|
107
|
+
values
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Handle different return formats
|
|
111
|
+
id_value = case Database.adapter
|
|
112
|
+
when :postgresql
|
|
113
|
+
result[0]['id'].to_i
|
|
114
|
+
when :sqlite3
|
|
115
|
+
result[0]['id'].to_i
|
|
116
|
+
else
|
|
117
|
+
result[0]['id'].to_i
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
record.with(id: id_value)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Update existing record
|
|
125
|
+
define_singleton_method(:update) do |record|
|
|
126
|
+
Result.try do
|
|
127
|
+
raise 'Cannot update record without id' unless record.id
|
|
128
|
+
|
|
129
|
+
updated_record = record.with(updated_at: Time.now)
|
|
130
|
+
|
|
131
|
+
set_clauses = db_columns.map.with_index(1) { |col, i| "#{col} = #{placeholder(i)}" }.join(', ')
|
|
132
|
+
values = db_columns.map { |col| serialize_value(updated_record.send(col)) }
|
|
133
|
+
values << updated_record.id
|
|
134
|
+
|
|
135
|
+
id_placeholder = placeholder(db_columns.length + 1)
|
|
136
|
+
result = Database.query(
|
|
137
|
+
"UPDATE #{table_name} SET #{set_clauses} WHERE id = #{id_placeholder} #{returning_clause}",
|
|
138
|
+
values
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
raise 'Record not found' if result_empty?(result)
|
|
142
|
+
|
|
143
|
+
updated_record
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Save (create or update)
|
|
148
|
+
define_singleton_method(:save) do |record|
|
|
149
|
+
if record.id
|
|
150
|
+
update(record)
|
|
151
|
+
else
|
|
152
|
+
attrs = record.to_h
|
|
153
|
+
attrs.delete(:id) # Remove id key if present
|
|
154
|
+
create(attrs)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Delete record
|
|
159
|
+
define_singleton_method(:delete) do |record|
|
|
160
|
+
Result.try do
|
|
161
|
+
raise 'Cannot delete record without id' unless record.id
|
|
162
|
+
|
|
163
|
+
result = Database.query(
|
|
164
|
+
"DELETE FROM #{table_name} WHERE id = #{placeholder(1)} #{returning_clause}",
|
|
165
|
+
[record.id]
|
|
166
|
+
)
|
|
167
|
+
raise 'Record not found' if result_empty?(result)
|
|
168
|
+
|
|
169
|
+
record
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Query methods
|
|
174
|
+
# Where with predicate
|
|
175
|
+
define_singleton_method(:where) do |predicate|
|
|
176
|
+
find_all.map { |records| records.select(&predicate) }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Find by attributes
|
|
180
|
+
define_singleton_method(:find_by) do |**attrs|
|
|
181
|
+
Result.try do
|
|
182
|
+
conditions = attrs.keys.map.with_index(1) { |key, i| "#{key} = #{placeholder(i)}" }.join(' AND ')
|
|
183
|
+
values = attrs.values.map { |val| serialize_value(val) }
|
|
184
|
+
|
|
185
|
+
result = Database.query("SELECT * FROM #{table_name} WHERE #{conditions}", values)
|
|
186
|
+
raise 'Record not found' if result_empty?(result)
|
|
187
|
+
|
|
188
|
+
row_to_data(result[0])
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Find all by attributes
|
|
193
|
+
define_singleton_method(:find_all_by) do |**attrs|
|
|
194
|
+
Result.try do
|
|
195
|
+
conditions = attrs.keys.map.with_index(1) { |key, i| "#{key} = #{placeholder(i)}" }.join(' AND ')
|
|
196
|
+
values = attrs.values.map { |val| serialize_value(val) }
|
|
197
|
+
|
|
198
|
+
result = Database.query("SELECT * FROM #{table_name} WHERE #{conditions}", values)
|
|
199
|
+
result.map { |row| row_to_data(row) }
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Count records
|
|
204
|
+
define_singleton_method(:count) do
|
|
205
|
+
Result.try do
|
|
206
|
+
result = Database.query("SELECT COUNT(*) as count FROM #{table_name}")
|
|
207
|
+
case Database.adapter
|
|
208
|
+
when :postgresql
|
|
209
|
+
result[0]['count'].to_i
|
|
210
|
+
when :sqlite3
|
|
211
|
+
result[0]['count'].to_i
|
|
212
|
+
else
|
|
213
|
+
result[0]['count'].to_i
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Validation method
|
|
219
|
+
define_singleton_method(:validate_attrs) do |attrs|
|
|
220
|
+
validations.each do |field, rules|
|
|
221
|
+
value = attrs[field]
|
|
222
|
+
|
|
223
|
+
if rules[:required] && (value.nil? || (value.respond_to?(:empty?) && value.empty?) || (value.respond_to?(:strip) && value.strip.empty?))
|
|
224
|
+
raise ValidationError, "#{field} is required"
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
if rules[:format] && value && !value.match?(rules[:format])
|
|
228
|
+
raise ValidationError, "#{field} has invalid format"
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
if rules[:length] && value && !rules[:length].include?(value.length)
|
|
232
|
+
raise ValidationError, "#{field} length must be #{rules[:length]}"
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
if rules[:range] && value && !rules[:range].include?(value)
|
|
236
|
+
raise ValidationError, "#{field} must be in range #{rules[:range]}"
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Helper methods
|
|
242
|
+
define_singleton_method(:row_to_data) do |row|
|
|
243
|
+
attrs = {}
|
|
244
|
+
columns.each do |col|
|
|
245
|
+
attrs[col] = deserialize_value(col, row[col.to_s])
|
|
246
|
+
end
|
|
247
|
+
data_class.new(**attrs)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
define_singleton_method(:serialize_value) do |value|
|
|
251
|
+
case value
|
|
252
|
+
when Time
|
|
253
|
+
case Database.adapter
|
|
254
|
+
when :postgresql
|
|
255
|
+
value # PostgreSQL handles Time objects natively
|
|
256
|
+
when :sqlite3
|
|
257
|
+
value.to_s # SQLite needs string representation
|
|
258
|
+
else
|
|
259
|
+
value.to_s
|
|
260
|
+
end
|
|
261
|
+
when true, false
|
|
262
|
+
case Database.adapter
|
|
263
|
+
when :postgresql
|
|
264
|
+
value # PostgreSQL handles booleans natively
|
|
265
|
+
when :sqlite3
|
|
266
|
+
value ? 1 : 0 # SQLite uses integers for booleans
|
|
267
|
+
else
|
|
268
|
+
value
|
|
269
|
+
end
|
|
270
|
+
else
|
|
271
|
+
value
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
define_singleton_method(:deserialize_value) do |column, value|
|
|
276
|
+
return nil if value.nil?
|
|
277
|
+
|
|
278
|
+
case column
|
|
279
|
+
when :id, :user_id, :post_id, :comment_id
|
|
280
|
+
value.to_i
|
|
281
|
+
when :created_at, :updated_at
|
|
282
|
+
case Database.adapter
|
|
283
|
+
when :postgresql
|
|
284
|
+
# PostgreSQL might return Time objects or strings
|
|
285
|
+
value.is_a?(Time) ? value : Time.parse(value.to_s)
|
|
286
|
+
when :sqlite3
|
|
287
|
+
Time.parse(value.to_s)
|
|
288
|
+
else
|
|
289
|
+
Time.parse(value.to_s)
|
|
290
|
+
end
|
|
291
|
+
when /.*_id$/
|
|
292
|
+
value.to_i
|
|
293
|
+
when :published, :active, :approved
|
|
294
|
+
# Handle boolean fields
|
|
295
|
+
case Database.adapter
|
|
296
|
+
when :postgresql
|
|
297
|
+
# PostgreSQL returns actual booleans or 't'/'f' strings
|
|
298
|
+
case value
|
|
299
|
+
when true, 't', 'true', '1', 1
|
|
300
|
+
true
|
|
301
|
+
when false, 'f', 'false', '0', 0
|
|
302
|
+
false
|
|
303
|
+
else
|
|
304
|
+
!!value
|
|
305
|
+
end
|
|
306
|
+
when :sqlite3
|
|
307
|
+
# SQLite returns integers for booleans
|
|
308
|
+
case value
|
|
309
|
+
when 1, '1', 'true', true
|
|
310
|
+
true
|
|
311
|
+
when 0, '0', 'false', false
|
|
312
|
+
false
|
|
313
|
+
else
|
|
314
|
+
!!value
|
|
315
|
+
end
|
|
316
|
+
else
|
|
317
|
+
!!value
|
|
318
|
+
end
|
|
319
|
+
else
|
|
320
|
+
value
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def pluralize(word)
|
|
327
|
+
# Simple pluralization - could be enhanced with inflector gem
|
|
328
|
+
case word
|
|
329
|
+
when /y$/
|
|
330
|
+
word.sub(/y$/, 'ies')
|
|
331
|
+
when /s$/, /x$/, /z$/, /ch$/, /sh$/
|
|
332
|
+
word + 'es'
|
|
333
|
+
else
|
|
334
|
+
word + 's'
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
end
|
data/lib/dorm/result.rb
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dorm
|
|
4
|
+
# Result monad using Data - inspired by dry-monads
|
|
5
|
+
module Result
|
|
6
|
+
Success = Data.define(:value) do
|
|
7
|
+
def success? = true
|
|
8
|
+
def failure? = false
|
|
9
|
+
|
|
10
|
+
def bind(&block)
|
|
11
|
+
block.call(value)
|
|
12
|
+
rescue StandardError => e
|
|
13
|
+
Failure.new(error: e.message)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def map(&block)
|
|
17
|
+
Success.new(value: block.call(value))
|
|
18
|
+
rescue StandardError => e
|
|
19
|
+
Failure.new(error: e.message)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def value_or(default = nil)
|
|
23
|
+
value
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
Failure = Data.define(:error) do
|
|
28
|
+
def success? = false
|
|
29
|
+
def failure? = true
|
|
30
|
+
|
|
31
|
+
def bind(&block)
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def map(&block)
|
|
36
|
+
self
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def value_or(default = nil)
|
|
40
|
+
default
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Add aliases to instance methods by reopening the Data classes
|
|
45
|
+
Success.class_eval do
|
|
46
|
+
alias_method :fmap, :map
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
Failure.class_eval do
|
|
50
|
+
alias_method :fmap, :map
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
module_function
|
|
54
|
+
|
|
55
|
+
def success(value)
|
|
56
|
+
Success.new(value: value)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def failure(error)
|
|
60
|
+
Failure.new(error: error)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def try(&block)
|
|
64
|
+
success(block.call)
|
|
65
|
+
rescue StandardError => e
|
|
66
|
+
failure(e.message)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Combine multiple Results
|
|
70
|
+
def combine(*results)
|
|
71
|
+
failures = results.select(&:failure?)
|
|
72
|
+
return failure(failures.map(&:error).join(', ')) unless failures.empty?
|
|
73
|
+
|
|
74
|
+
success(results.map(&:value))
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
data/lib/dorm/version.rb
ADDED
data/lib/dorm.rb
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "dorm/version"
|
|
4
|
+
require_relative "dorm/result"
|
|
5
|
+
require_relative "dorm/database"
|
|
6
|
+
require_relative "dorm/repository"
|
|
7
|
+
require_relative "dorm/query_builder"
|
|
8
|
+
require_relative "dorm/connection_pool"
|
|
9
|
+
require_relative "dorm/functional_helpers"
|
|
10
|
+
|
|
11
|
+
module Dorm
|
|
12
|
+
class Error < StandardError; end
|
|
13
|
+
class ConfigurationError < Error; end
|
|
14
|
+
class ValidationError < Error; end
|
|
15
|
+
class RecordNotFoundError < Error; end
|
|
16
|
+
|
|
17
|
+
def self.configure(**options)
|
|
18
|
+
Database.configure(**options)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Convenience method for creating repositories
|
|
22
|
+
def self.repository_for(data_class, **options)
|
|
23
|
+
Repository.for(data_class, **options)
|
|
24
|
+
end
|
|
25
|
+
end
|
data/sig/Dorm.rbs
ADDED
metadata
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: dorm
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- ecnal
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: pg
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: sqlite3
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.4'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.4'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: bundler
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '2.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '2.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rake
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '13.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '13.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rspec
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '3.0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '3.0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: rubocop
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '1.0'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '1.0'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: yard
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - "~>"
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '0.9'
|
|
103
|
+
type: :development
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - "~>"
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '0.9'
|
|
110
|
+
description: |
|
|
111
|
+
Dorm (Data ORM) is a lightweight, functional ORM built on Ruby's Data class.
|
|
112
|
+
Features immutable records, monadic error handling inspired by dry-monads,
|
|
113
|
+
and a functional programming approach to database operations.
|
|
114
|
+
executables: []
|
|
115
|
+
extensions: []
|
|
116
|
+
extra_rdoc_files: []
|
|
117
|
+
files:
|
|
118
|
+
- ".rubocop.yml"
|
|
119
|
+
- ".ruby-gemset"
|
|
120
|
+
- ".ruby-version"
|
|
121
|
+
- README.md
|
|
122
|
+
- Rakefile
|
|
123
|
+
- examples/connection_pool_example.rb
|
|
124
|
+
- examples/query_builder_examples.rb
|
|
125
|
+
- lib/dorm.rb
|
|
126
|
+
- lib/dorm/connection_pool.rb
|
|
127
|
+
- lib/dorm/database.rb
|
|
128
|
+
- lib/dorm/functional_helpers.rb
|
|
129
|
+
- lib/dorm/query_builder.rb
|
|
130
|
+
- lib/dorm/repository.rb
|
|
131
|
+
- lib/dorm/result.rb
|
|
132
|
+
- lib/dorm/version.rb
|
|
133
|
+
- sig/Dorm.rbs
|
|
134
|
+
homepage: https://github.com/ecnal/dorm
|
|
135
|
+
licenses:
|
|
136
|
+
- MIT
|
|
137
|
+
metadata:
|
|
138
|
+
allowed_push_host: https://rubygems.org
|
|
139
|
+
homepage_uri: https://github.com/ecnal/dorm
|
|
140
|
+
source_code_uri: https://github.com/ecnal/dorm
|
|
141
|
+
changelog_uri: https://github.com/ecnal/dorm/blob/master/CHANGELOG.md
|
|
142
|
+
rdoc_options: []
|
|
143
|
+
require_paths:
|
|
144
|
+
- lib
|
|
145
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
146
|
+
requirements:
|
|
147
|
+
- - ">="
|
|
148
|
+
- !ruby/object:Gem::Version
|
|
149
|
+
version: 3.2.0
|
|
150
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
151
|
+
requirements:
|
|
152
|
+
- - ">="
|
|
153
|
+
- !ruby/object:Gem::Version
|
|
154
|
+
version: '0'
|
|
155
|
+
requirements: []
|
|
156
|
+
rubygems_version: 3.6.9
|
|
157
|
+
specification_version: 4
|
|
158
|
+
summary: A functional ORM using Ruby's Data class with monadic error handling
|
|
159
|
+
test_files: []
|