ai_guardrails 1.2.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.
data/README.md ADDED
@@ -0,0 +1,528 @@
1
+ # AiGuardrails
2
+
3
+ [![Gem Version](https://img.shields.io/gem/v/ai_guardrails.svg)](https://rubygems.org/gems/ai_guardrails)
4
+ [![Build Status](https://github.com/logicbunchhq/ai_guardrails/actions/workflows/ci.yml/badge.svg)](https://github.com/logicbunchhq/ai_guardrails/actions)
5
+ [![Coverage Status](https://coveralls.io/repos/github/logicbunchhq/ai_guardrails/badge.svg?branch=main)](https://coveralls.io/github/logicbunchhq/ai_guardrails?branch=main)
6
+
7
+ > **AiGuardrails** is a Ruby gem for validating, repairing, and securing AI-generated outputs.
8
+ > It ensures responses from LLMs (like OpenAI or Anthropic) are **valid, safe, and structured**.
9
+
10
+ ---
11
+
12
+ ## Table of Contents
13
+
14
+ 1. [Overview](#overview)
15
+ 2. [Installation](#installation)
16
+ 3. [Quick Start](#quick-start)
17
+ 4. [Features](#features)
18
+
19
+ * [Schema Validation](#schema-validation)
20
+ * [Automatic JSON Repair](#automatic-json-repair)
21
+ * [Unit Test Helpers / Mock Model Client](#unit-test-helpers--mock-model-client)
22
+ * [Provider-Agnostic API](#provider-agnostic-api)
23
+ * [Auto-Correction / Retry Layer](#auto-correction--retry-layer)
24
+ * [Safety & Content Filters](#safety--content-filters)
25
+ * [Easy DSL / Developer-Friendly API](#easy-dsl--developer-friendly-api)
26
+ * [Background Job / CLI Friendly](#background-job--cli-friendly)
27
+ * [Optional Caching](#optional-caching)
28
+ * [JSON + Schema Auto-Fix Hooks](#json--schema-auto-fix-hooks)
29
+ 5. [Error Handling](#error-handling)
30
+ 6. [Development](#development)
31
+ 7. [Contributing](#contributing)
32
+ 8. [License](#license)
33
+ 9. [Code of Conduct](#code-of-conduct)
34
+
35
+ ---
36
+
37
+ ## Overview
38
+
39
+ AI models often generate invalid JSON, inconsistent data types, or unsafe content.
40
+ **AiGuardrails** helps you handle all of this automatically, providing:
41
+
42
+ * ✅ JSON repair for malformed AI responses
43
+ * ✅ Schema validation for predictable structure
44
+ * ✅ Safety filters for blocked or harmful content
45
+ * ✅ Retry + correction for invalid responses
46
+ * ✅ Easy integration via a single `AiGuardrails.run` call
47
+
48
+ ---
49
+
50
+ ```mermaid
51
+ flowchart LR
52
+ A[AI Response] --> B[JSON Parse Attempt]
53
+ B -->|Valid| F[Return Output ✅]
54
+ B -->|Invalid| C[Auto-Fix Hook 🔧]
55
+ C --> D[Revalidate Schema]
56
+ D -->|Fixed| F
57
+ D -->|Still Invalid| E[Retry or Fallback 🚨]
58
+ E --> G[Raise AiGuardrails::SchemaError]
59
+ ```
60
+
61
+ ---
62
+
63
+ ## Installation
64
+
65
+ Add AiGuardrails to your project using Bundler:
66
+
67
+ ```bash
68
+ bundle add ai_guardrails
69
+ ```
70
+
71
+ Or install manually:
72
+
73
+ ```bash
74
+ gem install ai_guardrails
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Requirements
80
+
81
+ - Ruby >= 3.0.0
82
+ - Rails >= 6.0 (if using Rails integration)
83
+ - Optional dependencies:
84
+ - `ruby-openai` for OpenAI provider
85
+ - `ruby-anthropic` gem if using Anthropic provider
86
+
87
+ ---
88
+
89
+ ## Quick Start
90
+
91
+ ```ruby
92
+ require "ai_guardrails"
93
+
94
+ schema = { name: :string, price: :float }
95
+
96
+ # Optional: Helps AI return structured output matching the schema.
97
+ # Can be any type: String, Hash, etc.
98
+ schema_hint = schema
99
+
100
+ result = AiGuardrails::DSL.run(
101
+ prompt: "Generate a product JSON",
102
+ provider: :openai,
103
+ provider_config: { api_key: ENV["OPENAI_API_KEY"] },
104
+ schema: schema,
105
+ schema_hint: schema_hint
106
+ )
107
+
108
+ puts result
109
+ # => { "name" => "Laptop", "price" => 1200.0 }
110
+ ```
111
+
112
+ ---
113
+
114
+ ## Features
115
+
116
+ ### Schema Validation
117
+
118
+ Validate AI output against a Ruby schema.
119
+
120
+ ```ruby
121
+ require "ai_guardrails"
122
+
123
+ schema = { name: :string, price: :float, tags: [:string] }
124
+ validator = AiGuardrails::SchemaValidator.new(schema)
125
+
126
+ input = { name: "Laptop", price: 1200.0, tags: ["electronics", "sale"] }
127
+ success, result = validator.validate(input)
128
+
129
+ if success
130
+ puts "Valid output: #{result}"
131
+ else
132
+ puts "Validation errors: #{result}"
133
+ end
134
+ ```
135
+
136
+ #### Supported Types
137
+
138
+ | Type | Example |
139
+ | -------------------- | ---------------------- |
140
+ | `:string` | `"Laptop"` |
141
+ | `:integer` | `42` |
142
+ | `:float` | `19.99` |
143
+ | `:boolean` | `true` |
144
+ | `[:string]` | `["a", "b"]` |
145
+ | `[{ key: :string }]` | `[{"key" => "value"}]` |
146
+
147
+ #### Example Invalid Input
148
+
149
+ ```ruby
150
+ input = { name: 123, price: "abc", tags: ["electronics", 2] }
151
+ success, errors = validator.validate(input)
152
+ # => { name: ["must be a string"], price: ["must be a float"], tags: ["element 1 must be a string"] }
153
+ ```
154
+
155
+ ---
156
+
157
+ ### Automatic JSON Repair
158
+
159
+ LLMs often return invalid JSON.
160
+ `AiGuardrails::JsonRepair` automatically fixes common issues.
161
+
162
+ ```ruby
163
+ require "ai_guardrails"
164
+
165
+ raw_json = "{name: 'Laptop' price: 1200, tags: ['electronics' 'sale']}"
166
+ fixed = AiGuardrails::JsonRepair.repair(raw_json)
167
+
168
+ puts fixed
169
+ # => { "name" => "Laptop", "price" => 1200, "tags" => ["electronics", "sale"] }
170
+ ```
171
+
172
+ **What It Fixes**
173
+
174
+ * Missing quotes or commas
175
+ * Single → double quotes
176
+ * Trailing commas
177
+ * Unbalanced braces/brackets
178
+ * Nested arrays/objects without separators
179
+
180
+ **Unrepairable JSON Example**
181
+
182
+ ```ruby
183
+ begin
184
+ AiGuardrails::JsonRepair.repair("NOT JSON")
185
+ rescue AiGuardrails::JsonRepair::RepairError => e
186
+ puts "Could not repair JSON: #{e.message}"
187
+ end
188
+ ```
189
+
190
+ **Integration with Schema Validation**
191
+
192
+ ```ruby
193
+ schema = { name: :string, price: :float }
194
+ fixed = AiGuardrails::JsonRepair.repair("{name: 'Laptop', price: '1200'}")
195
+ validator = AiGuardrails::SchemaValidator.new(schema)
196
+ success, result = validator.validate(fixed)
197
+ ```
198
+
199
+ ---
200
+
201
+ ### Unit Test Helpers / Mock Model Client
202
+
203
+ Simulate AI model responses for testing without API calls.
204
+
205
+ ```ruby
206
+ mock_client = AiGuardrails::MockModelClient.new(
207
+ "Generate product" => '{"name": "Laptop", "price": 1200}'
208
+ )
209
+
210
+ response = mock_client.call(prompt: "Generate product")
211
+ puts response
212
+ # => '{"name": "Laptop", "price": 1200}'
213
+
214
+ mock_client.add_response("Generate user", '{"name": "Alice"}')
215
+ ```
216
+
217
+ **Simulate API Errors**
218
+
219
+ ```ruby
220
+ begin
221
+ mock_client.call(prompt: "error", raise_error: true)
222
+ rescue AiGuardrails::MockModelClient::MockError => e
223
+ puts e.message
224
+ end
225
+ ```
226
+
227
+ **Fallback Example**
228
+
229
+ ```ruby
230
+ response = mock_client.call(prompt: "error", default_fallback: "No mock response defined")
231
+ puts response
232
+ # => "No mock response defined"
233
+ ```
234
+
235
+ ---
236
+
237
+ ### Provider-Agnostic API
238
+
239
+ AiGuardrails supports **any LLM provider**, with dynamic loading and zero vendor dependencies.
240
+
241
+ | Provider | Gem | Status |
242
+ | -------------- | ---------------- | ----------- |
243
+ | OpenAI | `ruby-openai` | ✅ Supported |
244
+ | Anthropic | `ruby-anthropic` | 🔜 Planned |
245
+ | Google Gemini | `gemini-ai` | 🔜 Planned |
246
+ | Ollama (local) | `ollama-ai` | 🔜 Planned |
247
+
248
+ ```ruby
249
+ client = AiGuardrails::Provider::Factory.build(
250
+ provider: :openai,
251
+ config: { api_key: ENV["OPENAI_API_KEY"], model: "gpt-4o-mini" }
252
+ )
253
+
254
+ puts client.call_model(prompt: "Hello!")
255
+ ```
256
+
257
+ ---
258
+
259
+ ### Auto-Correction / Retry Layer
260
+
261
+ Automatically repairs and retries AI output until it passes schema validation.
262
+
263
+ ```ruby
264
+ schema = { name: :string, price: :float }
265
+
266
+ client = AiGuardrails::Provider::Factory.build(
267
+ provider: :openai,
268
+ config: { api_key: ENV["OPENAI_API_KEY"] }
269
+ )
270
+
271
+ auto = AiGuardrails::AutoCorrection.new(provider: client, schema: schema, max_retries: 3, sleep_time: 1)
272
+
273
+ result = auto.call(prompt: "Generate a product JSON", schema_hint: schema)
274
+ puts result
275
+ # => { "name" => "Laptop", "price" => 1200.0 }
276
+ ```
277
+
278
+ **Optional Parameters**
279
+
280
+ * `max_retries`: Maximum retry attempts
281
+ * `sleep_time`: Delay between retries
282
+ * `schema_hint`: Optional guide to help AI produce valid output
283
+ * Raises `AiGuardrails::AutoCorrection::RetryLimitReached` if retries exhausted
284
+
285
+ ---
286
+
287
+ ### Safety & Content Filters
288
+
289
+ Detect and block unsafe or prohibited content.
290
+
291
+ ```ruby
292
+ filter = AiGuardrails::SafetyFilter.new(blocklist: ["badword", /forbidden/i])
293
+
294
+ filter.safe?("clean text") # => true
295
+ filter.safe?("badword inside") # => false
296
+ ```
297
+
298
+ Raise exception on violation:
299
+
300
+ ```ruby
301
+ begin
302
+ filter.check!("This has badword")
303
+ rescue AiGuardrails::SafetyFilter::UnsafeContentError => e
304
+ puts e.message
305
+ end
306
+ ```
307
+
308
+ ---
309
+
310
+ ### Easy DSL / Developer-Friendly API
311
+
312
+ `AiGuardrails::DSL.run` is a single entry point combining:
313
+
314
+ * JSON repair
315
+ * Schema validation
316
+ * Auto-correction & retries
317
+ * Safety filters
318
+ * Logging & debugging
319
+ * Optional caching
320
+
321
+ ```ruby
322
+ result = AiGuardrails::DSL.run(
323
+ prompt: "Generate product",
324
+ schema: { name: :string, price: :float },
325
+ schema_hint: schema, # It could be any other data type eg, string, json etc
326
+ provider: :openai,
327
+ provider_config: { api_key: ENV["OPENAI_API_KEY"] },
328
+ blocklist: ["Forbidden"]
329
+ )
330
+
331
+ puts result
332
+ # => { "name" => "Laptop", "price" => 1200.0 }
333
+ ```
334
+
335
+ **Schema vs Schema Hint**
336
+
337
+ * `schema` (required): Full validation for final output.
338
+ * `schema_hint` (optional): Guides AI generation, can be a subset or modified version.
339
+
340
+ ```ruby
341
+ schema = { name: :string, price: :float, tags: [:string] }
342
+ hint = "JSON should contain name, price and tags"
343
+
344
+ result = AiGuardrails::DSL.run(
345
+ prompt: "Generate product JSON",
346
+ schema: schema,
347
+ schema_hint: hint,
348
+ provider_config: { api_key: ENV["OPENAI_API_KEY"] },
349
+ blocklist: ["Forbidden"],
350
+ max_retries: 3,
351
+ sleep_time: 1
352
+ )
353
+
354
+ puts result
355
+ # => { "name" => "Laptop", "price" => 1200.0, "tags" => ["electronics", "sale"] }
356
+ ```
357
+
358
+ ---
359
+
360
+ ### Background Job / CLI Friendly
361
+
362
+ Works seamlessly in Rails background jobs, Sidekiq, or standalone CLI scripts.
363
+
364
+ **Background Job Example**
365
+
366
+ ```ruby
367
+ require "logger"
368
+
369
+ logger = Logger.new($stdout)
370
+
371
+ AiGuardrails::BackgroundJob.perform(logger: logger, debug: true) do
372
+ AiGuardrails::DSL.run(
373
+ prompt: "Generate product",
374
+ schema: { name: :string, price: :float },
375
+ schema_hint: { name: :string, price: :float },
376
+ provider_config: { api_key: ENV["OPENAI_API_KEY"] }
377
+ )
378
+ end
379
+ ```
380
+
381
+ **CLI Example (Standalone Script or IRB)**
382
+ Use the CLI for running workflows outside of Rails or for local testing.
383
+
384
+ ```ruby
385
+ # If running as a standalone Ruby script, make sure the gem lib path is loaded:
386
+ $LOAD_PATH.unshift(File.expand_path("lib", __dir__))
387
+ require "ai_guardrails"
388
+
389
+ AiGuardrails::CLI.run(debug: true) do
390
+ result = AiGuardrails::DSL.run(
391
+ prompt: "Generate product",
392
+ schema: { name: :string, value: :integer },
393
+ schema_hint: { name: :string, value: :integer },
394
+ provider_config: { api_key: ENV["OPENAI_API_KEY"] }
395
+ )
396
+
397
+ puts result
398
+ end
399
+
400
+ ```
401
+ ---
402
+
403
+ **Note**: End-users of the gem in Rails projects do not need the CLI. It is primarily for gem developers or for running workflows outside a Rails app.
404
+
405
+ ### Optional Caching
406
+
407
+ Cache AI responses for repeated prompts to reduce cost and latency.
408
+
409
+ ```ruby
410
+ AiGuardrails::Cache.configure(enabled: true, store: Rails.cache, expires_in: 300)
411
+
412
+ schema = { name: :string, price: :float }
413
+
414
+ result1 = AiGuardrails::DSL.run(prompt: "Generate product", schema: schema, schema_hint: schema, provider_config: { api_key: ENV["OPENAI_API_KEY"] })
415
+ result2 = AiGuardrails::DSL.run(prompt: "Generate product", schema: schema, schema_hint: schema, provider_config: { api_key: ENV["OPENAI_API_KEY"] })
416
+
417
+ puts result1 == result2 # => true
418
+ ```
419
+
420
+ **Fetch Examples**
421
+
422
+ ```ruby
423
+ key = AiGuardrails::Cache.key("prompt", schema)
424
+
425
+ # Using default
426
+ value = AiGuardrails::Cache.fetch(key, "default_value")
427
+
428
+ # Using block
429
+ value = AiGuardrails::Cache.fetch(key) { "computed_result" }
430
+ ```
431
+
432
+ ---
433
+
434
+ ### JSON + Schema Auto-Fix Hooks
435
+
436
+ Automatically repair and coerce malformed JSON to match schema.
437
+
438
+ ```ruby
439
+ schema = { name: :string, price: :float, available: :boolean }
440
+ raw = '{"name": "Shirt", "price": "19.99", "available": "true"}'
441
+
442
+ fixed = AiGuardrails::AutoFix.fix(raw, schema: schema)
443
+ # => {"name"=>"Shirt", "price"=>19.99, "available"=>true}
444
+ ```
445
+
446
+ **Custom Hooks**
447
+
448
+ ```ruby
449
+ hooks = [proc { |h| h["price"] *= 2 }]
450
+ fixed = AiGuardrails::AutoFix.fix(raw, schema: schema, hooks: hooks)
451
+ puts fixed["price"] # => 39.98
452
+ ```
453
+
454
+ Hooks allow:
455
+
456
+ * Setting **default values**
457
+ * Transforming or normalizing data
458
+ * Custom calculations or aggregations
459
+ * Injecting metadata before final output
460
+
461
+ Hooks run after schema validation and JSON repair, ensuring safe, valid, and tailored output.
462
+
463
+ ---
464
+
465
+ ## Error Handling
466
+
467
+ | Exception | When It Occurs |
468
+ | ------------------------------------------------- | ----------------------------------------------- |
469
+ | `AiGuardrails::JsonRepair::RepairError` | Cannot repair invalid JSON input |
470
+ | `AiGuardrails::AutoCorrection::RetryLimitReached` | Maximum retries reached without valid output |
471
+ | `AiGuardrails::SafetyFilter::UnsafeContentError` | Blocked or unsafe content detected in AI output |
472
+
473
+ Example:
474
+
475
+ ```ruby
476
+ begin
477
+ result = AiGuardrails::DSL.run(prompt: "Generate product", schema: schema)
478
+ rescue AiGuardrails::AutoCorrection::RetryLimitReached => e
479
+ puts "Retries exceeded: #{e.message}"
480
+ rescue AiGuardrails::SafetyFilter::UnsafeContentError => e
481
+ puts "Blocked content detected: #{e.message}"
482
+ end
483
+ ```
484
+
485
+ ---
486
+
487
+ ## Development
488
+
489
+ Install dependencies:
490
+
491
+ ```bash
492
+ bin/setup
493
+ rake spec
494
+ ```
495
+
496
+ Interactive console:
497
+
498
+ ```bash
499
+ bin/console
500
+ ```
501
+
502
+ Release a new version:
503
+
504
+ ```bash
505
+ bundle exec rake release
506
+ ```
507
+
508
+ ---
509
+
510
+ ## Contributing
511
+
512
+ Bug reports and pull requests are welcome at:
513
+ 👉 [https://github.com/logicbunchhq/ai_guardrails](https://github.com/logicbunchhq/ai_guardrails)
514
+
515
+ This project follows the [Contributor Covenant](https://www.contributor-covenant.org/).
516
+
517
+ ---
518
+
519
+ ## License
520
+
521
+ Released under the [MIT License](https://opensource.org/licenses/MIT).
522
+
523
+ ---
524
+
525
+ ## Code of Conduct
526
+
527
+ Everyone interacting with AiGuardrails project is expected to follow the
528
+ [Code of Conduct](https://github.com/logicbunchhq/ai_guardrails/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AiGuardrails
4
+ # AutoCorrection handles retries, JSON repair, and schema validation
5
+ class AutoCorrection
6
+ class RetryLimitReached < StandardError; end
7
+
8
+ # Initialize with a provider client and schema
9
+ # options:
10
+ # max_retries: number of attempts (default: 3)
11
+ # sleep_time: seconds to wait between retries (default: 0)
12
+ def initialize(provider:, schema:, max_retries: 3, sleep_time: 0)
13
+ @provider = provider
14
+ @validator = SchemaValidator.new(schema)
15
+ @max_retries = max_retries
16
+ @sleep_time = sleep_time
17
+ end
18
+
19
+ # Call the AI model with prompt
20
+ # Returns validated hash (symbolized keys and correct types)
21
+ # rubocop:disable Metrics/MethodLength
22
+ def call(prompt:, schema_hint: nil)
23
+ attempts = 0
24
+
25
+ # Append schema hint to prompt if provided
26
+ prompt = prepare_prompt(prompt, schema_hint)
27
+
28
+ loop do
29
+ attempts += 1
30
+
31
+ # Call AI provider
32
+ raw_output = @provider.call_model(prompt: prompt)
33
+
34
+ # Repair JSON if needed
35
+ input_for_validation = parse_and_repair(raw_output)
36
+
37
+ # Validate against schema
38
+ valid, result = @validator.validate(input_for_validation)
39
+ puts "valid: #{valid}, result: #{result.inspect}"
40
+
41
+ return result if valid
42
+
43
+ # Raise error if max retries reached
44
+ raise RetryLimitReached, "Max retries reached" if attempts >= @max_retries
45
+
46
+ # Log retry attempt
47
+ puts "[AiGuardrails] Attempt #{attempts}: Invalid output, retrying..."
48
+ sleep @sleep_time
49
+ end
50
+ end
51
+ # rubocop:enable Metrics/MethodLength
52
+
53
+ private
54
+
55
+ # Prepare prompt with schema hint if provided
56
+ def prepare_prompt(prompt, schema_hint)
57
+ return prompt unless schema_hint
58
+
59
+ "#{prompt}\n\nReturn only a valid JSON object matching this schema " \
60
+ "(no explanations, no formatting): #{schema_hint}"
61
+ end
62
+
63
+ # Parse and repair raw AI output
64
+ def parse_and_repair(raw_output)
65
+ repaired = repair_json(raw_output)
66
+ parse_json_or_empty(repaired)
67
+ end
68
+
69
+ # Attempt to repair JSON using JsonRepair
70
+ def repair_json(raw)
71
+ JsonRepair.repair(raw)
72
+ rescue JsonRepair::RepairError
73
+ raw
74
+ end
75
+
76
+ # Parse string JSON or return hash, fallback to empty hash
77
+ def parse_json_or_empty(input)
78
+ return input if input.is_a?(Hash)
79
+
80
+ JSON.parse(input)
81
+ rescue JSON::ParserError
82
+ {}
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module AiGuardrails
6
+ # Automatically fixes JSON outputs based on schema hints
7
+ module AutoFix
8
+ class << self
9
+ # Fix a JSON string or hash according to schema
10
+ #
11
+ # @param json_input [String, Hash] raw JSON output
12
+ # @param schema [Hash] expected schema (key => type)
13
+ # @param hooks [Array<Proc>] optional hooks for custom fixes
14
+ # @return [Hash] fixed output
15
+ def fix(json_input, schema:, hooks: [])
16
+ data = parse_json(json_input)
17
+
18
+ fixed = apply_schema_fixes(data, schema)
19
+
20
+ hooks.each do |hook|
21
+ fixed = apply_hook(fixed, hook)
22
+ end
23
+
24
+ fixed
25
+ end
26
+
27
+ def apply_hook(fixed, hook)
28
+ result = hook.call(fixed)
29
+
30
+ # Ensure hook returns a Hash
31
+ if result.is_a?(Hash)
32
+ result
33
+ else
34
+ warn_hook_return_type(hook, result)
35
+ fixed # keep previous hash
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def warn_hook_return_type(hook, result)
42
+ warn(
43
+ "[AiGuardrails::AutoFix] WARNING: hook #{hook} returned " \
44
+ "#{result.class}, expected Hash. Using previous hash instead."
45
+ )
46
+ end
47
+
48
+ # Parse string or return hash
49
+ def parse_json(input)
50
+ case input
51
+ when String
52
+ JSON.parse(input)
53
+ when Hash
54
+ input
55
+ else
56
+ raise ArgumentError, "Unsupported input type: #{input.class}"
57
+ end
58
+ rescue JSON::ParserError
59
+ {} # fallback empty hash
60
+ end
61
+
62
+ # Convert/repair values according to schema types
63
+ def apply_schema_fixes(data, schema)
64
+ fixed = {}
65
+ schema.each do |key, type|
66
+ str_key = key.to_s
67
+ value = data[str_key] || data[key.to_sym]
68
+
69
+ fixed[str_key] = convert_value(value, type)
70
+ end
71
+ fixed
72
+ end
73
+
74
+ def convert_value(value, type)
75
+ return value.to_s if type == :string
76
+ return value.to_i if type == :integer
77
+ return value.to_s.to_f if type == :float
78
+ return !!value if type == :boolean
79
+ return value.is_a?(Array) ? value : Array(value) if type == Array
80
+
81
+ value
82
+ end
83
+ end
84
+ end
85
+ end