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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5e104dd0bcd9069ad670b176be0ad3e56231f8a2a02b2237b9d76cd8e1504302
4
- data.tar.gz: b14d540d205c843a34355444f0893b7190c7df870d616c07357bb484e23105ad
3
+ metadata.gz: d346e5035fb3c5370c2507bc6e7ea91f120f1d757e45e8335ece92bbd9c266b7
4
+ data.tar.gz: abfeb9b199feb81a5d7629f146b206c475a84498e33780816ceedfb524a9296d
5
5
  SHA512:
6
- metadata.gz: f946faaa8dc5fa672e765f6a9b5261f499b12c164324e8df7144d3a821009f5dbdb4b3c572d1dc4903578ae20a5802f4f7d0f42d52fd7125a36e445e86357f45
7
- data.tar.gz: eae79479eff4abc7a24ac1966f76b24d6b4b8ede45e00d899136f93fec3f714ec4e4e9afe33362c386f9289db6b87dd46ca64c6fa98b54dd4b28788411773ff7
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
- $ rails generate structured_store:install
39
- $ rails db:migrate
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
- $ rails db:migrate
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 :structured_store_versioned_schema, null: false, foreign_key: true
115
- t.json :store
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`, it automatically gets accessor methods for all properties defined in the associated JSON schema:
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
- versioned_schema: schema,
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 `store` column
147
- preference.store # => {"theme"=>"light", "notifications"=>true, "language"=>"es"}
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
- versioned_schema: latest_schema,
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(versioned_schema: schema)
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 Complex Data Types
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
- versioned_schema: schema,
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.store['event_period']
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
- ### 7. Finding and Querying Schemas
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
- ### 8. Advanced Usage: Custom Reference Resolvers
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
- /^#\/custom\//
441
+ /^external:\/\/my_custom_type\//
336
442
  end
337
443
 
338
444
  def define_attribute
339
- lambda do |instance|
340
- # Define custom attribute behavior
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. 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).
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
- # The `storeable_attributes` method defines the attributes that will be stored.
5
- # The `to_s` method is overridden to return the name of the object or the default string representation.
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 :define_store_accessors!
27
+ after_initialize :define_all_store_accessors!
11
28
 
12
- belongs_to :versioned_schema, # rubocop:disable Rails/InverseOf
13
- class_name: 'StructuredStore::VersionedSchema',
14
- foreign_key: 'structured_store_versioned_schema_id'
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
- delegate :json_schema, to: :versioned_schema
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 that this record has.
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
- def define_store_accessors!
27
- return unless sufficient_info_to_define_store_accessors?
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(:store, json_schema_properties.keys)
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
- # @return [Array<StructuredStore::RefResolvers::Base>] Array of resolver instances
41
- def property_resolvers
42
- @property_resolvers ||= json_schema_properties.keys.index_with do |property_name|
43
- StructuredStore::RefResolvers::Registry.matching_resolver(schema_inspector,
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 current JSON schema.
51
- def schema_inspector
52
- @schema_inspector ||= StructuredStore::SchemaInspector.new(json_schema)
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
- json_schema.fetch('properties', {})
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 this audit_store,
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
- def sufficient_info_to_define_store_accessors?
68
- if json_schema.nil?
69
- Rails.logger.info('This storable instance has no JSON schema')
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 json_schema_properties.is_a?(Hash)
74
- Rails.logger.warn 'The JSON schema for this storable instance does not contain ' \
75
- "a valid 'properties' hash: #{json_schema_properties.inspect}"
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 = options[:schema] || options
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
- :schema_inspector
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 schema [Hash] JSON Schema for the resolver
29
- # @param options [Hash] Additional options for the resolver
30
- def initialize(schema_inspector, property_name, ref_string, context = {})
31
- @schema_inspector = schema_inspector
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 = json_property_schema['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
- enum = json_property_schema['enum']
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
- enum.map { |option| [option, option] }
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 = schema_inspector.definition_schema(definition_name)
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 [Hash] schema The JSON schema containing the property reference
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
- ref_string = schema_inspector.property_schema(property_name)['$ref']
40
+ property_schema = schema_inspector.property_schema(property_name)
41
+ ref_string = property_schema['$ref'].to_s
41
42
 
42
- klass_factory(ref_string).new(schema_inspector, property_name, ref_string, context)
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
@@ -1,3 +1,3 @@
1
1
  module StructuredStore
2
- VERSION = '0.1.0'.freeze
2
+ VERSION = '1.0.0'.freeze
3
3
  end
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: 0.1.0
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-07-01 00:00:00.000000000 Z
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.6.2
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: []