familia 2.0.0.pre24 → 2.0.0.pre25

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: 9b6118ccf4f7a1ed2da6b080a8e256088481ecd93d9f60378f2fca40fe5bb364
4
- data.tar.gz: 1960335d0f688f6b9fb8c8a0ccfc8c4314b85ee36cf4dc56281c1e6d0642f455
3
+ metadata.gz: 99afa47f80c0cbf07cf0c1c88dbf2fc6d9f720641d9404efbb181c90dd83631b
4
+ data.tar.gz: 91ea00ff6aa997daa9781b75723e90bd77534bdf7d32f159f305d50e33e6a62e
5
5
  SHA512:
6
- metadata.gz: 51a4ab15e2181939e0b2b43f440e2629f149b9e774c7061737812c39f8b722280b20c8468d0e41ae6045c08a1751a661865622e0277f252edbd79e12eedbd9bc
7
- data.tar.gz: 3cebb9d48404ce9b3aaffc43d74d605dda64d90463fbc604c8b0ad2a53853132683bb8b39be24e905ab8b86d5d4736a36fee91dc1fe9cd57b9fc18fd10331265
6
+ metadata.gz: d2e0bdba2c70ee453af1cf7601930ce44d434a1219eca2ae03786c40d8cb5c02ad689e6e1850d31df6e1a9525fbcd8a10aa95b0202bd68b9873fcb4084e2e10f
7
+ data.tar.gz: 47d6c6c5c8e51184124532754caced75d0e3c6f5ceace675c9ed2164f61ffdcbcb77370c0759ed70cf70ea3edc7290247e69f6d5dfe4a6a6dbd94831879c6054
data/CHANGELOG.rst CHANGED
@@ -7,6 +7,43 @@ The format is based on `Keep a Changelog <https://keepachangelog.com/en/1.1.0/>`
7
7
 
8
8
  <!--scriv-insert-here-->
9
9
 
10
+ .. _changelog-2.0.0.pre25:
11
+
12
+ 2.0.0.pre25 — 2026-01-08
13
+ ========================
14
+
15
+ Added
16
+ -----
17
+
18
+ - Class-level multi-value indexing with ``multi_index :field, :index_name`` (``within: :class`` is now the default). Creates class methods like ``Model.find_all_by_field(value)`` and ``Model.sample_from_field(value, count)`` for grouping objects by field values at the class level.
19
+
20
+ - New ``JsonStringKey`` DataType for type-preserving string storage. Unlike
21
+ ``StringKey`` which uses raw strings (for INCR/DECR support), ``JsonStringKey``
22
+ uses JSON serialization to preserve Ruby types (Integer, Float, Boolean, Hash,
23
+ Array) across the Redis storage boundary. Registered as ``:json_string`` and
24
+ ``:json_stringkey``, enabling DSL methods like ``json_string :metadata`` and
25
+ ``class_json_string :last_synced_at``.
26
+
27
+ Changed
28
+ -------
29
+
30
+ - ``multi_index`` now defaults to ``within: :class`` instead of requiring a scope class. Existing instance-scoped indexes (``within: SomeClass``) continue to work unchanged.
31
+
32
+ Removed
33
+ -------
34
+
35
+ - **BREAKING**: Removed ``dump_method`` and ``load_method`` configuration options
36
+ from ``Familia::Base``, ``Familia::Horreum``, and ``Familia::DataType``. JSON
37
+ serialization via ``to_json``/``from_json`` is now hard-coded for consistency
38
+ and type safety. Custom serialization methods are no longer supported.
39
+
40
+ AI Assistance
41
+ -------------
42
+
43
+ - Claude Opus 4.5 assisted with design, implementation, and testing of serialization consistency, the JsonStringKey feature, and multi_index :class mode.
44
+ - Gemini 3 Flash assisted with editing and trimming this section.
45
+
46
+
10
47
  .. _changelog-2.0.0.pre24:
11
48
 
12
49
  2.0.0.pre24 — 2026-01-07
data/CLAUDE.md CHANGED
@@ -75,7 +75,7 @@ Add changelog fragment with each user-facing or documented change (optional but
75
75
 
76
76
  2. **`Familia::DataType`** - Base class for Valkey data type wrappers
77
77
  - Located in `lib/familia/data_type.rb`
78
- - Provides String, List, UnsortedSet, SortedSet, HashKey implementations
78
+ - Provides String, JsonStringKey, List, UnsortedSet, SortedSet, HashKey implementations
79
79
  - Each type has its own class in `lib/familia/data_type/types/`
80
80
 
81
81
  3. **`Familia::Base`** - Common module for both Horreum and DataType
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- familia (2.0.0.pre24)
4
+ familia (2.0.0.pre25)
5
5
  benchmark (~> 0.4)
6
6
  concurrent-ruby (~> 1.3)
7
7
  connection_pool (~> 2.5)
@@ -16,7 +16,8 @@ Indexing creates fast lookups for finding objects by field values:
16
16
  |------|-------|----------|-----------|
17
17
  | `unique_index` | Class | Global unique fields | Redis HashKey |
18
18
  | `unique_index` | Instance | Parent-scoped unique | Redis HashKey |
19
- | `multi_index` | Instance | Non-unique groupings | Redis Set |
19
+ | `multi_index` | Class (default) | Global non-unique groupings | Redis Set per value |
20
+ | `multi_index` | Instance | Parent-scoped groupings | Redis Set per value |
20
21
 
21
22
  ## Class-Level Unique Indexing
22
23
 
@@ -96,9 +97,80 @@ company2.find_by_badge_number('12345') # => emp2
96
97
  | `remove_from_company_badge_index(company)` | Remove from index |
97
98
  | `in_company_badge_index?(company)` | Check if indexed |
98
99
 
99
- ## Multi-Value Indexing
100
+ ## Class-Level Multi-Value Indexing
100
101
 
101
- One-to-many mappings for non-unique field values:
102
+ Class-level multi-value indexes group objects by field values at the class level. This is the default behavior when no `within:` parameter is specified.
103
+
104
+ ```ruby
105
+ class Customer < Familia::Horreum
106
+ feature :relationships
107
+ field :role
108
+
109
+ # Class-level multi_index (within: :class is the default)
110
+ multi_index :role, :role_index
111
+ end
112
+
113
+ # Create customers with various roles
114
+ alice = Customer.create(custid: 'cust_001', role: 'admin')
115
+ bob = Customer.create(custid: 'cust_002', role: 'user')
116
+ charlie = Customer.create(custid: 'cust_003', role: 'admin')
117
+
118
+ # Manually add to index (or use auto-indexing via save hooks)
119
+ alice.add_to_class_role_index
120
+ bob.add_to_class_role_index
121
+ charlie.add_to_class_role_index
122
+
123
+ # Query all customers with a specific role
124
+ admins = Customer.find_all_by_role('admin') # => [alice, charlie]
125
+ users = Customer.find_all_by_role('user') # => [bob]
126
+
127
+ # Random sampling
128
+ sample = Customer.sample_from_role('admin', 1) # => [random admin]
129
+ ```
130
+
131
+ ### Redis Key Pattern
132
+
133
+ Class-level multi-indexes use the pattern: `{classname}:{index_name}:{field_value}`
134
+
135
+ ```ruby
136
+ Customer.role_index_for('admin').dbkey # => "customer:role_index:admin"
137
+ Customer.role_index_for('user').dbkey # => "customer:role_index:user"
138
+ ```
139
+
140
+ ### Generated Class Methods
141
+
142
+ | Method | Description |
143
+ |--------|-------------|
144
+ | `Customer.role_index_for(value)` | Factory returning `Familia::UnsortedSet` for the field value |
145
+ | `Customer.find_all_by_role(value)` | Find all objects with that field value |
146
+ | `Customer.sample_from_role(value, count)` | Random sample of objects |
147
+ | `Customer.rebuild_role_index` | Rebuild the entire index from source data |
148
+
149
+ ### Generated Instance Methods
150
+
151
+ | Method | Description |
152
+ |--------|-------------|
153
+ | `customer.add_to_class_role_index` | Add this object to its field value's index |
154
+ | `customer.remove_from_class_role_index` | Remove this object from its field value's index |
155
+ | `customer.update_in_class_role_index(old_value)` | Move object from old index to new index |
156
+
157
+ ### Update Operations
158
+
159
+ When a field value changes, use the update method to atomically move the object between indexes:
160
+
161
+ ```ruby
162
+ old_role = customer.role
163
+ customer.role = 'superadmin'
164
+ customer.update_in_class_role_index(old_role)
165
+
166
+ # Customer is now in 'superadmin' index, removed from old 'admin' index
167
+ Customer.find_all_by_role('superadmin') # => includes customer
168
+ Customer.find_all_by_role('admin') # => no longer includes customer
169
+ ```
170
+
171
+ ## Instance-Scoped Multi-Value Indexing
172
+
173
+ For indexes scoped to a parent object, use `within:` to specify the scope class. This allows the same field values across different parent contexts.
102
174
 
103
175
  ```ruby
104
176
  class Employee < Familia::Horreum
@@ -125,13 +197,22 @@ sales_team = company.find_all_by_department('sales') # => [emp3]
125
197
  sample = company.sample_from_department('engineering', 1) # => [random engineer]
126
198
  ```
127
199
 
128
- ### Generated Methods
200
+ ### Generated Methods (Instance-Scoped)
201
+
202
+ **On scope class (Company):**
203
+ | Method | Description |
204
+ |--------|-------------|
205
+ | `company.dept_index_for(value)` | Factory returning UnsortedSet for value |
206
+ | `company.find_all_by_department(dept)` | Find all in department |
207
+ | `company.sample_from_department(dept, count)` | Random sample |
208
+ | `company.rebuild_dept_index` | Rebuild index from participation |
129
209
 
130
- **On scope class:**
210
+ **On indexed class (Employee):**
131
211
  | Method | Description |
132
212
  |--------|-------------|
133
- | `find_all_by_department(dept)` | Find all in department |
134
- | `sample_from_department(dept, count)` | Random sample |
213
+ | `employee.add_to_company_dept_index(company)` | Add to company's index |
214
+ | `employee.remove_from_company_dept_index(company)` | Remove from index |
215
+ | `employee.update_in_company_dept_index(company, old_dept)` | Move between indexes |
135
216
 
136
217
  ## Advanced Patterns
137
218
 
@@ -189,18 +270,30 @@ todays_events = user.find_all_by_daily_partition(today)
189
270
 
190
271
  ### Class vs Instance Scoping
191
272
 
192
- **Class-level (`unique_index :email, :email_lookup`):**
273
+ **Class-level unique (`unique_index :email, :email_lookup`):**
193
274
  - Automatic indexing on save/destroy
194
275
  - System-wide uniqueness
195
276
  - No parent context needed
196
277
  - Examples: emails, usernames, API keys
197
278
 
279
+ **Class-level multi (`multi_index :role, :role_index`):**
280
+ - Default behavior (no `within:` needed)
281
+ - Groups all objects by field value at class level
282
+ - Manual indexing via instance methods
283
+ - Examples: roles, categories, statuses
284
+
198
285
  **Instance-scoped (`unique_index :badge, :badge_index, within: Company`):**
199
286
  - Manual indexing required
200
287
  - Unique within parent only
201
288
  - Requires parent context
202
289
  - Examples: employee IDs, project names per team
203
290
 
291
+ **Instance-scoped multi (`multi_index :dept, :dept_index, within: Company`):**
292
+ - Groups objects by field value within parent scope
293
+ - Same field value allowed across different parents
294
+ - Manual indexing with parent context
295
+ - Examples: departments per company, tags per project
296
+
204
297
  ### Unique vs Multi Indexing
205
298
 
206
299
  **Unique index (`unique_index`):**
@@ -212,6 +305,7 @@ todays_events = user.find_all_by_daily_partition(today)
212
305
  - 1:many field-to-objects mapping
213
306
  - Returns array of objects
214
307
  - Allows duplicate values
308
+ - Default: class-level scope (use `within:` for instance scope)
215
309
 
216
310
  ## Rebuilding Indexes
217
311
 
@@ -277,8 +371,9 @@ end
277
371
  | Type | Pattern | Example |
278
372
  |------|---------|---------|
279
373
  | Class unique | `{class}:{index_name}` | `user:email_lookup` |
374
+ | Class multi | `{class}:{index_name}:{value}` | `customer:role_index:admin` |
280
375
  | Instance unique | `{scope}:{id}:{index_name}` | `company:123:badge_index` |
281
- | Multi-value | `{scope}:{id}:{index_name}:{value}` | `company:123:dept_index:engineering` |
376
+ | Instance multi | `{scope}:{id}:{index_name}:{value}` | `company:123:dept_index:engineering` |
282
377
 
283
378
  ## Troubleshooting
284
379
 
@@ -134,7 +134,30 @@ end
134
134
  | `employee.remove_from_company_badge_index(company)` | Remove from index |
135
135
  | `employee.in_company_badge_index?(company)` | Check if indexed |
136
136
 
137
- ### Multi-Value Index
137
+ ### Class-Level Multi-Value Index
138
+
139
+ ```ruby
140
+ class Customer < Familia::Horreum
141
+ multi_index :role, :role_index # within: :class is the default
142
+ end
143
+ ```
144
+
145
+ **Class methods on indexed class (Customer):**
146
+ | Method | Description |
147
+ |--------|-------------|
148
+ | `Customer.role_index_for(value)` | Factory returning UnsortedSet for value |
149
+ | `Customer.find_all_by_role(role)` | Find all with that role |
150
+ | `Customer.sample_from_role(role, count)` | Random sample |
151
+ | `Customer.rebuild_role_index` | Rebuild index from instances |
152
+
153
+ **Instance methods on indexed class (Customer):**
154
+ | Method | Description |
155
+ |--------|-------------|
156
+ | `customer.add_to_class_role_index` | Add to index |
157
+ | `customer.remove_from_class_role_index` | Remove from index |
158
+ | `customer.update_in_class_role_index(old_value)` | Move between indexes |
159
+
160
+ ### Instance-Scoped Multi-Value Index
138
161
 
139
162
  ```ruby
140
163
  class Employee < Familia::Horreum
@@ -145,14 +168,17 @@ end
145
168
  **Methods on scope class (Company):**
146
169
  | Method | Description |
147
170
  |--------|-------------|
171
+ | `company.dept_index_for(value)` | Factory returning UnsortedSet for value |
148
172
  | `company.find_all_by_department(dept)` | Find all in department |
149
173
  | `company.sample_from_department(dept, count)` | Random sample |
174
+ | `company.rebuild_dept_index` | Rebuild index from participation |
150
175
 
151
176
  **Methods on indexed class (Employee):**
152
177
  | Method | Description |
153
178
  |--------|-------------|
154
179
  | `employee.add_to_company_dept_index(company)` | Add to index |
155
180
  | `employee.remove_from_company_dept_index(company)` | Remove from index |
181
+ | `employee.update_in_company_dept_index(company, old_dept)` | Move between indexes |
156
182
 
157
183
  ## Method Naming Patterns
158
184
 
@@ -165,8 +191,9 @@ end
165
191
  ### Indexing
166
192
 
167
193
  - **Class unique**: `find_by_{field}`, `index_{field}_for`, `unindex_{field}_for`
194
+ - **Class multi**: `{Class}.find_all_by_{field}`, `{Class}.sample_from_{field}`, `{item}.add_to_class_{index}`
168
195
  - **Scoped unique**: `{scope}.find_by_{field}`, `{item}.add_to_{scope}_{index}`
169
- - **Multi-value**: `{scope}.find_all_by_{field}`, `{scope}.sample_from_{field}`
196
+ - **Scoped multi**: `{scope}.find_all_by_{field}`, `{scope}.sample_from_{field}`, `{item}.add_to_{scope}_{index}`
170
197
 
171
198
  ## Common Usage Examples
172
199
 
@@ -206,15 +233,20 @@ customer.domains.range(0, 9) # First 10
206
233
  ### Working with Indexes
207
234
 
208
235
  ```ruby
209
- # Automatic class-level indexing
236
+ # Automatic class-level unique indexing
210
237
  user = User.create(email: 'alice@example.com')
211
238
  User.find_by_email('alice@example.com') # => user
212
239
 
213
- # Manual scoped indexing
240
+ # Class-level multi-value indexing
241
+ customer.add_to_class_role_index
242
+ Customer.find_all_by_role('admin') # => [customer, ...]
243
+ Customer.sample_from_role('admin', 2) # => random sample
244
+
245
+ # Manual scoped unique indexing
214
246
  employee.add_to_company_badge_index(company)
215
247
  company.find_by_badge_number('12345') # => employee
216
248
 
217
- # Multi-value indexing
249
+ # Scoped multi-value indexing
218
250
  employee.add_to_company_dept_index(company)
219
251
  engineers = company.find_all_by_department('engineering')
220
252
  ```
data/docs/overview.md CHANGED
@@ -76,6 +76,10 @@ class Product < Familia::Horreum
76
76
  string :view_count, default: '0'
77
77
  # Usage: view_count.increment (atomic increment)
78
78
 
79
+ # JSON string fields (type-preserving storage)
80
+ json_string :last_synced_at, default: 0.0
81
+ # Usage: last_synced_at stores Float, retrieves Float (not String)
82
+
79
83
  # Lists (ordered, allows duplicates)
80
84
  list :categories
81
85
  # Usage: categories.push('fruit'), categories.pop
@@ -110,6 +114,10 @@ class Product < Familia::Horreum
110
114
  stringkey :description # Creates StringKey instance
111
115
  listkey :history # Creates ListKey instance
112
116
 
117
+ # JSON string (type-preserving alternative to StringKey)
118
+ json_string :metadata # Creates JsonStringKey instance
119
+ json_stringkey :config # Creates JsonStringKey instance
120
+
113
121
  # Both work identically - choose based on preference
114
122
  set :tags # UnsortedSet (unchanged)
115
123
  sorted_set :ratings # SortedSet (unchanged)
@@ -119,6 +127,7 @@ end
119
127
  # Access patterns are identical
120
128
  product.view_count.class # => Familia::StringKey
121
129
  product.description.class # => Familia::StringKey
130
+ product.metadata.class # => Familia::JsonStringKey
122
131
  product.categories.class # => Familia::ListKey
123
132
  product.history.class # => Familia::ListKey
124
133
  ```
data/lib/familia/base.rb CHANGED
@@ -29,7 +29,6 @@ module Familia
29
29
  #
30
30
  module ClassMethods
31
31
  attr_reader :features_available, :feature_definitions
32
- attr_accessor :dump_method, :load_method
33
32
 
34
33
  def add_feature(klass, feature_name, depends_on: [], field_group: nil)
35
34
  @features_available ||= {}
@@ -111,7 +110,6 @@ module Familia
111
110
  # Module-level methods for Familia::Base itself
112
111
  class << self
113
112
  attr_reader :features_available, :feature_definitions
114
- attr_accessor :dump_method, :load_method
115
113
 
116
114
  def add_feature(klass, feature_name, depends_on: [], field_group: nil)
117
115
  @features_available ||= {}
@@ -70,11 +70,10 @@ module Familia
70
70
  # @param values [Array<String>] The values to deserialize.
71
71
  # @return [Array<Object, nil>] Deserialized objects, including nil values.
72
72
  #
73
- # @raise [Familia::Problem] If the specified class doesn't respond to the
74
- # load method.
73
+ # @raise [Familia::Problem] If the specified class doesn't respond to from_json.
75
74
  #
76
75
  # @note This method attempts to deserialize each value using the specified
77
- # class's load method. If deserialization fails for a value, it's
76
+ # class's from_json method. If deserialization fails for a value, it's
78
77
  # replaced with nil.
79
78
  #
80
79
  def deserialize_values_with_nil(*values)
@@ -83,20 +82,20 @@ module Familia
83
82
 
84
83
  # If a class option is specified, use class-based deserialization
85
84
  if @opts[:class]
86
- unless @opts[:class].respond_to?(load_method)
87
- raise Familia::Problem, "No such method: #{@opts[:class]}##{load_method}"
85
+ unless @opts[:class].respond_to?(:from_json)
86
+ raise Familia::Problem, "No such method: #{@opts[:class]}.from_json"
88
87
  end
89
88
 
90
89
  values.collect! do |obj|
91
90
  next if obj.nil?
92
91
 
93
- val = @opts[:class].send load_method, obj
94
- Familia.debug "[#{self.class}#deserialize_values] nil returned for #{@opts[:class]}##{name}" if val.nil?
92
+ val = @opts[:class].from_json(obj)
93
+ Familia.debug "[#{self.class}#deserialize_values] nil returned for #{@opts[:class]}.from_json" if val.nil?
95
94
 
96
95
  val
97
96
  rescue StandardError => e
98
- Familia.info val
99
- Familia.info "Parse error for #{dbkey} (#{load_method}): #{e.message}"
97
+ Familia.info obj
98
+ Familia.info "Parse error for #{dbkey} (from_json): #{e.message}"
100
99
  Familia.info e.backtrace
101
100
  nil
102
101
  end
@@ -85,14 +85,6 @@ module Familia
85
85
  def uri=(value)
86
86
  @uri = value
87
87
  end
88
-
89
- def dump_method
90
- self.class.dump_method
91
- end
92
-
93
- def load_method
94
- self.class.load_method
95
- end
96
88
  end
97
89
  end
98
90
  end
@@ -0,0 +1,155 @@
1
+ # lib/familia/data_type/types/json_stringkey.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ module Familia
6
+ # JsonStringKey - A string DataType that uses JSON serialization for type preservation.
7
+ #
8
+ # Unlike StringKey which uses raw string serialization (to support Redis operations
9
+ # like INCR/DECR/APPEND), JsonStringKey uses the base DataType's JSON serialization
10
+ # to preserve Ruby types across the Redis storage boundary.
11
+ #
12
+ # @example Basic usage
13
+ # class MyIndex < Familia::Horreum
14
+ # class_json_string :last_synced_at, default: 0.0
15
+ # end
16
+ #
17
+ # MyIndex.last_synced_at = Time.now.to_f # Stored as JSON number
18
+ # MyIndex.last_synced_at #=> 1704067200.123 (Float preserved)
19
+ #
20
+ # @example Type preservation
21
+ # json_str.value = 42 # Stored in Redis as: 42 (JSON number)
22
+ # json_str.value = true # Stored in Redis as: true (JSON boolean)
23
+ # json_str.value = [1, 2, 3] # Stored in Redis as: [1,2,3] (JSON array)
24
+ #
25
+ # @note This class intentionally does NOT include increment/decrement or other
26
+ # raw string operations that are incompatible with JSON serialization.
27
+ #
28
+ # @note Performance: Each call to value, to_s, to_i, to_f makes a Redis
29
+ # roundtrip. If you need the value multiple times and don't expect it to
30
+ # change, store it in a local variable:
31
+ #
32
+ # # Inefficient (3 Redis calls):
33
+ # puts json_str.to_s
34
+ # puts json_str.to_i
35
+ # puts json_str.to_f
36
+ #
37
+ # # Efficient (1 Redis call):
38
+ # val = json_str.value
39
+ # puts val.to_s
40
+ # puts val.to_i
41
+ # puts val.to_f
42
+ #
43
+ class JsonStringKey < DataType
44
+ # Initialization hook (required by DataType contract)
45
+ def init; end
46
+
47
+ # Returns the number of characters in the string representation of the value.
48
+ #
49
+ # @return [Integer] number of characters
50
+ #
51
+ def char_count
52
+ to_s&.size || 0
53
+ end
54
+ alias size char_count
55
+ alias length char_count
56
+
57
+ # Returns the current value stored at the key.
58
+ #
59
+ # If a default option was provided during initialization, the default
60
+ # is set via SETNX (set if not exists) before retrieval.
61
+ #
62
+ # @return [Object] the deserialized value, or the default if not set
63
+ #
64
+ def value
65
+ echo :value, Familia.pretty_stack(limit: 1) if Familia.debug
66
+ if @opts.key?(:default)
67
+ was_set = dbclient.setnx(dbkey, serialize_value(@opts[:default]))
68
+ update_expiration if was_set
69
+ end
70
+ deserialize_value dbclient.get(dbkey)
71
+ end
72
+ alias content value
73
+ alias get value
74
+
75
+ # Sets the value at the key.
76
+ #
77
+ # The value is JSON-serialized before storage, preserving its Ruby type.
78
+ #
79
+ # @param val [Object] the value to store
80
+ # @return [String] "OK" on success
81
+ #
82
+ def value=(val)
83
+ ret = dbclient.set(dbkey, serialize_value(val))
84
+ update_expiration
85
+ ret
86
+ end
87
+ alias replace value=
88
+ alias set value=
89
+
90
+ # Sets the value only if the key does not already exist.
91
+ #
92
+ # @param val [Object] the value to store
93
+ # @return [Boolean] true if the key was set, false if it already existed
94
+ #
95
+ def setnx(val)
96
+ ret = dbclient.setnx(dbkey, serialize_value(val))
97
+ update_expiration if ret
98
+ ret
99
+ end
100
+
101
+ # Deletes the key from the database.
102
+ #
103
+ # @return [Boolean] true if the key was deleted, false if it didn't exist
104
+ #
105
+ def del
106
+ ret = dbclient.del dbkey
107
+ ret.positive?
108
+ end
109
+
110
+ # Checks if the value is nil (key does not exist or has no value).
111
+ #
112
+ # @return [Boolean] true if the value is nil
113
+ #
114
+ def empty?
115
+ value.nil?
116
+ end
117
+
118
+ # Returns the string representation of the deserialized value.
119
+ #
120
+ # @return [String, nil] the deserialized value converted to string, or nil
121
+ #
122
+ def to_s
123
+ val = deserialize_value(dbclient.get(dbkey))
124
+ return nil if val.nil?
125
+
126
+ val.to_s
127
+ end
128
+
129
+ # Returns the integer representation of the deserialized value.
130
+ #
131
+ # @return [Integer, nil] the deserialized value converted to integer, or nil
132
+ #
133
+ def to_i
134
+ val = deserialize_value(dbclient.get(dbkey))
135
+ return nil if val.nil?
136
+
137
+ val.to_i
138
+ end
139
+
140
+ # Returns the float representation of the deserialized value.
141
+ #
142
+ # @return [Float, nil] the deserialized value converted to float, or nil
143
+ #
144
+ def to_f
145
+ val = deserialize_value(dbclient.get(dbkey))
146
+ return nil if val.nil?
147
+
148
+ val.to_f
149
+ end
150
+
151
+ Familia::DataType.register self, :json_string
152
+ Familia::DataType.register self, :json_stringkey
153
+ Familia::DataType.register self, :jsonkey
154
+ end
155
+ end
@@ -14,7 +14,7 @@ module Familia
14
14
  # DataType - Base class for Database data type wrappers
15
15
  #
16
16
  # This class provides common functionality for various Database data types
17
- # such as String, List, UnsortedSet, SortedSet, and HashKey.
17
+ # such as String, JsonStringKey, List, UnsortedSet, SortedSet, and HashKey.
18
18
  #
19
19
  # @abstract Subclass and implement Database data type specific methods
20
20
  class DataType
@@ -41,9 +41,9 @@ module Familia
41
41
  #
42
42
  # Options:
43
43
  #
44
- # :class => A class that responds to Familia.load_method and
45
- # Familia.dump_method. These will be used when loading and
46
- # saving data from/to the database to unmarshal/marshal the class.
44
+ # :class => A class that responds to from_json. This will be used
45
+ # when loading data from the database to unmarshal the class.
46
+ # JSON serialization is used for all data storage.
47
47
  #
48
48
  # :parent => The Familia object that this datatype object belongs
49
49
  # to. This can be a class that includes Familia or an instance.
@@ -88,4 +88,5 @@ module Familia
88
88
  require_relative 'data_type/types/sorted_set'
89
89
  require_relative 'data_type/types/hashkey'
90
90
  require_relative 'data_type/types/stringkey'
91
+ require_relative 'data_type/types/json_stringkey'
91
92
  end