action_ai 0.1.0 → 0.3.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/CHANGELOG.md +35 -0
- data/README.rdoc +129 -1
- data/lib/action_ai/agent.rb +2 -0
- data/lib/action_ai/implicit_interaction.rb +19 -0
- data/lib/action_ai/interaction.rb +17 -0
- data/lib/action_ai/model_schema/active_model.rb +123 -0
- data/lib/action_ai/model_schema/active_record.rb +137 -0
- data/lib/action_ai/model_schema.rb +195 -0
- data/lib/action_ai/railtie.rb +8 -1
- data/lib/action_ai/refinements/numeric.rb +32 -0
- data/lib/action_ai/refinements/regexp.rb +13 -0
- data/lib/action_ai/schema.rb +20 -0
- data/lib/action_ai/structured_output.rb +85 -0
- data/lib/action_ai/version.rb +1 -1
- data/lib/action_ai.rb +6 -0
- data/lib/ruby_llm/protocols/echo.rb +26 -0
- data/lib/ruby_llm/providers/test.rb +5 -21
- data/lib/ruby_llm/tester.rb +7 -3
- metadata +182 -143
- data/lib/ruby_llm/providers/test/echo.rb +0 -47
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 626b0d7eb4998d7c208dc99eb8e60932a3006988176bbd784935f1e00437b485
|
|
4
|
+
data.tar.gz: 97512e0c8fd015af6235d61b31ed36e7030702ddaf4b436b18b462b39013bd55
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5721c21b4234b75cc9afce08eef6e5df89b4cb3364b75df8bbc3735c9ed3735d19fe6186d1893b678e5bfb9cddcbe15511909babd0ab7851f7663fd2c4fb1011
|
|
7
|
+
data.tar.gz: 025dec0b60c2b398450fe31174789df35689bef9a48a5c77627619c9df3c6fd2de62b153c968430a2135b0ca2cc9ee73e29049e502e5b34acbbd1217ba523fa4
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
|
|
6
|
+
## [0.3.0] - 2026-10-13
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
|
|
10
|
+
- Moved to RubyLLM 2.
|
|
11
|
+
- Dropped `echo` test model in favor of `echo` protocol.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
#### Structured output
|
|
16
|
+
|
|
17
|
+
- `.schema` class method on any `ActiveModel::API` model, returning a cached `Schematist::Schema` instance derived from the model’s attribute types and constraints.
|
|
18
|
+
- Include Active Record associations as nested schemas via objects/arrays.
|
|
19
|
+
Consider associations registered for _nested attributes_ only.
|
|
20
|
+
- Prefer custom schemas over inferred ones.
|
|
21
|
+
Define them as `<ModelName>Schema < ActionAI::Schema` classes under `app/schemas`.
|
|
22
|
+
- `#returns(ModelClass)` instance method in agent actions: configures structured output via `chat.with_schema` and decorates the resulting `RubyLLM::Message` with `#object` returning a model instance.
|
|
23
|
+
- Use Strong Parameters to decorate returned JSON for security reasons.
|
|
24
|
+
AI model’s output shouldn’t be considered a trusted input.
|
|
25
|
+
- `#returns([ModelClass])` array syntax: configures an array schema and `#object` returns an array of model instances.
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## [0.2.0] - 2026-06-30
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- Implicit `ask` behavior for AI actions without explicit prompts, matching Action Controller-style ergonomics.
|
|
33
|
+
- Action chaining, enabling users to construct complex AI workflows from discrete jobs.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- `echo` test model to support multiple chat interactions.
|
|
38
|
+
|
|
39
|
+
|
|
5
40
|
## [0.1.0] - 2026-05-05
|
|
6
41
|
|
|
7
42
|
Refactored from Action Mailer.
|
data/README.rdoc
CHANGED
|
@@ -16,6 +16,8 @@ shape for AI interactions rather than building an unrelated API from scratch.
|
|
|
16
16
|
|
|
17
17
|
The framework works by initializing any instance variables you want to be
|
|
18
18
|
available in the prompt template.
|
|
19
|
+
Like +render+ in Action Controller, the implicit +ask+ will be triggered
|
|
20
|
+
automatically at the end of the action unless you have already called it.
|
|
19
21
|
|
|
20
22
|
This can be as simple as:
|
|
21
23
|
|
|
@@ -25,10 +27,16 @@ This can be as simple as:
|
|
|
25
27
|
def code(task, language)
|
|
26
28
|
@task = task
|
|
27
29
|
@language = language
|
|
28
|
-
ask
|
|
29
30
|
end
|
|
30
31
|
end
|
|
31
32
|
|
|
33
|
+
If you need to customize the prompt execution (e.g., pass options or a custom prompt),
|
|
34
|
+
you can call +ask+ explicitly:
|
|
35
|
+
|
|
36
|
+
def run(spec_file)
|
|
37
|
+
ask "Run the attached spec", with: spec_file
|
|
38
|
+
end
|
|
39
|
+
|
|
32
40
|
After the action method completes, the framework will automatically:
|
|
33
41
|
1. Render the prompt from the corresponding template (e.g., +app/ai/prompts/generator/code.erb+)
|
|
34
42
|
2. Send it to the configured AI model via +ask+
|
|
@@ -64,6 +72,22 @@ Or you can just chain the methods together like:
|
|
|
64
72
|
|
|
65
73
|
Generator.code("Parse CSV and dedupe", :ruby).content # Returns AI's response for the prompt
|
|
66
74
|
|
|
75
|
+
You can also chain multiple agent actions to compose a small workflow before reading the final response:
|
|
76
|
+
|
|
77
|
+
class WorkflowAgent < ApplicationAI
|
|
78
|
+
def collect(topic)
|
|
79
|
+
@topic = topic
|
|
80
|
+
ask "Collect #{@topic}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def refine(style:)
|
|
84
|
+
ask "Refine #{@topic} as #{style}"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
WorkflowAgent.collect("release notes").refine(style: "bullet list").content
|
|
89
|
+
# => collected release notes refined as bullet list
|
|
90
|
+
|
|
67
91
|
== Setting defaults
|
|
68
92
|
|
|
69
93
|
It is possible to set default values that will be used in every method in your
|
|
@@ -93,6 +117,110 @@ The Agent class has the full list of configuration options. Here's an example:
|
|
|
93
117
|
}
|
|
94
118
|
|
|
95
119
|
|
|
120
|
+
== Structured output (data schemas)
|
|
121
|
+
|
|
122
|
+
Action AI can instruct the model to return structured JSON that maps directly
|
|
123
|
+
to an ActiveModel model. Two features cooperate to make this work:
|
|
124
|
+
|
|
125
|
+
=== +.schema+ on ActiveModel models
|
|
126
|
+
|
|
127
|
+
Any model that includes +ActiveModel::API+ automatically gains a +.schema+
|
|
128
|
+
class method that returns a +Schematist::Schema+ instance derived from the model's
|
|
129
|
+
attribute types.
|
|
130
|
+
|
|
131
|
+
class Person
|
|
132
|
+
include ActiveModel::Model
|
|
133
|
+
include ActiveModel::Attributes
|
|
134
|
+
|
|
135
|
+
attribute :name, :string
|
|
136
|
+
attribute :age, :integer
|
|
137
|
+
attribute :score, :float
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
Person.schema # => a Schematist::Schema instance
|
|
141
|
+
Person.schema.new.to_json_schema # => {name: "Person", schema: {...}}
|
|
142
|
+
|
|
143
|
+
Supported type mappings:
|
|
144
|
+
|
|
145
|
+
* +:string+, +:immutable_string+, +:text+ → JSON +string+
|
|
146
|
+
* +:integer+, +:big_integer+ → JSON +integer+
|
|
147
|
+
* +:float+, +:decimal+, +:big_decimal+ → JSON +number+
|
|
148
|
+
* +:boolean+ → JSON +boolean+
|
|
149
|
+
* +:date+, +:datetime+, +:time+ → JSON +string+ with +format+ set to
|
|
150
|
+
+"date"+, +"date-time"+, or +"time"+ respectively
|
|
151
|
+
* +:binary+ → JSON +string+ with +contentEncoding+ set to +"base64"+
|
|
152
|
+
|
|
153
|
+
Other types are not included in the schema.
|
|
154
|
+
|
|
155
|
+
Active Record associations are included as nested schemas via +object+/+array+.
|
|
156
|
+
To prevent unnescessary nesting or circular dependencies, the schema will include
|
|
157
|
+
associations registered for _nested attributes_ (via +accepts_nested_attributes_for+) only.
|
|
158
|
+
|
|
159
|
+
=== +returns+ in agent actions
|
|
160
|
+
|
|
161
|
+
Call +returns ModelClass+ inside an action method to declare the expected
|
|
162
|
+
return type. Action AI will:
|
|
163
|
+
|
|
164
|
+
1. Configure the chat to request output matching the model's JSON schema
|
|
165
|
+
(+chat.with_schema ModelClass.schema+).
|
|
166
|
+
2. Decorate the resulting +RubyLLM::Message+ with Strong Parameters.
|
|
167
|
+
3. Construct a model instance accessible via +#object+.
|
|
168
|
+
|
|
169
|
+
class Extractor < ApplicationAI
|
|
170
|
+
def person(text)
|
|
171
|
+
@text = text
|
|
172
|
+
|
|
173
|
+
returns Person do |params|
|
|
174
|
+
params.permit(:name, :age)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
result = Extractor.person("Alice is 30 years old")
|
|
180
|
+
result.content # => '{"name":"Alice","age":30}'
|
|
181
|
+
result.parsed # => {"name" => "Alice", "age" => 30}
|
|
182
|
+
result.object # => #<Person name="Alice" age=30>
|
|
183
|
+
|
|
184
|
+
Pass the class wrapped in an array to declare an array return type:
|
|
185
|
+
|
|
186
|
+
class Extractor < ApplicationAI
|
|
187
|
+
def people(text)
|
|
188
|
+
@text = text
|
|
189
|
+
|
|
190
|
+
returns [Person] do |params|
|
|
191
|
+
params.permit(:name, :age)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
result = Extractor.people("Alice is 30 and Bob is 25")
|
|
197
|
+
result.content # => '[{"name":"Alice","age":30},{"name":"Bob","age":25}]'
|
|
198
|
+
result.parsed # => [{"name" => "Alice", "age" => 30}, {"name" => "Bob", "age" => 25}]
|
|
199
|
+
result.object # => [#<Person name="Alice" age=30>, #<Person name="Bob" age=25>]
|
|
200
|
+
|
|
201
|
+
The schema is built from the model's declared attributes and cached.
|
|
202
|
+
|
|
203
|
+
=== Custom schemas
|
|
204
|
+
|
|
205
|
+
If an inferred schema is not sufficient, define a +Schematist::Schema+
|
|
206
|
+
subclass under +app/schemas+ with the model's full class name followed by
|
|
207
|
+
+Schema+. Custom schemas are looked up first; models without a matching class
|
|
208
|
+
continue to use the inferred schema.
|
|
209
|
+
|
|
210
|
+
# app/schemas/person_schema.rb
|
|
211
|
+
class PersonSchema < ActionAI::Schema
|
|
212
|
+
string :name
|
|
213
|
+
integer :age, minimum: 0
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
For namespaced models, mirror the namespace in +app/schemas+:
|
|
217
|
+
|
|
218
|
+
# app/schemas/admin/person_schema.rb
|
|
219
|
+
class Admin::PersonSchema < ActionAI::Schema
|
|
220
|
+
string :name
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
|
|
96
224
|
== Download and installation
|
|
97
225
|
|
|
98
226
|
The latest version of Action AI can be installed with RubyGems:
|
data/lib/action_ai/agent.rb
CHANGED
|
@@ -278,6 +278,8 @@ module ActionAI
|
|
|
278
278
|
# <tt>execution_job</tt>. Agents can set this to use a custom queue name.
|
|
279
279
|
class Agent < AbstractController::Base
|
|
280
280
|
include Callbacks
|
|
281
|
+
include ImplicitInteraction
|
|
282
|
+
prepend StructuredOutput
|
|
281
283
|
include QueuedExecution
|
|
282
284
|
include Rescuable
|
|
283
285
|
include Parameterized
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionAI
|
|
4
|
+
# # Action AI Implicit Interaction
|
|
5
|
+
#
|
|
6
|
+
# Handles implicit interactions for an agent action that does not explicitly
|
|
7
|
+
# respond with `ask`.
|
|
8
|
+
#
|
|
9
|
+
# If no explicit `ask` is performed by the action, the implicit response is
|
|
10
|
+
# to call `ask` with the default prompt rendering.
|
|
11
|
+
module ImplicitInteraction
|
|
12
|
+
def send_action(...)
|
|
13
|
+
super
|
|
14
|
+
.tap { default_interaction unless performed? }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def default_interaction = ask
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -77,6 +77,23 @@ module ActionAI
|
|
|
77
77
|
# end
|
|
78
78
|
def later(...) = enqueue_execution(...)
|
|
79
79
|
|
|
80
|
+
ruby2_keywords def method_missing(method, *args, &)
|
|
81
|
+
case method.name
|
|
82
|
+
when *@agent_class.action_methods
|
|
83
|
+
processed_agent.process(@action = method, *(@args = args), &)
|
|
84
|
+
self
|
|
85
|
+
else
|
|
86
|
+
super
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def respond_to_missing?(method, include_private = false)
|
|
91
|
+
[
|
|
92
|
+
method.name.in?(@agent_class.action_methods),
|
|
93
|
+
super,
|
|
94
|
+
].any?
|
|
95
|
+
end
|
|
96
|
+
|
|
80
97
|
private
|
|
81
98
|
# Returns the processed Agent instance. We keep this instance
|
|
82
99
|
# on hand so we can run callbacks and delegate exception handling to it.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_ai/refinements/regexp"
|
|
4
|
+
|
|
5
|
+
module ActionAI
|
|
6
|
+
module ModelSchema
|
|
7
|
+
# Extends schema inference with Active Model–specific behavior.
|
|
8
|
+
#
|
|
9
|
+
# Overrides the base inference methods to extract constraint metadata
|
|
10
|
+
# from Active Model validation declarations (presence, inclusion,
|
|
11
|
+
# numericality, format, length, comparison, and acceptance).
|
|
12
|
+
module Inference::ActiveModel
|
|
13
|
+
using Refinements::Regexp
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
# Determines whether an attribute is required by checking for
|
|
18
|
+
# a presence validator, in addition to any other sources.
|
|
19
|
+
def attribute_required? name
|
|
20
|
+
super { [
|
|
21
|
+
attribute_validators(name, :presence)
|
|
22
|
+
.any?,
|
|
23
|
+
|
|
24
|
+
*(yield if block_given?)
|
|
25
|
+
] }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Infers enum values from inclusion and numericality validators
|
|
29
|
+
# that declare an `:in` option with multiple values.
|
|
30
|
+
def attribute_enum name
|
|
31
|
+
super { [
|
|
32
|
+
*[
|
|
33
|
+
*attribute_validators(name, :numericality),
|
|
34
|
+
*attribute_validators(name, :inclusion),
|
|
35
|
+
].filter_map { it.options[:in] },
|
|
36
|
+
|
|
37
|
+
*(yield if block_given?)
|
|
38
|
+
] }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Infers a constant value from validators that restrict the
|
|
42
|
+
# attribute to a single value: comparison/equality, single-item
|
|
43
|
+
# inclusion, or acceptance validators without `allow_nil`.
|
|
44
|
+
def attribute_const name
|
|
45
|
+
super { [
|
|
46
|
+
*[
|
|
47
|
+
*attribute_validators(name, :comparison),
|
|
48
|
+
*attribute_validators(name, :numericality),
|
|
49
|
+
].filter_map { it.options[:equal_to] },
|
|
50
|
+
|
|
51
|
+
*[
|
|
52
|
+
*attribute_validators(name, :inclusion),
|
|
53
|
+
*attribute_validators(name, :numericality),
|
|
54
|
+
].filter_map { it.options[:in] }
|
|
55
|
+
.select(&:one?)
|
|
56
|
+
.map(&:first),
|
|
57
|
+
|
|
58
|
+
*attribute_validators(name, :acceptance)
|
|
59
|
+
.reject { it.options[:allow_nil] }
|
|
60
|
+
.presence
|
|
61
|
+
&.any?,
|
|
62
|
+
|
|
63
|
+
*(yield if block_given?)
|
|
64
|
+
] }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Infers a regex pattern from format validators, combining
|
|
68
|
+
# multiple patterns into a single expression.
|
|
69
|
+
# Validators with `:without` are negated.
|
|
70
|
+
def attribute_format name
|
|
71
|
+
super { [
|
|
72
|
+
*attribute_validators(name, :format)
|
|
73
|
+
.map(&:options)
|
|
74
|
+
.filter_map { it[:with] or ~it[:without] rescue nil },
|
|
75
|
+
|
|
76
|
+
*(yield if block_given?)
|
|
77
|
+
] }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Infers minimum and maximum string length from length validators.
|
|
81
|
+
def attribute_length name
|
|
82
|
+
super { [
|
|
83
|
+
*attribute_validators(name, :length)
|
|
84
|
+
.map(&:options)
|
|
85
|
+
.map { {
|
|
86
|
+
minimum: it.values_at(:is, :minimum).find(&:itself),
|
|
87
|
+
maximum: it.values_at(:is, :maximum).find(&:itself),
|
|
88
|
+
} },
|
|
89
|
+
|
|
90
|
+
*(yield if block_given?)
|
|
91
|
+
] }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Infers numeric boundaries (minimum, maximum, greater_than,
|
|
95
|
+
# less_than, multiple_of) from comparison and numericality validators.
|
|
96
|
+
def attribute_boundaries name
|
|
97
|
+
super { [
|
|
98
|
+
*[
|
|
99
|
+
*attribute_validators(name, :comparison),
|
|
100
|
+
*attribute_validators(name, :numericality),
|
|
101
|
+
].map(&:options)
|
|
102
|
+
.map { {
|
|
103
|
+
minimum: it[:greater_than_or_equal_to],
|
|
104
|
+
greater_than: it[:greater_than],
|
|
105
|
+
less_than: it[:less_than],
|
|
106
|
+
maximum: it[:less_than_or_equal_to],
|
|
107
|
+
|
|
108
|
+
multiple_of: it[:multiple_of],
|
|
109
|
+
} },
|
|
110
|
+
|
|
111
|
+
*(yield if block_given?)
|
|
112
|
+
] }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Returns the validators for an attribute, optionally filtered
|
|
116
|
+
# by validator type (e.g., `:presence`, `:inclusion`, `:format`).
|
|
117
|
+
def attribute_validators name, type = nil
|
|
118
|
+
validators_on(name)
|
|
119
|
+
.grep(ActiveModel::Validations.const_get "#{type.to_s.camelize}Validator")
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionAI
|
|
4
|
+
module ModelSchema
|
|
5
|
+
# Extends schema inference with Active Record–specific behavior.
|
|
6
|
+
#
|
|
7
|
+
# Overrides the base inference methods to incorporate database column
|
|
8
|
+
# metadata (nullability, defaults, limits, decimal scale) and model-level
|
|
9
|
+
# features (defined enums, STI inheritance).
|
|
10
|
+
#
|
|
11
|
+
# Unlike the Active Model inference, which relies solely on validation
|
|
12
|
+
# declarations, this module reads the actual column schema to produce
|
|
13
|
+
# more precise constraints.
|
|
14
|
+
module Inference::ActiveRecord
|
|
15
|
+
include Memery
|
|
16
|
+
|
|
17
|
+
def foreign_keys = reflect_on_all_associations(:belongs_to)
|
|
18
|
+
.map(&:foreign_key)
|
|
19
|
+
|
|
20
|
+
def cache_columns = [
|
|
21
|
+
*(reflect_on_all_associations
|
|
22
|
+
.filter_map(&:counter_cache_column)
|
|
23
|
+
),
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
# Excludes DB-managed columns, since they are not typically part of the
|
|
29
|
+
# model’s input data.
|
|
30
|
+
def schema_attributes = super
|
|
31
|
+
.except(*[
|
|
32
|
+
*primary_key,
|
|
33
|
+
*foreign_keys.flatten,
|
|
34
|
+
:created_at, :updated_at,
|
|
35
|
+
locking_column,
|
|
36
|
+
*cache_columns,
|
|
37
|
+
].map(&:to_sym))
|
|
38
|
+
.merge(schema_reflections)
|
|
39
|
+
|
|
40
|
+
memoize def schema_reflections = nested_attributes_options
|
|
41
|
+
.filter_map { |name, options|
|
|
42
|
+
reflection = reflect_on_association name
|
|
43
|
+
|
|
44
|
+
[ name, {
|
|
45
|
+
type: reflection.collection? ? :array : :object,
|
|
46
|
+
of: reflection.klass.schema,
|
|
47
|
+
description: try(:human_attribute_name, name),
|
|
48
|
+
required: reflection_required?(name),
|
|
49
|
+
max_items: options[:limit],
|
|
50
|
+
}.compact ]
|
|
51
|
+
}
|
|
52
|
+
.to_h
|
|
53
|
+
|
|
54
|
+
# Determines whether an attribute is required by checking the column’s
|
|
55
|
+
# null constraint, in addition to any Active Model checks.
|
|
56
|
+
def attribute_required? name
|
|
57
|
+
super { [
|
|
58
|
+
!column_for_attribute(name).null,
|
|
59
|
+
|
|
60
|
+
*(yield if block_given?)
|
|
61
|
+
] }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Returns the default value for an attribute from the column defaults.
|
|
65
|
+
def attribute_default name
|
|
66
|
+
column_defaults[name.to_s]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Infers enum values from Active Record `enum` declarations and,
|
|
70
|
+
# for the inheritance column, includes valid STI class names (the base
|
|
71
|
+
# class and all descendants).
|
|
72
|
+
def attribute_enum name
|
|
73
|
+
super { [
|
|
74
|
+
defined_enums[name],
|
|
75
|
+
|
|
76
|
+
([self, *descendants]
|
|
77
|
+
.map(&:name) if
|
|
78
|
+
name.to_s == inheritance_column),
|
|
79
|
+
|
|
80
|
+
*(yield if block_given?)
|
|
81
|
+
] }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Infers a constant value from acceptance validators that do not allow
|
|
85
|
+
# nil, but only for non-nullable columns.
|
|
86
|
+
def attribute_const name
|
|
87
|
+
super { [
|
|
88
|
+
*attribute_validators(name, :acceptance)
|
|
89
|
+
&.select { it.options[:allow_nil] }
|
|
90
|
+
.presence
|
|
91
|
+
&.any?,
|
|
92
|
+
|
|
93
|
+
*(yield if block_given?)
|
|
94
|
+
] unless column_for_attribute(name).null }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Infers the maximum string length from the column’s database limit.
|
|
98
|
+
def attribute_length name
|
|
99
|
+
super { [
|
|
100
|
+
{
|
|
101
|
+
maximum: column_for_attribute(name).limit
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
*(yield if block_given?)
|
|
105
|
+
] }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# For decimal columns, sets `multiple_of` based on the column scale
|
|
109
|
+
# (e.g., scale 2 → multiple_of 0.01), ensuring the generated schema
|
|
110
|
+
# rejects values with more decimal places than the column supports.
|
|
111
|
+
def attribute_boundaries name
|
|
112
|
+
super { [
|
|
113
|
+
{
|
|
114
|
+
multiple_of: column_for_attribute(name)
|
|
115
|
+
.then.detect { it.type == :decimal }
|
|
116
|
+
&.then { 10 ** (-it.scale) },
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
*(yield if block_given?)
|
|
120
|
+
] }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def reflection_required? name
|
|
124
|
+
reflect_on_association(name)&.then do
|
|
125
|
+
case it
|
|
126
|
+
when ActiveRecord::Reflection::BelongsToReflection
|
|
127
|
+
not it.options[:optional]
|
|
128
|
+
when ActiveRecord::Reflection::HasOneReflection
|
|
129
|
+
it.options[:required] || false
|
|
130
|
+
else
|
|
131
|
+
false
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|