structured_store 0.1.0 → 1.0.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 +4 -4
- data/README.md +148 -25
- data/app/models/concerns/structured_store/storable.rb +125 -29
- data/app/validators/json_schema_validator.rb +16 -2
- data/lib/structured_store/ref_resolvers/base.rb +10 -11
- data/lib/structured_store/ref_resolvers/blank_ref_resolver.rb +51 -3
- data/lib/structured_store/ref_resolvers/definitions_resolver.rb +1 -5
- data/lib/structured_store/ref_resolvers/registry.rb +18 -3
- data/lib/structured_store/version.rb +1 -1
- metadata +6 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d346e5035fb3c5370c2507bc6e7ea91f120f1d757e45e8335ece92bbd9c266b7
|
|
4
|
+
data.tar.gz: abfeb9b199feb81a5d7629f146b206c475a84498e33780816ceedfb524a9296d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 227e320720ecf341eb778dd0cdc4a4c63d72fcb179c122b7e83b4c238c032447259f58d06cfd355eeaff9e55b75f9480cd465380d540ff6f2a75cc485c252c7c
|
|
7
|
+
data.tar.gz: 2ed007f16a85163a8231a361451f6f495d12fdcf9854e7e4a2121052be3c46cf36267a076f69a67d2d70723cdd267ebdd55c5c33cbf9672b14e17b6019fcee44
|
data/README.md
CHANGED
|
@@ -35,8 +35,8 @@ StructuredStore provides a robust way to manage JSON data with versioned schemas
|
|
|
35
35
|
After installation, create the necessary database tables:
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
rails generate structured_store:install
|
|
39
|
+
rails db:migrate
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
This creates a `structured_store_versioned_schemas` table and a `db/structured_store_versioned_schemas/` directory for your schema files.
|
|
@@ -90,17 +90,20 @@ end
|
|
|
90
90
|
Run the migration:
|
|
91
91
|
|
|
92
92
|
```bash
|
|
93
|
-
|
|
93
|
+
rails db:migrate
|
|
94
94
|
```
|
|
95
95
|
|
|
96
96
|
### 2. Creating a Model with Structured Storage
|
|
97
97
|
|
|
98
|
-
Create a model that includes the `StructuredStore::Storable` concern:
|
|
98
|
+
Create a model that includes the `StructuredStore::Storable` concern and explicitly configure the structured store column(s):
|
|
99
99
|
|
|
100
100
|
```ruby
|
|
101
101
|
# app/models/user_preference.rb
|
|
102
102
|
class UserPreference < ApplicationRecord
|
|
103
103
|
include StructuredStore::Storable
|
|
104
|
+
|
|
105
|
+
# Must explicitly configure structured store column(s)
|
|
106
|
+
structured_store :preferences
|
|
104
107
|
end
|
|
105
108
|
```
|
|
106
109
|
|
|
@@ -111,8 +114,8 @@ Generate and run a migration for your model:
|
|
|
111
114
|
class CreateUserPreferences < ActiveRecord::Migration[7.0]
|
|
112
115
|
def change
|
|
113
116
|
create_table :user_preferences do |t|
|
|
114
|
-
t.references :
|
|
115
|
-
t.json :
|
|
117
|
+
t.references :structured_store_preferences_versioned_schema, null: false, foreign_key: { to_table: :structured_store_versioned_schemas }
|
|
118
|
+
t.json :preferences
|
|
116
119
|
t.timestamps
|
|
117
120
|
end
|
|
118
121
|
end
|
|
@@ -121,7 +124,7 @@ end
|
|
|
121
124
|
|
|
122
125
|
### 3. Using the Structured Store
|
|
123
126
|
|
|
124
|
-
Once your model includes `StructuredStore::Storable
|
|
127
|
+
Once your model includes `StructuredStore::Storable` and configures structured stores, it automatically gets accessor methods for all properties defined in the associated JSON schema:
|
|
125
128
|
|
|
126
129
|
```ruby
|
|
127
130
|
# Find the latest version of your schema
|
|
@@ -129,7 +132,7 @@ schema = StructuredStore::VersionedSchema.latest("UserPreferences")
|
|
|
129
132
|
|
|
130
133
|
# Create a new record
|
|
131
134
|
preference = UserPreference.create!(
|
|
132
|
-
|
|
135
|
+
preferences_versioned_schema: schema,
|
|
133
136
|
theme: "dark",
|
|
134
137
|
notifications: false,
|
|
135
138
|
language: "es"
|
|
@@ -143,8 +146,23 @@ preference.language # => "es"
|
|
|
143
146
|
# Update structured data
|
|
144
147
|
preference.update!(theme: "light", notifications: true)
|
|
145
148
|
|
|
146
|
-
# The data is stored in the JSON `
|
|
147
|
-
preference.
|
|
149
|
+
# The data is stored in the JSON `preferences` column
|
|
150
|
+
preference.preferences # => {"theme"=>"light", "notifications"=>true, "language"=>"es"}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
If you chose to alter the migration to use a column type other than `json` or `jsonb`, you will need to amend your model to define the store and JSON serialiser (aka coder):
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
# app/models/user_preference.rb
|
|
157
|
+
class UserPreference < ApplicationRecord
|
|
158
|
+
include StructuredStore::Storable
|
|
159
|
+
|
|
160
|
+
# Declare the ActiveRecord::Store and coder for unstructured database data types
|
|
161
|
+
store :preferences, coder: JSON
|
|
162
|
+
|
|
163
|
+
# Declare that the structured store using the unstructured preferences column
|
|
164
|
+
structured_store :preferences
|
|
165
|
+
end
|
|
148
166
|
```
|
|
149
167
|
|
|
150
168
|
### 4. Schema Versioning
|
|
@@ -164,7 +182,7 @@ Create `db/structured_store_versioned_schemas/UserPreferences-1.1.0.json`:
|
|
|
164
182
|
"examples": ["light", "dark", "system"]
|
|
165
183
|
},
|
|
166
184
|
"notifications": {
|
|
167
|
-
"type": "boolean",
|
|
185
|
+
"type": "boolean",
|
|
168
186
|
"description": "Whether user notifications are enabled"
|
|
169
187
|
},
|
|
170
188
|
"language": {
|
|
@@ -201,7 +219,7 @@ latest_schema = StructuredStore::VersionedSchema.latest("UserPreferences")
|
|
|
201
219
|
|
|
202
220
|
# Create a record with the new schema
|
|
203
221
|
new_preference = UserPreference.create!(
|
|
204
|
-
|
|
222
|
+
preferences_versioned_schema: latest_schema,
|
|
205
223
|
theme: "system",
|
|
206
224
|
timezone: "America/New_York"
|
|
207
225
|
)
|
|
@@ -214,7 +232,7 @@ new_preference.timezone # => "America/New_York"
|
|
|
214
232
|
All data is automatically validated against the associated JSON schema:
|
|
215
233
|
|
|
216
234
|
```ruby
|
|
217
|
-
preference = UserPreference.new(
|
|
235
|
+
preference = UserPreference.new(preferences_versioned_schema: schema)
|
|
218
236
|
|
|
219
237
|
# This will fail validation because 'theme' is required
|
|
220
238
|
preference.valid? # => false
|
|
@@ -227,7 +245,85 @@ preference.valid? # => true
|
|
|
227
245
|
preference.notifications = "invalid" # Will cause validation error
|
|
228
246
|
```
|
|
229
247
|
|
|
230
|
-
### 6. Working with
|
|
248
|
+
### 6. Working with Array Properties
|
|
249
|
+
|
|
250
|
+
StructuredStore supports JSON schema properties with `type: "array"` for both arrays with `$ref` items and arrays with direct type items.
|
|
251
|
+
|
|
252
|
+
#### Arrays with Direct Type Items
|
|
253
|
+
|
|
254
|
+
```json
|
|
255
|
+
{
|
|
256
|
+
"$schema": "https://json-schema.org/draft/2019-09/schema",
|
|
257
|
+
"type": "object",
|
|
258
|
+
"properties": {
|
|
259
|
+
"tags": {
|
|
260
|
+
"type": "array",
|
|
261
|
+
"items": {
|
|
262
|
+
"type": "string"
|
|
263
|
+
},
|
|
264
|
+
"description": "List of tags"
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
```ruby
|
|
271
|
+
schema = StructuredStore::VersionedSchema.create!(
|
|
272
|
+
name: 'BlogPost',
|
|
273
|
+
version: '1.0.0',
|
|
274
|
+
json_schema: schema
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
post = BlogPost.new(data_versioned_schema: schema)
|
|
278
|
+
post.tags = ['ruby', 'rails', 'testing']
|
|
279
|
+
post.save!
|
|
280
|
+
|
|
281
|
+
post.tags # => ['ruby', 'rails', 'testing']
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
#### Arrays with $ref Items
|
|
285
|
+
|
|
286
|
+
```json
|
|
287
|
+
{
|
|
288
|
+
"$schema": "https://json-schema.org/draft/2019-09/schema",
|
|
289
|
+
"type": "object",
|
|
290
|
+
"definitions": {
|
|
291
|
+
"status_type": {
|
|
292
|
+
"type": "string",
|
|
293
|
+
"enum": ["pending", "active", "completed"]
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
"properties": {
|
|
297
|
+
"statuses": {
|
|
298
|
+
"type": "array",
|
|
299
|
+
"items": {
|
|
300
|
+
"$ref": "#/definitions/status_type"
|
|
301
|
+
},
|
|
302
|
+
"description": "List of statuses"
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
```ruby
|
|
309
|
+
schema = StructuredStore::VersionedSchema.create!(
|
|
310
|
+
name: 'Workflow',
|
|
311
|
+
version: '1.0.0',
|
|
312
|
+
json_schema: schema
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
workflow = Workflow.new(data_versioned_schema: schema)
|
|
316
|
+
workflow.statuses = ['pending', 'active']
|
|
317
|
+
workflow.save!
|
|
318
|
+
|
|
319
|
+
workflow.statuses # => ['pending', 'active']
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
**Supported item types:** `string`, `integer`, `boolean`
|
|
323
|
+
|
|
324
|
+
**Note:** Arrays with `object` or other complex item types are not currently supported.
|
|
325
|
+
|
|
326
|
+
### 7. Working with Complex Data Types
|
|
231
327
|
|
|
232
328
|
StructuredStore includes a `JsonDateRangeResolver` for handling date ranges through JSON schema references. This resolver works with date range converters to transform natural language date strings into structured date ranges.
|
|
233
329
|
|
|
@@ -255,7 +351,9 @@ require 'structured_store/ref_resolvers/json_date_range_resolver'
|
|
|
255
351
|
|
|
256
352
|
class EventRecord < ApplicationRecord
|
|
257
353
|
include StructuredStore::Storable
|
|
258
|
-
|
|
354
|
+
|
|
355
|
+
structured_store :event_data
|
|
356
|
+
|
|
259
357
|
def date_range_converter
|
|
260
358
|
@date_range_converter ||= StructuredStore::Converters::ChronicDateRangeConverter.new
|
|
261
359
|
end
|
|
@@ -269,12 +367,12 @@ If you choose to use the `ChronicDateRangeConverter`, you will also need to add
|
|
|
269
367
|
```ruby
|
|
270
368
|
# Create a record with a natural language date range
|
|
271
369
|
event = EventRecord.create!(
|
|
272
|
-
|
|
370
|
+
event_data_versioned_schema: schema,
|
|
273
371
|
event_period: "January 2024"
|
|
274
372
|
)
|
|
275
373
|
|
|
276
374
|
# The converter transforms this to structured data internally
|
|
277
|
-
event.
|
|
375
|
+
event.event_data['event_period']
|
|
278
376
|
# => {"date1"=>"2024-01-01 00:00:00", "date2"=>"2024-01-31 00:00:00"}
|
|
279
377
|
|
|
280
378
|
# When accessed, it's converted back to the natural language format
|
|
@@ -295,7 +393,7 @@ class CustomDateRangeConverter
|
|
|
295
393
|
# Your custom logic to parse date ranges
|
|
296
394
|
# Should return [start_date, end_date]
|
|
297
395
|
end
|
|
298
|
-
|
|
396
|
+
|
|
299
397
|
def convert_to_string(date1, date2)
|
|
300
398
|
# Your custom logic to format date ranges
|
|
301
399
|
# Should return a string representation
|
|
@@ -304,14 +402,16 @@ end
|
|
|
304
402
|
|
|
305
403
|
class MyModel < ApplicationRecord
|
|
306
404
|
include StructuredStore::Storable
|
|
307
|
-
|
|
405
|
+
|
|
406
|
+
structured_store :data
|
|
407
|
+
|
|
308
408
|
def date_range_converter
|
|
309
409
|
@date_range_converter ||= CustomDateRangeConverter.new
|
|
310
410
|
end
|
|
311
411
|
end
|
|
312
412
|
```
|
|
313
413
|
|
|
314
|
-
###
|
|
414
|
+
### 8. Finding and Querying Schemas
|
|
315
415
|
|
|
316
416
|
```ruby
|
|
317
417
|
# Find the latest version of a schema
|
|
@@ -325,27 +425,50 @@ all_versions = StructuredStore::VersionedSchema.where(name: "UserPreferences")
|
|
|
325
425
|
.order(:version)
|
|
326
426
|
```
|
|
327
427
|
|
|
328
|
-
###
|
|
428
|
+
### 9. Configurable Store Columns
|
|
429
|
+
|
|
430
|
+
StructuredStore supports configurable store columns, allowing you to use alternative column names for a single store (e.g., `depot` instead of `store`) or configure multiple store columns within the same model. This enables you to organize different types of structured data separately while maintaining proper schema versioning.
|
|
431
|
+
|
|
432
|
+
For detailed information on configuring single custom stores and multiple stores, see the [Custom Stores documentation](docs/custom_stores.md).
|
|
433
|
+
|
|
434
|
+
### 10. Advanced Usage: Custom Reference Resolvers
|
|
329
435
|
|
|
330
436
|
StructuredStore includes a resolver system for handling JSON schema references. You can create custom resolvers by extending `StructuredStore::RefResolvers::Base`:
|
|
331
437
|
|
|
332
438
|
```ruby
|
|
333
439
|
class CustomResolver < StructuredStore::RefResolvers::Base
|
|
334
440
|
def self.matching_ref_pattern
|
|
335
|
-
|
|
441
|
+
/^external:\/\/my_custom_type\//
|
|
336
442
|
end
|
|
337
443
|
|
|
338
444
|
def define_attribute
|
|
339
|
-
|
|
340
|
-
|
|
445
|
+
# Access property_schema to get the property's JSON schema
|
|
446
|
+
type = property_schema['type']
|
|
447
|
+
|
|
448
|
+
lambda do |object|
|
|
449
|
+
# Define custom attribute behavior on the object
|
|
450
|
+
object.singleton_class.attribute(property_name, type.to_sym)
|
|
341
451
|
end
|
|
342
452
|
end
|
|
453
|
+
|
|
454
|
+
def options_array
|
|
455
|
+
# Return array of [value, label] pairs for form selects
|
|
456
|
+
# Access parent_schema.definition_schema(name) if you need to look up definitions
|
|
457
|
+
[]
|
|
458
|
+
end
|
|
343
459
|
end
|
|
344
460
|
|
|
345
461
|
# Register your custom resolver
|
|
346
462
|
CustomResolver.register
|
|
347
463
|
```
|
|
348
464
|
|
|
465
|
+
**Available instance variables in your resolver:**
|
|
466
|
+
- `property_schema` - The property's JSON schema hash
|
|
467
|
+
- `parent_schema` - The parent SchemaInspector for looking up definitions
|
|
468
|
+
- `property_name` - The property name (for error messages)
|
|
469
|
+
- `ref_string` - The `$ref` value
|
|
470
|
+
- `context` - Additional context hash
|
|
471
|
+
|
|
349
472
|
### Best Practices
|
|
350
473
|
|
|
351
474
|
1. **Version your schemas semantically**: Use semantic versioning (e.g., "1.0.0", "1.1.0", "2.0.0") to track schema changes.
|
|
@@ -370,7 +493,7 @@ CustomResolver.register
|
|
|
370
493
|
|
|
371
494
|
## Contributing
|
|
372
495
|
|
|
373
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/HealthDataInsight/structured_store
|
|
496
|
+
Bug reports and pull requests are welcome on GitHub at <https://github.com/HealthDataInsight/structured_store>. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/HealthDataInsight/structured_store/blob/main/CODE_OF_CONDUCT.md).
|
|
374
497
|
|
|
375
498
|
## License
|
|
376
499
|
|
|
@@ -1,34 +1,101 @@
|
|
|
1
1
|
module StructuredStore
|
|
2
2
|
# This module is included in models that need to be stored in a structured way.
|
|
3
3
|
# It provides the necessary methods and attributes for structured storage.
|
|
4
|
-
#
|
|
5
|
-
#
|
|
4
|
+
#
|
|
5
|
+
# To use this module, include it in your model and call `structured_store` for each
|
|
6
|
+
# store column you want to configure. Each call will create a belongs_to association
|
|
7
|
+
# to a VersionedSchema and define helper methods for accessing the JSON schema.
|
|
8
|
+
#
|
|
9
|
+
# @example Basic usage
|
|
10
|
+
# class User < ApplicationRecord
|
|
11
|
+
# include StructuredStore::Storable
|
|
12
|
+
#
|
|
13
|
+
# structured_store :preferences
|
|
14
|
+
# structured_store :metadata
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# @example Custom schema name
|
|
18
|
+
# class Product < ApplicationRecord
|
|
19
|
+
# include StructuredStore::Storable
|
|
20
|
+
#
|
|
21
|
+
# structured_store :configuration, schema_name: 'product_config_schema'
|
|
22
|
+
# end
|
|
6
23
|
module Storable
|
|
7
24
|
extend ActiveSupport::Concern
|
|
8
25
|
|
|
9
26
|
included do
|
|
10
|
-
after_initialize :
|
|
27
|
+
after_initialize :define_all_store_accessors!
|
|
11
28
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
29
|
+
class_attribute :_structured_store_configurations, default: []
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class_methods do
|
|
33
|
+
# Configures a structured store column and creates the necessary associations.
|
|
34
|
+
#
|
|
35
|
+
# This method must be called explicitly for each store column you want to use.
|
|
36
|
+
# It will:
|
|
37
|
+
# - Add the column configuration to the internal tracking
|
|
38
|
+
# - Create a belongs_to association to StructuredStore::VersionedSchema
|
|
39
|
+
# - Define a helper method to access the JSON schema for this store
|
|
40
|
+
#
|
|
41
|
+
# @param column_name [String, Symbol] The name of the store column in your model
|
|
42
|
+
# @param schema_name [String, Symbol, nil] Optional custom name for the schema association
|
|
43
|
+
# If not provided, defaults to "#{column_name}_versioned_schema"
|
|
44
|
+
#
|
|
45
|
+
# @example Simple store configuration
|
|
46
|
+
# structured_store :preferences
|
|
47
|
+
# # Creates: belongs_to :preferences_versioned_schema
|
|
48
|
+
# # Helper method: preferences_json_schema
|
|
49
|
+
#
|
|
50
|
+
# @example Custom schema name
|
|
51
|
+
# structured_store :config, schema_name: 'product_configuration'
|
|
52
|
+
# # Creates: belongs_to :product_configuration
|
|
53
|
+
# # Helper method: product_configuration_json_schema
|
|
54
|
+
def structured_store(column_name, schema_name: nil)
|
|
55
|
+
column_name = column_name.to_s
|
|
56
|
+
schema_name ||= "#{column_name}_versioned_schema"
|
|
57
|
+
schema_name = schema_name.to_s
|
|
58
|
+
|
|
59
|
+
# Add configuration for this column
|
|
60
|
+
self._structured_store_configurations = _structured_store_configurations + [{
|
|
61
|
+
column_name: column_name,
|
|
62
|
+
schema_name: schema_name
|
|
63
|
+
}]
|
|
64
|
+
|
|
65
|
+
# Define the belongs_to association immediately
|
|
66
|
+
belongs_to schema_name.to_sym, # rubocop:disable Rails/InverseOf
|
|
67
|
+
class_name: 'StructuredStore::VersionedSchema',
|
|
68
|
+
foreign_key: "structured_store_#{schema_name}_id"
|
|
69
|
+
|
|
70
|
+
# Define helper method to get schema for this specific store
|
|
71
|
+
define_method "#{column_name}_json_schema" do
|
|
72
|
+
send(schema_name)&.json_schema
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
15
76
|
|
|
16
|
-
|
|
77
|
+
# Define accessors for all configured store columns
|
|
78
|
+
def define_all_store_accessors!
|
|
79
|
+
_structured_store_configurations.each do |config|
|
|
80
|
+
define_store_accessors_for_column(config[:column_name])
|
|
81
|
+
end
|
|
17
82
|
end
|
|
18
83
|
|
|
19
84
|
# Dynamically define accessors for the properties defined in the
|
|
20
|
-
# JSON schema
|
|
85
|
+
# JSON schema for this specific store column.
|
|
21
86
|
#
|
|
22
87
|
# This method is run automatically as an `after_initialize` callback, but can be called at
|
|
23
88
|
# any time for debugging and testing purposes.
|
|
24
89
|
#
|
|
25
90
|
# It skips defining the accessors if there is insufficient information to do so.
|
|
26
|
-
|
|
27
|
-
|
|
91
|
+
#
|
|
92
|
+
# @param column_name [String] The name of the store column
|
|
93
|
+
def define_store_accessors_for_column(column_name)
|
|
94
|
+
return unless sufficient_info_to_define_store_accessors?(column_name)
|
|
28
95
|
|
|
29
|
-
singleton_class.store_accessor(
|
|
96
|
+
singleton_class.store_accessor(column_name.to_sym, json_schema_properties(column_name).keys)
|
|
30
97
|
|
|
31
|
-
property_resolvers.each_value do |resolver|
|
|
98
|
+
property_resolvers(column_name).each_value do |resolver|
|
|
32
99
|
resolver.define_attribute.call(self)
|
|
33
100
|
end
|
|
34
101
|
end
|
|
@@ -37,42 +104,71 @@ module StructuredStore
|
|
|
37
104
|
# The resolvers are responsible for handling references and defining attributes
|
|
38
105
|
# for each property defined in the schema.
|
|
39
106
|
#
|
|
40
|
-
# @
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
107
|
+
# @param column_name [String] The name of the store column
|
|
108
|
+
# @return [Hash<String, StructuredStore::RefResolvers::Base>] Hash of resolver instances
|
|
109
|
+
def property_resolvers(column_name)
|
|
110
|
+
return {} if column_name.nil?
|
|
111
|
+
|
|
112
|
+
@property_resolvers ||= {}
|
|
113
|
+
@property_resolvers[column_name] ||= json_schema_properties(column_name).keys.index_with do |property_name|
|
|
114
|
+
StructuredStore::RefResolvers::Registry.matching_resolver(schema_inspector(column_name),
|
|
44
115
|
property_name)
|
|
45
116
|
end
|
|
46
117
|
end
|
|
47
118
|
|
|
48
119
|
private
|
|
49
120
|
|
|
50
|
-
# Returns a SchemaInspector instance for the
|
|
51
|
-
|
|
52
|
-
|
|
121
|
+
# Returns a SchemaInspector instance for the specified store column's JSON schema.
|
|
122
|
+
#
|
|
123
|
+
# @param column_name [String] The name of the store column
|
|
124
|
+
def schema_inspector(column_name)
|
|
125
|
+
return nil if column_name.nil?
|
|
126
|
+
|
|
127
|
+
@schema_inspectors ||= {}
|
|
128
|
+
@schema_inspectors[column_name] ||= StructuredStore::SchemaInspector.new(json_schema_for_column(column_name))
|
|
53
129
|
end
|
|
54
130
|
|
|
55
|
-
# Retrieves the properties from the JSON schema
|
|
131
|
+
# Retrieves the properties from the JSON schema for the specified store column
|
|
56
132
|
#
|
|
133
|
+
# @param column_name [String] The name of the store column
|
|
57
134
|
# @return [Hash] a hash containing the properties defined in the JSON schema,
|
|
58
135
|
# or an empty hash if no properties exist
|
|
59
|
-
def json_schema_properties
|
|
60
|
-
|
|
136
|
+
def json_schema_properties(column_name)
|
|
137
|
+
return {} if column_name.nil?
|
|
138
|
+
|
|
139
|
+
json_schema_for_column(column_name).fetch('properties', {})
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Gets the JSON schema for a specific store column
|
|
143
|
+
#
|
|
144
|
+
# @param column_name [String] The name of the store column
|
|
145
|
+
# @return [Hash] The JSON schema hash
|
|
146
|
+
def json_schema_for_column(column_name)
|
|
147
|
+
return {} if column_name.nil?
|
|
148
|
+
|
|
149
|
+
send("#{column_name}_json_schema") || {}
|
|
61
150
|
end
|
|
62
151
|
|
|
63
|
-
# Returns true if there is sufficient information to define accessors for
|
|
152
|
+
# Returns true if there is sufficient information to define accessors for the specified store column,
|
|
64
153
|
# and false otherwise.
|
|
65
154
|
#
|
|
66
155
|
# The JSON schema must be defined, containing property definitions.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
156
|
+
#
|
|
157
|
+
# @param column_name [String] The name of the store column
|
|
158
|
+
def sufficient_info_to_define_store_accessors?(column_name)
|
|
159
|
+
return false if column_name.nil?
|
|
160
|
+
|
|
161
|
+
schema = json_schema_for_column(column_name)
|
|
162
|
+
properties = json_schema_properties(column_name)
|
|
163
|
+
|
|
164
|
+
if schema.blank?
|
|
165
|
+
Rails.logger.info("This storable instance has no JSON schema for column '#{column_name}'")
|
|
70
166
|
return false
|
|
71
167
|
end
|
|
72
168
|
|
|
73
|
-
unless
|
|
74
|
-
Rails.logger.warn
|
|
75
|
-
"a valid 'properties' hash: #{
|
|
169
|
+
unless properties.is_a?(Hash)
|
|
170
|
+
Rails.logger.warn "The JSON schema for column '#{column_name}' does not contain " \
|
|
171
|
+
"a valid 'properties' hash: #{properties.inspect}"
|
|
76
172
|
return false
|
|
77
173
|
end
|
|
78
174
|
|
|
@@ -9,8 +9,8 @@ class JsonSchemaValidator < ActiveModel::EachValidator
|
|
|
9
9
|
# Convert value to hash if it's a string
|
|
10
10
|
json_data = value.is_a?(String) ? JSON.parse(value) : value
|
|
11
11
|
|
|
12
|
-
# Get the schema from options
|
|
13
|
-
schema =
|
|
12
|
+
# Get the schema from options, evaluating lambda if provided
|
|
13
|
+
schema = resolve_schema(record, attribute, value)
|
|
14
14
|
|
|
15
15
|
# Initialize JSONSchemer with proper handling based on schema type
|
|
16
16
|
schemer = json_schemer(schema)
|
|
@@ -28,6 +28,20 @@ class JsonSchemaValidator < ActiveModel::EachValidator
|
|
|
28
28
|
|
|
29
29
|
private
|
|
30
30
|
|
|
31
|
+
# Resolves the schema from options, evaluating lambda if provided.
|
|
32
|
+
#
|
|
33
|
+
# If the schema option is a lambda, it will be called with the record,
|
|
34
|
+
# attribute, and value as arguments.
|
|
35
|
+
def resolve_schema(record, attribute, value)
|
|
36
|
+
schema = options[:schema] || options
|
|
37
|
+
|
|
38
|
+
if schema.respond_to?(:call)
|
|
39
|
+
schema.call(record, attribute, value)
|
|
40
|
+
else
|
|
41
|
+
schema
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
31
45
|
# Converts given schema to a JSONSchemer::Schema object.
|
|
32
46
|
#
|
|
33
47
|
# Accepts either a symbol referencing a known schema (e.g. :draft7), a string
|
|
@@ -7,7 +7,8 @@ module StructuredStore
|
|
|
7
7
|
attr_reader :context,
|
|
8
8
|
:property_name,
|
|
9
9
|
:ref_string,
|
|
10
|
-
:
|
|
10
|
+
:property_schema,
|
|
11
|
+
:parent_schema
|
|
11
12
|
|
|
12
13
|
class << self
|
|
13
14
|
def matching_ref_pattern
|
|
@@ -25,10 +26,14 @@ module StructuredStore
|
|
|
25
26
|
|
|
26
27
|
# Initialize method for the base reference resolver
|
|
27
28
|
#
|
|
28
|
-
# @param
|
|
29
|
-
# @param
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
# @param property_schema [Hash] The property's JSON schema (with type, $ref, etc.)
|
|
30
|
+
# @param parent_schema [SchemaInspector] Parent schema for definition lookups
|
|
31
|
+
# @param property_name [String] Name of the property (for error messages)
|
|
32
|
+
# @param ref_string [String] The $ref string if present
|
|
33
|
+
# @param context [Hash] Additional context
|
|
34
|
+
def initialize(property_schema, parent_schema, property_name, ref_string = '', context = {})
|
|
35
|
+
@property_schema = property_schema
|
|
36
|
+
@parent_schema = parent_schema
|
|
32
37
|
@property_name = property_name
|
|
33
38
|
@ref_string = ref_string
|
|
34
39
|
@context = context
|
|
@@ -54,12 +59,6 @@ module StructuredStore
|
|
|
54
59
|
def options_array
|
|
55
60
|
raise NotImplementedError, 'Subclasses must implement the options_array method'
|
|
56
61
|
end
|
|
57
|
-
|
|
58
|
-
private
|
|
59
|
-
|
|
60
|
-
def json_property_schema
|
|
61
|
-
@json_property_schema ||= schema_inspector.property_schema(property_name) || {}
|
|
62
|
-
end
|
|
63
62
|
end
|
|
64
63
|
end
|
|
65
64
|
end
|
|
@@ -16,7 +16,10 @@ module StructuredStore
|
|
|
16
16
|
# @return [Proc] a lambda that defines the attribute on the singleton class
|
|
17
17
|
# @raise [RuntimeError] if the property type is unsupported
|
|
18
18
|
def define_attribute
|
|
19
|
-
type =
|
|
19
|
+
type = property_schema['type']
|
|
20
|
+
|
|
21
|
+
# Handle arrays
|
|
22
|
+
return define_array_attribute if type == 'array'
|
|
20
23
|
|
|
21
24
|
unless %w[boolean integer string].include?(type)
|
|
22
25
|
raise "Unsupported attribute type: #{type.inspect} for property '#{property_name}'"
|
|
@@ -31,11 +34,56 @@ module StructuredStore
|
|
|
31
34
|
# Returns a two dimensional array of options from the 'enum' property definition
|
|
32
35
|
# Each element contains a duplicate of the enum option for both the label and value
|
|
33
36
|
#
|
|
37
|
+
# For arrays, delegates to a resolver for the items to get options recursively
|
|
38
|
+
#
|
|
34
39
|
# @return [Array<Array>] Array of arrays containing id, value option pairs
|
|
35
40
|
def options_array
|
|
36
|
-
|
|
41
|
+
# For arrays, delegate to the items resolver
|
|
42
|
+
if property_schema['type'] == 'array'
|
|
43
|
+
items_resolver = create_items_resolver
|
|
44
|
+
return items_resolver.options_array
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# For non-arrays, get enum directly
|
|
48
|
+
enum = property_schema['enum']
|
|
49
|
+
enum&.map { |option| [option, option] } || []
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
# Defines an array attribute by delegating to the items resolver
|
|
55
|
+
#
|
|
56
|
+
# @return [Proc] a lambda that defines the array attribute
|
|
57
|
+
def define_array_attribute
|
|
58
|
+
items_resolver = create_items_resolver
|
|
59
|
+
|
|
60
|
+
# Get the item type - different resolvers expose this differently
|
|
61
|
+
item_type = if items_resolver.is_a?(DefinitionsResolver)
|
|
62
|
+
# DefinitionsResolver stores type in the resolved definition
|
|
63
|
+
items_resolver.send(:local_definition)['type']
|
|
64
|
+
else
|
|
65
|
+
# BlankRefResolver has it directly in property_schema
|
|
66
|
+
items_resolver.property_schema['type']
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
unless %w[boolean integer string].include?(item_type)
|
|
70
|
+
raise "Unsupported array item type: #{item_type.inspect} for property '#{property_name}'"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Return a no-op lambda as the serializer will handle casting to an array
|
|
74
|
+
->(_object) {}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Creates a resolver for array items by delegating to the registry
|
|
78
|
+
# This allows arrays to recursively use any resolver (BlankRefResolver, DefinitionsResolver, etc.)
|
|
79
|
+
#
|
|
80
|
+
# @return [Base] A resolver instance for the items
|
|
81
|
+
def create_items_resolver
|
|
82
|
+
items_schema = property_schema['items'] || {}
|
|
83
|
+
items_ref = items_schema['$ref'].to_s
|
|
37
84
|
|
|
38
|
-
|
|
85
|
+
# Use the registry to create a resolver for the items schema
|
|
86
|
+
Registry.resolver_for_schema_hash(items_schema, parent_schema, property_name, items_ref, context)
|
|
39
87
|
end
|
|
40
88
|
end
|
|
41
89
|
|
|
@@ -12,10 +12,6 @@ module StructuredStore
|
|
|
12
12
|
%r{\A#/definitions/}
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def initialize(schema, property_name, ref_string, context = {})
|
|
16
|
-
super
|
|
17
|
-
end
|
|
18
|
-
|
|
19
15
|
# Defines the rails attribute(s) on the given singleton class
|
|
20
16
|
#
|
|
21
17
|
# @return [Proc] a lambda that defines the attribute on the singleton class
|
|
@@ -53,7 +49,7 @@ module StructuredStore
|
|
|
53
49
|
# resolver.local_definition # => { "type" => "string" }
|
|
54
50
|
def local_definition
|
|
55
51
|
definition_name = ref_string.sub('#/definitions/', '')
|
|
56
|
-
local_definition =
|
|
52
|
+
local_definition = parent_schema.definition_schema(definition_name)
|
|
57
53
|
|
|
58
54
|
raise "No definition for #{ref_string}" if local_definition.nil?
|
|
59
55
|
|
|
@@ -31,15 +31,30 @@ module StructuredStore
|
|
|
31
31
|
|
|
32
32
|
# Returns a resolver instance for the given schema property reference
|
|
33
33
|
#
|
|
34
|
-
# @param [
|
|
34
|
+
# @param [SchemaInspector] schema_inspector The JSON schema inspector
|
|
35
35
|
# @param [String, Symbol] property_name The name of the property containing the reference
|
|
36
36
|
# @param [Hash] context Optional context hash (default: {})
|
|
37
37
|
# @return [RefResolver] An instance of the appropriate resolver class for the reference
|
|
38
38
|
# @raise [RuntimeError] If no matching resolver can be found for the reference
|
|
39
39
|
def matching_resolver(schema_inspector, property_name, context = {})
|
|
40
|
-
|
|
40
|
+
property_schema = schema_inspector.property_schema(property_name)
|
|
41
|
+
ref_string = property_schema['$ref'].to_s
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
resolver_for_schema_hash(property_schema, schema_inspector, property_name, ref_string, context)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Returns a resolver instance for a schema hash (e.g., array items)
|
|
47
|
+
# Direct method that doesn't require looking up properties
|
|
48
|
+
#
|
|
49
|
+
# @param [Hash] schema_hash The schema hash (with potential $ref)
|
|
50
|
+
# @param [SchemaInspector] parent_schema Parent schema for definition lookups
|
|
51
|
+
# @param [String] property_name The property name for error messages
|
|
52
|
+
# @param [String] ref_string The $ref string
|
|
53
|
+
# @param [Hash] context Optional context hash (default: {})
|
|
54
|
+
# @return [RefResolver] An instance of the appropriate resolver class
|
|
55
|
+
def resolver_for_schema_hash(schema_hash, parent_schema, property_name, ref_string, context = {})
|
|
56
|
+
klass = klass_factory(ref_string)
|
|
57
|
+
klass.new(schema_hash, parent_schema, property_name, ref_string, context)
|
|
43
58
|
end
|
|
44
59
|
|
|
45
60
|
private
|
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: structured_store
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tim Gentry
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: bin
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date: 2025-
|
|
11
|
+
date: 2025-11-12 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: json_schemer
|
|
@@ -88,6 +89,7 @@ metadata:
|
|
|
88
89
|
source_code_uri: https://github.com/HealthDataInsight/structured_store.git
|
|
89
90
|
changelog_uri: https://github.com/HealthDataInsight/structured_store.git/blob/main/CHANGELOG.md
|
|
90
91
|
rubygems_mfa_required: 'true'
|
|
92
|
+
post_install_message:
|
|
91
93
|
rdoc_options: []
|
|
92
94
|
require_paths:
|
|
93
95
|
- lib
|
|
@@ -102,7 +104,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
102
104
|
- !ruby/object:Gem::Version
|
|
103
105
|
version: '0'
|
|
104
106
|
requirements: []
|
|
105
|
-
rubygems_version: 3.
|
|
107
|
+
rubygems_version: 3.3.26
|
|
108
|
+
signing_key:
|
|
106
109
|
specification_version: 4
|
|
107
110
|
summary: Store JSON structured using versioned JSON Schemas.
|
|
108
111
|
test_files: []
|