desiru 0.1.0 → 0.1.1

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.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/.env.example +34 -0
  3. data/.rubocop.yml +7 -4
  4. data/.ruby-version +1 -0
  5. data/CLAUDE.md +4 -0
  6. data/Gemfile +21 -2
  7. data/Gemfile.lock +87 -12
  8. data/README.md +295 -2
  9. data/Rakefile +1 -0
  10. data/db/migrations/001_create_initial_tables.rb +96 -0
  11. data/db/migrations/002_create_job_results.rb +39 -0
  12. data/desiru.db +0 -0
  13. data/desiru.gemspec +2 -5
  14. data/docs/background_processing_roadmap.md +87 -0
  15. data/docs/job_scheduling.md +167 -0
  16. data/dspy-analysis-swarm.yml +60 -0
  17. data/dspy-feature-analysis.md +121 -0
  18. data/examples/README.md +69 -0
  19. data/examples/api_with_persistence.rb +122 -0
  20. data/examples/assertions_example.rb +232 -0
  21. data/examples/async_processing.rb +2 -0
  22. data/examples/few_shot_learning.rb +1 -2
  23. data/examples/graphql_api.rb +4 -2
  24. data/examples/graphql_integration.rb +3 -3
  25. data/examples/graphql_optimization_summary.md +143 -0
  26. data/examples/graphql_performance_benchmark.rb +247 -0
  27. data/examples/persistence_example.rb +102 -0
  28. data/examples/react_agent.rb +203 -0
  29. data/examples/rest_api.rb +173 -0
  30. data/examples/rest_api_advanced.rb +333 -0
  31. data/examples/scheduled_job_example.rb +116 -0
  32. data/examples/simple_qa.rb +1 -2
  33. data/examples/sinatra_api.rb +109 -0
  34. data/examples/typed_signatures.rb +1 -2
  35. data/graphql_optimization_summary.md +53 -0
  36. data/lib/desiru/api/grape_integration.rb +284 -0
  37. data/lib/desiru/api/persistence_middleware.rb +148 -0
  38. data/lib/desiru/api/sinatra_integration.rb +217 -0
  39. data/lib/desiru/api.rb +42 -0
  40. data/lib/desiru/assertions.rb +74 -0
  41. data/lib/desiru/async_status.rb +65 -0
  42. data/lib/desiru/cache.rb +1 -1
  43. data/lib/desiru/configuration.rb +2 -1
  44. data/lib/desiru/errors.rb +160 -0
  45. data/lib/desiru/field.rb +17 -14
  46. data/lib/desiru/graphql/batch_loader.rb +85 -0
  47. data/lib/desiru/graphql/data_loader.rb +242 -75
  48. data/lib/desiru/graphql/enum_builder.rb +75 -0
  49. data/lib/desiru/graphql/executor.rb +37 -4
  50. data/lib/desiru/graphql/schema_generator.rb +62 -158
  51. data/lib/desiru/graphql/type_builder.rb +138 -0
  52. data/lib/desiru/graphql/type_cache_warmer.rb +91 -0
  53. data/lib/desiru/jobs/async_predict.rb +1 -1
  54. data/lib/desiru/jobs/base.rb +67 -0
  55. data/lib/desiru/jobs/batch_processor.rb +6 -6
  56. data/lib/desiru/jobs/retriable.rb +119 -0
  57. data/lib/desiru/jobs/retry_strategies.rb +169 -0
  58. data/lib/desiru/jobs/scheduler.rb +219 -0
  59. data/lib/desiru/jobs/webhook_notifier.rb +242 -0
  60. data/lib/desiru/models/anthropic.rb +164 -0
  61. data/lib/desiru/models/base.rb +37 -3
  62. data/lib/desiru/models/open_ai.rb +151 -0
  63. data/lib/desiru/models/open_router.rb +161 -0
  64. data/lib/desiru/module.rb +59 -9
  65. data/lib/desiru/modules/chain_of_thought.rb +3 -3
  66. data/lib/desiru/modules/majority.rb +51 -0
  67. data/lib/desiru/modules/multi_chain_comparison.rb +204 -0
  68. data/lib/desiru/modules/predict.rb +8 -1
  69. data/lib/desiru/modules/program_of_thought.rb +139 -0
  70. data/lib/desiru/modules/react.rb +273 -0
  71. data/lib/desiru/modules/retrieve.rb +4 -2
  72. data/lib/desiru/optimizers/base.rb +2 -4
  73. data/lib/desiru/optimizers/bootstrap_few_shot.rb +2 -2
  74. data/lib/desiru/optimizers/copro.rb +268 -0
  75. data/lib/desiru/optimizers/knn_few_shot.rb +185 -0
  76. data/lib/desiru/persistence/database.rb +71 -0
  77. data/lib/desiru/persistence/models/api_request.rb +38 -0
  78. data/lib/desiru/persistence/models/job_result.rb +138 -0
  79. data/lib/desiru/persistence/models/module_execution.rb +37 -0
  80. data/lib/desiru/persistence/models/optimization_result.rb +28 -0
  81. data/lib/desiru/persistence/models/training_example.rb +25 -0
  82. data/lib/desiru/persistence/models.rb +11 -0
  83. data/lib/desiru/persistence/repositories/api_request_repository.rb +98 -0
  84. data/lib/desiru/persistence/repositories/base_repository.rb +77 -0
  85. data/lib/desiru/persistence/repositories/job_result_repository.rb +116 -0
  86. data/lib/desiru/persistence/repositories/module_execution_repository.rb +85 -0
  87. data/lib/desiru/persistence/repositories/optimization_result_repository.rb +67 -0
  88. data/lib/desiru/persistence/repositories/training_example_repository.rb +102 -0
  89. data/lib/desiru/persistence/repository.rb +29 -0
  90. data/lib/desiru/persistence/setup.rb +77 -0
  91. data/lib/desiru/persistence.rb +49 -0
  92. data/lib/desiru/registry.rb +3 -5
  93. data/lib/desiru/signature.rb +91 -24
  94. data/lib/desiru/version.rb +1 -1
  95. data/lib/desiru.rb +23 -8
  96. data/missing-features-analysis.md +192 -0
  97. metadata +63 -45
  98. data/lib/desiru/models/raix_adapter.rb +0 -210
@@ -0,0 +1,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sinatra/base'
4
+ require 'sinatra/json'
5
+ require 'json'
6
+ require 'rack/cors'
7
+
8
+ module Desiru
9
+ module API
10
+ # Sinatra integration for Desiru - lightweight REST API generation
11
+ class SinatraIntegration
12
+ attr_reader :modules, :async_enabled, :stream_enabled
13
+
14
+ def initialize(async_enabled: true, stream_enabled: false)
15
+ @modules = {}
16
+ @async_enabled = async_enabled
17
+ @stream_enabled = stream_enabled
18
+ end
19
+
20
+ # Register a Desiru module with an endpoint path
21
+ def register_module(path, desiru_module, description: nil)
22
+ @modules[path] = {
23
+ module: desiru_module,
24
+ description: description || "Endpoint for #{desiru_module.class.name}"
25
+ }
26
+ end
27
+
28
+ # Generate a Sinatra application with all registered modules
29
+ def generate_api
30
+ modules_config = @modules
31
+ async = @async_enabled
32
+ stream = @stream_enabled
33
+
34
+ Class.new(Sinatra::Base) do
35
+ set :show_exceptions, false
36
+ set :raise_errors, true
37
+
38
+ # Helpers for parameter validation and response formatting
39
+ helpers do
40
+ def validate_type(value, type_string)
41
+ case type_string.to_s.downcase
42
+ when 'string', 'str'
43
+ value.is_a?(String)
44
+ when 'integer', 'int'
45
+ value.is_a?(Integer) || (value.is_a?(String) && value.match?(/\A-?\d+\z/))
46
+ when 'float'
47
+ value.is_a?(Numeric)
48
+ when 'boolean', 'bool'
49
+ [true, false, 'true', 'false'].include?(value)
50
+ when /^list/
51
+ value.is_a?(Array)
52
+ else
53
+ true # Unknown types and literals pass validation
54
+ end
55
+ end
56
+
57
+ def coerce_value(value, type_string)
58
+ case type_string.to_s.downcase
59
+ when 'integer', 'int'
60
+ value.to_i
61
+ when 'float'
62
+ value.to_f
63
+ when 'boolean', 'bool'
64
+ ['true', true].include?(value)
65
+ else
66
+ value
67
+ end
68
+ end
69
+
70
+ def format_response(result)
71
+ if result.is_a?(Desiru::ModuleResult)
72
+ result.data
73
+ elsif result.is_a?(Hash)
74
+ result
75
+ else
76
+ { result: result }
77
+ end
78
+ end
79
+
80
+ def handle_async_request(desiru_module, inputs)
81
+ result = desiru_module.call_async(inputs)
82
+
83
+ {
84
+ job_id: result.job_id,
85
+ status: result.status,
86
+ progress: result.progress,
87
+ status_url: "/api/v1/jobs/#{result.job_id}"
88
+ }
89
+ end
90
+
91
+ def parse_json_body
92
+ request.body.rewind
93
+ JSON.parse(request.body.read)
94
+ rescue JSON::ParserError
95
+ halt 400, json(error: 'Invalid JSON')
96
+ end
97
+ end
98
+
99
+ # Content type handling
100
+ before do
101
+ content_type :json
102
+ end
103
+
104
+ # Health check endpoint
105
+ get '/api/v1/health' do
106
+ json status: 'ok', timestamp: Time.now.iso8601
107
+ end
108
+
109
+ # Generate endpoints for each registered module
110
+ modules_config.each do |path, config|
111
+ desiru_module = config[:module]
112
+
113
+ # Main module endpoint
114
+ post "/api/v1#{path}" do
115
+ params = parse_json_body
116
+
117
+ # Convert string keys to symbols for module call
118
+ symbolized_params = {}
119
+ params.each { |k, v| symbolized_params[k.to_sym] = v }
120
+
121
+ begin
122
+ result = desiru_module.call(symbolized_params)
123
+ json format_response(result)
124
+ rescue Desiru::ModuleError => e
125
+ halt 400, json(error: e.message)
126
+ rescue StandardError => e
127
+ halt 500, json(error: e.message)
128
+ end
129
+ end
130
+
131
+ # Async endpoint
132
+ if async && desiru_module.respond_to?(:call_async)
133
+ post "/api/v1/async#{path}" do
134
+ params = parse_json_body
135
+
136
+ begin
137
+ result = handle_async_request(desiru_module, params)
138
+ status 202
139
+ json result
140
+ rescue StandardError => e
141
+ halt 500, json(error: e.message)
142
+ end
143
+ end
144
+ end
145
+
146
+ # Streaming endpoint (Server-Sent Events)
147
+ next unless stream && desiru_module.respond_to?(:call_stream)
148
+
149
+ post "/api/v1/stream#{path}" do
150
+ content_type 'text/event-stream'
151
+ stream do |out|
152
+ params = parse_json_body
153
+
154
+ begin
155
+ desiru_module.call_stream(params) do |chunk|
156
+ out << "event: chunk\n"
157
+ out << "data: #{JSON.generate(chunk)}\n\n"
158
+ end
159
+
160
+ # Send final result
161
+ result = desiru_module.call(params)
162
+ out << "event: result\n"
163
+ out << "data: #{JSON.generate(format_response(result))}\n\n"
164
+ out << "event: done\n"
165
+ out << "data: #{JSON.generate({ status: 'complete' })}\n\n"
166
+ rescue StandardError => e
167
+ out << "event: error\n"
168
+ out << "data: #{JSON.generate(error: e.message)}\n\n"
169
+ ensure
170
+ out.close
171
+ end
172
+ end
173
+ end
174
+ end
175
+
176
+ # Job status endpoint for async requests
177
+ if async
178
+ get '/api/v1/jobs/:job_id' do
179
+ job_id = params[:job_id]
180
+
181
+ if Desiru.respond_to?(:check_job_status)
182
+ status = Desiru.check_job_status(job_id)
183
+
184
+ if status
185
+ json status
186
+ else
187
+ halt 404, json(error: 'Job not found')
188
+ end
189
+ else
190
+ halt 501, json(error: 'Async job tracking not implemented')
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
196
+
197
+ # Mount the API in a Rack application with CORS
198
+ def to_rack_app
199
+ api = generate_api
200
+
201
+ Rack::Builder.new do
202
+ use Rack::Cors do
203
+ allow do
204
+ origins '*'
205
+ resource '*',
206
+ headers: :any,
207
+ methods: %i[get post put patch delete options head],
208
+ expose: ['Access-Control-Allow-Origin']
209
+ end
210
+ end
211
+
212
+ run api
213
+ end
214
+ end
215
+ end
216
+ end
217
+ end
data/lib/desiru/api.rb ADDED
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'api/grape_integration'
4
+ require_relative 'api/sinatra_integration'
5
+ require_relative 'api/persistence_middleware'
6
+
7
+ module Desiru
8
+ module API
9
+ # Convenience method to create a new API integration
10
+ # @param framework [Symbol] :grape or :sinatra (default: :grape)
11
+ def self.create(framework: :grape, async_enabled: true, stream_enabled: false, &)
12
+ klass = case framework
13
+ when :grape
14
+ GrapeIntegration
15
+ when :sinatra
16
+ SinatraIntegration
17
+ else
18
+ raise ArgumentError, "Unknown framework: #{framework}. Use :grape or :sinatra"
19
+ end
20
+
21
+ integration = klass.new(
22
+ async_enabled: async_enabled,
23
+ stream_enabled: stream_enabled
24
+ )
25
+
26
+ # Allow DSL-style configuration
27
+ integration.instance_eval(&) if block_given?
28
+
29
+ integration
30
+ end
31
+
32
+ # Convenience method to create a new Grape API (backward compatibility)
33
+ def self.grape(async_enabled: true, stream_enabled: false, &)
34
+ create(framework: :grape, async_enabled: async_enabled, stream_enabled: stream_enabled, &)
35
+ end
36
+
37
+ # Convenience method to create a new Sinatra API
38
+ def self.sinatra(async_enabled: true, stream_enabled: false, &)
39
+ create(framework: :sinatra, async_enabled: async_enabled, stream_enabled: stream_enabled, &)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Desiru
4
+ # Assertion system for validating module outputs
5
+ module Assertions
6
+ # Error raised when an assertion fails
7
+ class AssertionError < StandardError
8
+ attr_reader :module_name, :retry_count
9
+
10
+ def initialize(message = nil, module_name: nil, retry_count: 0)
11
+ super(message)
12
+ @module_name = module_name
13
+ @retry_count = retry_count
14
+ end
15
+
16
+ # Assertions should trigger module retries
17
+ def retriable?
18
+ true
19
+ end
20
+ end
21
+
22
+ # Assert that a condition is true, raising AssertionError if false
23
+ # @param condition [Boolean] The condition to check
24
+ # @param message [String] Optional error message
25
+ # @raise [AssertionError] if condition is false
26
+ def self.assert(condition, message = nil)
27
+ return if condition
28
+
29
+ message ||= 'Assertion failed'
30
+ raise AssertionError, message
31
+ end
32
+
33
+ # Suggest that a condition should be true, logging a warning if false
34
+ # @param condition [Boolean] The condition to check
35
+ # @param message [String] Optional warning message
36
+ def self.suggest(condition, message = nil)
37
+ return if condition
38
+
39
+ message ||= 'Suggestion failed'
40
+ Desiru.logger.warn("[SUGGESTION] #{message}")
41
+ end
42
+
43
+ # Configuration for assertion behavior
44
+ class Configuration
45
+ attr_accessor :max_assertion_retries, :assertion_retry_delay, :log_assertions, :track_assertion_metrics
46
+
47
+ def initialize
48
+ @max_assertion_retries = 3
49
+ @assertion_retry_delay = 0.1 # seconds
50
+ @log_assertions = true
51
+ @track_assertion_metrics = false
52
+ end
53
+ end
54
+
55
+ # Get or set the assertion configuration
56
+ def self.configuration
57
+ @configuration ||= Configuration.new
58
+ end
59
+
60
+ # Configure assertion behavior
61
+ def self.configure
62
+ yield(configuration) if block_given?
63
+ end
64
+ end
65
+
66
+ # Module-level convenience methods
67
+ def self.assert(condition, message = nil)
68
+ Assertions.assert(condition, message)
69
+ end
70
+
71
+ def self.suggest(condition, message = nil)
72
+ Assertions.suggest(condition, message)
73
+ end
74
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'redis'
4
+ require 'json'
5
+
6
+ module Desiru
7
+ # AsyncStatus provides a simple interface for checking job status
8
+ # Compatible with the REST API's job status endpoint
9
+ class AsyncStatus
10
+ attr_reader :job_id
11
+
12
+ def initialize(job_id)
13
+ @job_id = job_id
14
+ @redis = Redis.new(url: Desiru.configuration.redis_url || ENV.fetch('REDIS_URL', nil))
15
+ end
16
+
17
+ def status
18
+ status_data = fetch_status
19
+ return 'pending' unless status_data
20
+
21
+ status_data[:status] || 'pending'
22
+ end
23
+
24
+ def progress
25
+ status_data = fetch_status
26
+ return 0 unless status_data
27
+
28
+ status_data[:progress] || 0
29
+ end
30
+
31
+ def ready?
32
+ result_data = fetch_result
33
+ !result_data.nil?
34
+ end
35
+
36
+ def result
37
+ result_data = fetch_result
38
+ return nil unless result_data
39
+
40
+ raise ModuleError, "Async job failed: #{result_data[:error]}" unless result_data[:success]
41
+
42
+ result_data[:result]
43
+ end
44
+
45
+ private
46
+
47
+ def fetch_status
48
+ raw = @redis.get("desiru:status:#{job_id}")
49
+ return nil unless raw
50
+
51
+ JSON.parse(raw, symbolize_names: true)
52
+ rescue JSON::ParserError
53
+ nil
54
+ end
55
+
56
+ def fetch_result
57
+ raw = @redis.get("desiru:results:#{job_id}")
58
+ return nil unless raw
59
+
60
+ JSON.parse(raw, symbolize_names: true)
61
+ rescue JSON::ParserError
62
+ nil
63
+ end
64
+ end
65
+ end
data/lib/desiru/cache.rb CHANGED
@@ -113,4 +113,4 @@ module Desiru
113
113
  @store.delete(lru_key) if lru_key
114
114
  end
115
115
  end
116
- end
116
+ end
@@ -6,7 +6,7 @@ module Desiru
6
6
  class Configuration
7
7
  attr_accessor :default_model, :cache_enabled, :cache_ttl, :max_retries,
8
8
  :retry_delay, :logger, :module_registry, :model_timeout,
9
- :redis_url
9
+ :redis_url, :retry_count
10
10
 
11
11
  def initialize
12
12
  @default_model = nil
@@ -14,6 +14,7 @@ module Desiru
14
14
  @cache_ttl = 3600 # 1 hour
15
15
  @max_retries = 3
16
16
  @retry_delay = 1
17
+ @retry_count = 3
17
18
  @logger = default_logger
18
19
  @module_registry = Desiru::Registry.instance
19
20
  @model_timeout = 30
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Desiru
4
+ # Base error class moved here for organization
5
+ class Error < StandardError
6
+ attr_reader :context, :original_error
7
+
8
+ def initialize(message = nil, context: {}, original_error: nil)
9
+ @context = context
10
+ @original_error = original_error
11
+
12
+ super(build_message(message))
13
+ end
14
+
15
+ private
16
+
17
+ def build_message(message)
18
+ parts = [message || self.class.name.split('::').last]
19
+
20
+ if context.any?
21
+ context_str = context.map { |k, v| "#{k}: #{v}" }.join(', ')
22
+ parts << "(#{context_str})"
23
+ end
24
+
25
+ parts << "caused by #{original_error.class}: #{original_error.message}" if original_error
26
+
27
+ parts.join(' ')
28
+ end
29
+ end
30
+
31
+ # Configuration errors
32
+ class ConfigurationError < Error; end
33
+
34
+ # Signature and validation errors
35
+ class SignatureError < Error; end
36
+ class ValidationError < Error; end
37
+
38
+ # Module execution errors
39
+ class ModuleError < Error; end
40
+ class TimeoutError < ModuleError; end
41
+
42
+ # Network and API errors
43
+ class NetworkError < Error; end
44
+
45
+ class RateLimitError < NetworkError
46
+ attr_reader :retry_after
47
+
48
+ def initialize(message = nil, retry_after: nil, **)
49
+ @retry_after = retry_after
50
+ super(message, **)
51
+ end
52
+ end
53
+
54
+ class AuthenticationError < NetworkError; end
55
+
56
+ # Model/LLM specific errors
57
+ class ModelError < Error; end
58
+
59
+ class TokenLimitError < ModelError
60
+ attr_reader :token_count, :token_limit
61
+
62
+ def initialize(message = nil, token_count: nil, token_limit: nil, **)
63
+ @token_count = token_count
64
+ @token_limit = token_limit
65
+ super(message, **)
66
+ end
67
+ end
68
+
69
+ class InvalidResponseError < ModelError; end
70
+ class ModelNotAvailableError < ModelError; end
71
+
72
+ # Job and async related errors
73
+ class JobError < Error; end
74
+ class JobNotFoundError < JobError; end
75
+ class JobTimeoutError < JobError; end
76
+ class JobFailedError < JobError; end
77
+
78
+ # Persistence related errors
79
+ class PersistenceError < Error; end
80
+ class DatabaseConnectionError < PersistenceError; end
81
+ class RecordNotFoundError < PersistenceError; end
82
+ class RecordInvalidError < PersistenceError; end
83
+
84
+ # Optimizer related errors
85
+ class OptimizerError < Error; end
86
+ class OptimizationFailedError < OptimizerError; end
87
+ class InsufficientDataError < OptimizerError; end
88
+
89
+ # Cache related errors
90
+ class CacheError < Error; end
91
+ class CacheConnectionError < CacheError; end
92
+
93
+ # Error handling utilities
94
+ module ErrorHandling
95
+ # Wrap a block with error context
96
+ def with_error_context(context = {})
97
+ yield
98
+ rescue StandardError => e
99
+ # Add context to existing Desiru errors
100
+ raise Desiru::Error.new(e.message, context: context, original_error: e) unless e.is_a?(Desiru::Error)
101
+
102
+ e.context.merge!(context)
103
+ raise e
104
+
105
+ # Wrap other errors with context
106
+ end
107
+
108
+ # Retry with exponential backoff
109
+ def with_retry(max_attempts: 3, backoff: :exponential, retriable_errors: [NetworkError, TimeoutError])
110
+ attempt = 0
111
+
112
+ begin
113
+ attempt += 1
114
+ yield(attempt)
115
+ rescue *retriable_errors => e
116
+ raise unless attempt < max_attempts
117
+
118
+ delay = calculate_backoff(attempt, backoff)
119
+ Desiru.logger.warn "Retrying after #{delay}s (attempt #{attempt}/#{max_attempts}): #{e.message}"
120
+ sleep delay
121
+ retry
122
+ end
123
+ end
124
+
125
+ # Log and swallow errors (use sparingly)
126
+ def safe_execute(default = nil, log_level: :error)
127
+ yield
128
+ rescue StandardError => e
129
+ Desiru.logger.send(log_level, "Error in safe_execute: #{e.class} - #{e.message}")
130
+ Desiru.logger.debug e.backtrace.join("\n") if log_level == :error
131
+ default
132
+ end
133
+
134
+ private
135
+
136
+ def calculate_backoff(attempt, strategy)
137
+ case strategy
138
+ when :exponential
139
+ [2**(attempt - 1), 60].min # Max 60 seconds
140
+ when :linear
141
+ attempt * 2
142
+ when Numeric
143
+ strategy
144
+ else
145
+ 1
146
+ end
147
+ end
148
+ end
149
+
150
+ # Include error handling in base classes
151
+ class Module
152
+ include ErrorHandling
153
+ end
154
+
155
+ module Jobs
156
+ class Base
157
+ include ErrorHandling
158
+ end
159
+ end
160
+ end
data/lib/desiru/field.rb CHANGED
@@ -3,14 +3,16 @@
3
3
  module Desiru
4
4
  # Represents a field in a signature with type information and metadata
5
5
  class Field
6
- attr_reader :name, :type, :description, :optional, :default, :validator, :literal_values, :element_type
6
+ attr_reader :name, :type, :description, :optional, :default, :validator, :literal_values, :element_type,
7
+ :original_type
7
8
 
8
9
  alias optional? optional
9
10
 
10
11
  def initialize(name, type = :string, description: nil, optional: false, default: nil, validator: nil,
11
- literal_values: nil, element_type: nil)
12
+ literal_values: nil, element_type: nil, original_type: nil)
12
13
  @name = name.to_sym
13
14
  @type = normalize_type(type)
15
+ @original_type = original_type || type.to_s
14
16
  @description = description
15
17
  @optional = optional
16
18
  @default = default
@@ -19,7 +21,7 @@ module Desiru
19
21
  @validator = validator || default_validator
20
22
  end
21
23
 
22
- def validate(value)
24
+ def valid?(value)
23
25
  return true if optional && value.nil?
24
26
  return true if value.nil? && !default.nil?
25
27
 
@@ -46,7 +48,7 @@ module Desiru
46
48
  when 'false', 'no', '0', 'f'
47
49
  false
48
50
  else
49
- !!value
51
+ !value.nil?
50
52
  end
51
53
  when :literal
52
54
  # For literal types, ensure the value is a string and matches one of the allowed values
@@ -63,8 +65,9 @@ module Desiru
63
65
  array_value.map do |elem|
64
66
  coerced_elem = elem.to_s
65
67
  unless element_type[:literal_values].include?(coerced_elem)
68
+ allowed = element_type[:literal_values].join(', ')
66
69
  raise ValidationError,
67
- "Array element '#{coerced_elem}' is not one of allowed values: #{element_type[:literal_values].join(', ')}"
70
+ "Array element '#{coerced_elem}' is not one of allowed values: #{allowed}"
68
71
  end
69
72
 
70
73
  coerced_elem
@@ -146,25 +149,25 @@ module Desiru
146
149
  def default_validator
147
150
  case type
148
151
  when :string
149
- ->(v) { v.is_a?(String) }
152
+ ->(value) { value.is_a?(String) }
150
153
  when :int
151
- ->(v) { v.is_a?(Integer) }
154
+ ->(value) { value.is_a?(Integer) }
152
155
  when :float
153
- ->(v) { v.is_a?(Float) || v.is_a?(Integer) }
156
+ ->(value) { value.is_a?(Float) || value.is_a?(Integer) }
154
157
  when :bool
155
- ->(v) { v.is_a?(TrueClass) || v.is_a?(FalseClass) }
158
+ ->(value) { value.is_a?(TrueClass) || value.is_a?(FalseClass) }
156
159
  when :literal
157
- ->(v) { v.is_a?(String) && literal_values.include?(v) }
160
+ ->(value) { value.is_a?(String) && literal_values.include?(value) }
158
161
  when :list
159
162
  if element_type && element_type[:type] == :literal
160
- ->(v) { v.is_a?(Array) && v.all? { |elem| element_type[:literal_values].include?(elem.to_s) } }
163
+ ->(value) { value.is_a?(Array) && value.all? { |elem| element_type[:literal_values].include?(elem.to_s) } }
161
164
  else
162
- ->(v) { v.is_a?(Array) }
165
+ ->(value) { value.is_a?(Array) }
163
166
  end
164
167
  when :hash
165
- ->(v) { v.is_a?(Hash) }
168
+ ->(value) { value.is_a?(Hash) }
166
169
  else
167
- ->(_v) { true }
170
+ ->(_value) { true }
168
171
  end
169
172
  end
170
173
  end