riffer 0.53.0 → 0.54.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/.claude/rules/rbs-inline.md +2 -2
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +15 -0
- data/Steepfile +19 -0
- data/docs/AGENTS.md +66 -4
- data/docs/OVERVIEW.md +1 -1
- data/docs/TOOLS.md +15 -12
- data/docs/providers/GEMINI.md +1 -0
- data/lib/riffer/agent/structured_output.rb +23 -3
- data/lib/riffer/params/any_of.rb +22 -0
- data/lib/riffer/params/format.rb +73 -0
- data/lib/riffer/params/param.rb +120 -17
- data/lib/riffer/params.rb +135 -24
- data/lib/riffer/providers/base.rb +9 -9
- data/lib/riffer/providers/gemini.rb +1 -0
- data/lib/riffer/version.rb +1 -1
- data/rbs_collection.lock.yaml +8 -0
- data/rbs_collection.yaml +2 -0
- data/sig/generated/riffer/agent/structured_output.rbs +6 -0
- data/sig/generated/riffer/params/any_of.rbs +13 -0
- data/sig/generated/riffer/params/format.rbs +39 -0
- data/sig/generated/riffer/params/param.rbs +22 -4
- data/sig/generated/riffer/params.rbs +34 -8
- data/sig/manifest.yaml +2 -1
- data/sig/manual/riffer/params/format.rbs +5 -0
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: da8c08b227ba3ddddaa025377b70f1dfb9acaa3c86b52ff9866446e8e908443c
|
|
4
|
+
data.tar.gz: 598cddab932d5f4e3c775847eafaba44d7d0fe21741539927fa1cad15cb1767e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d01ca1aa3fa2f183c93a7c783eb5d23040f8e76d403114abe4f53f2cb4a299b69046a0397c405380013467d0c035e9cb1b7e1fcc72b45e945a7a6c786159b274
|
|
7
|
+
data.tar.gz: 3f568d27f9b5cbd12d23be97acbe049d8e9dd70f817bc6375d273f2cae62d60cbc7ebf40d7588d516739107734a12f3b450863a13f327ed1e2d1d46b7f60ca1d
|
data/.claude/rules/rbs-inline.md
CHANGED
|
@@ -68,13 +68,13 @@ end
|
|
|
68
68
|
|
|
69
69
|
### Where stubs and stdlib deps live
|
|
70
70
|
|
|
71
|
-
Dependency signatures come from [`rbs collection`](https://github.com/ruby/gem_rbs_collection): `rbs_collection.yaml` pins the upstream repo to a commit SHA, the gem set is derived from `Gemfile.lock`, and `rbs_collection.lock.yaml` is committed (the installed `.gem_rbs_collection/` is gitignored). Steep auto-detects the config, so the `Steepfile` declares no `library` lines. `
|
|
71
|
+
Dependency signatures come from [`rbs collection`](https://github.com/ruby/gem_rbs_collection): `rbs_collection.yaml` pins the upstream repo to a commit SHA, the gem set is derived from `Gemfile.lock`, and `rbs_collection.lock.yaml` is committed (the installed `.gem_rbs_collection/` is gitignored). Steep auto-detects the config, so the `Steepfile` declares no `library` lines. Stdlib libraries that no gem in `Gemfile.lock` pulls in are hand-listed under `gems:` (`yaml`, `cgi`, `date`, `ipaddr`).
|
|
72
72
|
|
|
73
73
|
`bin/typecheck` runs `rbs collection update` before `steep check`, keeping the lock in sync with `Gemfile.lock`; CI fails on lock drift. So after a Dependabot gem bump, run `bin/typecheck` and commit the refreshed lock. To pull newer upstream stubs, bump `revision` in `rbs_collection.yaml` and run `bin/typecheck` — pinning is what makes `update` deterministic.
|
|
74
74
|
|
|
75
75
|
- `sig/_private/` — signatures that must **not** ship. RBS **skips** `_`-prefixed directories in library mode, so consumers never load them; riffer's own `steep check` does (via the `Steepfile`). Two kinds, by predictable path: external-gem signatures are named by gem at the top level — full stubs for the gems upstream has nothing for (`mcp.rbs`, `zeitwerk.rbs`, `opentelemetry.rbs`, `rspec.rbs`, `aws-sdk-core/*`) plus arity patches for the provider SDKs (`openai.rbs`, `anthropic.rbs`); riffer's own hidden stubs mirror `lib/` under `riffer/` (e.g. `riffer/providers/anthropic.rbs` narrows the private `client` method to the SDK-typed client). A stub must be **deleted** once the collection covers that gem, or RBS raises `DuplicatedMethodDefinitionError`.
|
|
76
76
|
- `sig/manual/` — hand-written riffer-only signatures that are **safe to ship**, for the few things rbs-inline can't generate _at all_ (mirroring `lib/`). In practice that's `extend self` modules (`riffer/agent/run.rbs`, `riffer/helpers/call_or_value.rbs`) and modeling an include applied dynamically (`riffer/tools/toolable.rbs`). SDK-free ivars are **not** hand-written here — declare them inline with `# @rbs` (see "Annotation Conventions"). SDK-typed signatures can't ship, so they go in `_private/riffer/providers/` (the narrowed `client`).
|
|
77
|
-
- `sig/manifest.yaml` — declares the **stdlib** RBS the shipped sigs reference (`uri`, `net-http`) so `rbs -r riffer` resolves them.
|
|
77
|
+
- `sig/manifest.yaml` — declares the **stdlib** RBS the shipped sigs reference (`uri`, `net-http`, `digest`, `ipaddr`) so `rbs -r riffer` resolves them.
|
|
78
78
|
|
|
79
79
|
## Workflow
|
|
80
80
|
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.54.0](https://github.com/janeapp/riffer/compare/riffer/v0.53.0...riffer/v0.54.0) (2026-10-20)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **params:** add `format:` for string formats ([#497](https://github.com/janeapp/riffer/issues/497)) ([3c2dec8](https://github.com/janeapp/riffer/commit/3c2dec8e70b28de2d7b9d655e8d17ce85dde6812))
|
|
14
|
+
* **params:** add `Riffer::Params::AnyOf` ([#491](https://github.com/janeapp/riffer/issues/491)) ([65493ca](https://github.com/janeapp/riffer/commit/65493caa8073cf86983ec27dc6fb84ede068b6b9))
|
|
15
|
+
* **params:** add arrays of arrays with nested `of:` ([#498](https://github.com/janeapp/riffer/issues/498)) ([2b3449c](https://github.com/janeapp/riffer/commit/2b3449c61474f5cc79643903c08668a8fd9db8b6))
|
|
16
|
+
* **structured_output:** append prompted schema to user messages ([#492](https://github.com/janeapp/riffer/issues/492)) ([332db21](https://github.com/janeapp/riffer/commit/332db216d2e7716904d5ff4a9c1d9d42e128ac83))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
* **params:** apply enum to each item of an Array of: param ([#493](https://github.com/janeapp/riffer/issues/493)) ([09da9b0](https://github.com/janeapp/riffer/commit/09da9b0ccb0e63b2ed3202a28546371b17118e8c))
|
|
22
|
+
|
|
8
23
|
## [0.53.0](https://github.com/janeapp/riffer/compare/riffer/v0.52.1...riffer/v0.53.0) (2026-10-13)
|
|
9
24
|
|
|
10
25
|
|
data/Steepfile
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
D = Steep::Diagnostic
|
|
4
4
|
|
|
5
|
+
# RBS 4.2.0 re-validates every declaration of a module each time its type
|
|
6
|
+
# params are read, which is quadratic in how often a module is reopened. The
|
|
7
|
+
# openai and anthropic gems reopen their namespace ~1,600 times each, so
|
|
8
|
+
# loading signatures takes over a minute per Steep worker. Fixed upstream in
|
|
9
|
+
# ruby/rbs a5458a83; delete this once the bundle has an RBS release newer
|
|
10
|
+
# than 4.2.
|
|
11
|
+
if RBS::Environment::ModuleEntry.instance_method(:validate_type_params).owner == RBS::Environment::ModuleEntry
|
|
12
|
+
validate_once = Module.new do
|
|
13
|
+
def validate_type_params
|
|
14
|
+
return if @validated_decl_count == context_decls.size
|
|
15
|
+
|
|
16
|
+
super
|
|
17
|
+
@validated_decl_count = context_decls.size
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
RBS::Environment::ModuleEntry.prepend(validate_once)
|
|
21
|
+
RBS::Environment::ClassEntry.prepend(validate_once)
|
|
22
|
+
end
|
|
23
|
+
|
|
5
24
|
target :lib do
|
|
6
25
|
signature "sig/generated"
|
|
7
26
|
signature "sig/manual"
|
data/docs/AGENTS.md
CHANGED
|
@@ -212,12 +212,12 @@ class SentimentAgent < Riffer::Agent
|
|
|
212
212
|
end
|
|
213
213
|
```
|
|
214
214
|
|
|
215
|
-
With `strategy: :prompted`, the provider never receives its native structured-output field. Instead, an instruction
|
|
215
|
+
With `strategy: :prompted`, the provider never receives its native structured-output field. Instead, an instruction is appended to the end of every user message on every request of the run, after consecutive user messages are merged (it is not stored in `session.messages`, and the system instructions are left untouched). The instruction carries the JSON Schema, an example of the expected shape with placeholder values (`"<string>"`, enum values as `"a | b"`, an `AnyOf` field as its first variant), and a nudge to reply with the JSON right away unless the request needs a tool. The reply is parsed and validated exactly as above: `response.content` is the reply's JSON text, `response.structured_output` holds the validated object, and a reply that fails parsing or validation ends with `:invalid_structured_output`. Because nothing forces the model to comply, expect more `:invalid_structured_output` outcomes than with the native strategy.
|
|
216
216
|
|
|
217
217
|
| Strategy | Behavior |
|
|
218
218
|
| ------------------- | --------------------------------------------------------------- |
|
|
219
219
|
| `:native` (default) | Schema sent through the provider's structured-output field |
|
|
220
|
-
| `:prompted` | Schema
|
|
220
|
+
| `:prompted` | Schema appended to each user message; experimental, may change |
|
|
221
221
|
|
|
222
222
|
Riffer does not pick a strategy per model — the choice is always yours. The strategy is copied to subclasses and survives [serialization](SERIALIZATION.md). An unknown value raises `Riffer::ArgumentError`. `strategy:` can also be passed with a `Riffer::Params` instance: `structured_output params, strategy: :prompted`.
|
|
223
223
|
|
|
@@ -249,7 +249,46 @@ structured_output do
|
|
|
249
249
|
end
|
|
250
250
|
```
|
|
251
251
|
|
|
252
|
-
|
|
252
|
+
`of:` takes a primitive type: `String`, `Integer`, `Float`, `Riffer::Params::Boolean`, `TrueClass`, `FalseClass`.
|
|
253
|
+
|
|
254
|
+
Add `enum:` to limit each item to a list of values:
|
|
255
|
+
|
|
256
|
+
```ruby
|
|
257
|
+
structured_output do
|
|
258
|
+
required :tags, Array, of: String, enum: %w[urgent billing technical]
|
|
259
|
+
end
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
An item outside the list fails validation with its index: `tags[1] must be one of: urgent, billing, technical`.
|
|
263
|
+
|
|
264
|
+
For an array of arrays, put the type in brackets. Each pair of brackets adds a level, and `enum:` and `format:` apply to the innermost items:
|
|
265
|
+
|
|
266
|
+
```ruby
|
|
267
|
+
structured_output do
|
|
268
|
+
required :rows, Array, of: [Integer], description: "Seat numbers, one array per row"
|
|
269
|
+
required :board, Array, of: [String], enum: %w[x o]
|
|
270
|
+
end
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
A row that isn't an array fails validation with its index: `rows[1] must be an array`.
|
|
274
|
+
|
|
275
|
+
#### Formats
|
|
276
|
+
|
|
277
|
+
Use `format:` on a `String`, or on an `Array` whose innermost type is `String`, to require a standard string format:
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
structured_output do
|
|
281
|
+
required :born_on, String, format: "date"
|
|
282
|
+
optional :email, String, format: "email"
|
|
283
|
+
required :visit_ids, Array, of: String, format: "uuid"
|
|
284
|
+
end
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Supported formats: `date-time`, `time`, `date`, `duration`, `email`, `hostname`, `ipv4`, `ipv6`, `uuid`. Dates and times use RFC 3339 syntax, and a `time` may leave out its UTC offset. `email` and `hostname` are checked loosely, rejecting only values that are clearly not an address or a host name.
|
|
288
|
+
|
|
289
|
+
A value that doesn't match fails validation, with its index on an array: `born_on must be a valid date`, `visit_ids[1] must be a valid uuid`. Values stay strings.
|
|
290
|
+
|
|
291
|
+
A model that must match a format writes a matching value even when it has none, so it may invent an email. Make such a field `optional` so the model can return `null`.
|
|
253
292
|
|
|
254
293
|
#### Arrays of Objects
|
|
255
294
|
|
|
@@ -285,9 +324,32 @@ structured_output do
|
|
|
285
324
|
end
|
|
286
325
|
```
|
|
287
326
|
|
|
327
|
+
#### Either/Or Shapes
|
|
328
|
+
|
|
329
|
+
Use `Riffer::Params::AnyOf` when a field must be one of several object shapes. Each `variant` block defines one shape, the same way a `Hash` block does:
|
|
330
|
+
|
|
331
|
+
```ruby
|
|
332
|
+
structured_output do
|
|
333
|
+
required :result, Riffer::Params::AnyOf, description: "The address, or why none was found" do
|
|
334
|
+
variant do
|
|
335
|
+
required :street, String, description: "Street address"
|
|
336
|
+
required :city, String, description: "City"
|
|
337
|
+
end
|
|
338
|
+
variant do
|
|
339
|
+
required :error_code, String, enum: %w[no_address multiple_addresses]
|
|
340
|
+
required :error_message, String
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
The schema sends the field as `anyOf`, with one object per variant. An answer must match one variant completely: every required field of that variant, and no field from outside it. Variants are tried in order and the first match is returned. When none match, the error lists each variant's problems: `result must match one of the variants: variant 1 (street is required, city is required), variant 2 (error_code is required, error_message is required)`.
|
|
347
|
+
|
|
348
|
+
An optional `AnyOf` field can also be `null`.
|
|
349
|
+
|
|
288
350
|
#### Limitations
|
|
289
351
|
|
|
290
|
-
A `Hash` param requires a block, and an `Array` param requires a block or `of:`. Using both `of:` and a block raises `Riffer::ArgumentError`. Using `of:` with a non-primitive type (e.g. `of: Hash`) also raises `Riffer::ArgumentError`.
|
|
352
|
+
A `Hash` param requires a block, and an `Array` param requires a block or `of:`. Using both `of:` and a block raises `Riffer::ArgumentError`. Using `of:` with a non-primitive type (e.g. `of: Hash` or `of: [Hash]`), or with more than one type (`of: [Integer, String]`), also raises `Riffer::ArgumentError`. An `AnyOf` param requires a block with at least two `variant`s, and can't be used with `of:` or `enum:`. Using `enum:` on a `Hash`, or on an `Array` without `of:`, also raises `Riffer::ArgumentError`. So does `format:` with an unsupported format, on a type other than `String` or an `Array` whose innermost type is `String`, or together with `enum:`.
|
|
291
353
|
|
|
292
354
|
Structured output is not compatible with streaming — calling `stream` on an agent with structured output configured raises `Riffer::ArgumentError`.
|
|
293
355
|
|
data/docs/OVERVIEW.md
CHANGED
|
@@ -39,7 +39,7 @@ See [Tools](TOOLS.md) for details.
|
|
|
39
39
|
|
|
40
40
|
### Structured Output
|
|
41
41
|
|
|
42
|
-
Agents can return structured JSON responses that conform to a schema. The response is automatically parsed and validated. Schemas support nested objects (`Hash`), typed arrays (`Array, of:`),
|
|
42
|
+
Agents can return structured JSON responses that conform to a schema. The response is automatically parsed and validated. Schemas support nested objects (`Hash`), typed arrays (`Array, of:`), arrays of arrays (`Array, of: [Integer]`), arrays of objects (`Array` with block), and either/or shapes (`Riffer::Params::AnyOf`):
|
|
43
43
|
|
|
44
44
|
```ruby
|
|
45
45
|
class SentimentAgent < Riffer::Agent
|
data/docs/TOOLS.md
CHANGED
|
@@ -82,7 +82,8 @@ end
|
|
|
82
82
|
Options:
|
|
83
83
|
|
|
84
84
|
- `description` - Human-readable description for the LLM
|
|
85
|
-
- `enum` - Array of allowed values
|
|
85
|
+
- `enum` - Array of allowed values (for the innermost items of an `Array` with `of:`)
|
|
86
|
+
- `format` - String format such as `"date"` or `"email"` (for the innermost `String` items of an `Array`; see [Formats](AGENTS.md#formats))
|
|
86
87
|
|
|
87
88
|
### optional
|
|
88
89
|
|
|
@@ -99,19 +100,21 @@ Options:
|
|
|
99
100
|
|
|
100
101
|
- `description` - Human-readable description
|
|
101
102
|
- `default` - Default value when not provided
|
|
102
|
-
- `enum` - Array of allowed values
|
|
103
|
+
- `enum` - Array of allowed values (for the innermost items of an `Array` with `of:`)
|
|
104
|
+
- `format` - String format such as `"date"` or `"email"` (for the innermost `String` items of an `Array`; see [Formats](AGENTS.md#formats))
|
|
103
105
|
|
|
104
106
|
### Supported Types
|
|
105
107
|
|
|
106
|
-
| Ruby Type | JSON Schema Type
|
|
107
|
-
| -------------------------- |
|
|
108
|
-
| `String` | `string`
|
|
109
|
-
| `Integer` | `integer`
|
|
110
|
-
| `Float` | `number`
|
|
111
|
-
| `Riffer::Params::Boolean` | `boolean`
|
|
112
|
-
| `TrueClass` / `FalseClass` | `boolean`
|
|
113
|
-
| `Array` | `array`
|
|
114
|
-
| `Hash` | `object`
|
|
108
|
+
| Ruby Type | JSON Schema Type |
|
|
109
|
+
| -------------------------- | ------------------ |
|
|
110
|
+
| `String` | `string` |
|
|
111
|
+
| `Integer` | `integer` |
|
|
112
|
+
| `Float` | `number` |
|
|
113
|
+
| `Riffer::Params::Boolean` | `boolean` |
|
|
114
|
+
| `TrueClass` / `FalseClass` | `boolean` |
|
|
115
|
+
| `Array` | `array` |
|
|
116
|
+
| `Hash` | `object` |
|
|
117
|
+
| `Riffer::Params::AnyOf` | `anyOf` of objects |
|
|
115
118
|
|
|
116
119
|
`Riffer::Params::Boolean` is the preferred way to declare boolean parameters. `TrueClass` and `FalseClass` continue to work for backwards compatibility.
|
|
117
120
|
|
|
@@ -119,7 +122,7 @@ A `Float` param accepts a whole number too, since JSON Schema's `number` covers
|
|
|
119
122
|
|
|
120
123
|
### Nested Parameters
|
|
121
124
|
|
|
122
|
-
Tool params support the same nested DSL as structured output — nested objects (`Hash` with block), typed arrays (`Array, of:`),
|
|
125
|
+
Tool params support the same nested DSL as structured output — nested objects (`Hash` with block), typed arrays (`Array, of:`), arrays of arrays (`Array, of: [Integer]`), arrays of objects (`Array` with block), and either/or shapes (`Riffer::Params::AnyOf` with `variant` blocks). See the [structured output section in Agents](AGENTS.md#nested-objects) for full syntax.
|
|
123
126
|
|
|
124
127
|
```ruby
|
|
125
128
|
class CreateOrderTool < Riffer::Tool
|
data/docs/providers/GEMINI.md
CHANGED
|
@@ -172,3 +172,4 @@ A `FilePart.from_url` source works too — riffer downloads and base64-encodes i
|
|
|
172
172
|
|
|
173
173
|
- **No web search** - Gemini's standard API does not include a web search tool
|
|
174
174
|
- **Tool call IDs** - Gemini does not return unique call IDs for tool invocations; IDs are generated client-side
|
|
175
|
+
- **String enums only** - Gemini rejects non-string `enum` values, including on `Array, of:` items
|
|
@@ -30,9 +30,14 @@ class Riffer::Agent::StructuredOutput
|
|
|
30
30
|
#--
|
|
31
31
|
#: () -> String
|
|
32
32
|
def prompt_instructions
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
33
|
+
schema = json_schema
|
|
34
|
+
[
|
|
35
|
+
"When you give your final answer, respond with only a JSON object that conforms to " \
|
|
36
|
+
"the following JSON Schema, with no other text before or after it:",
|
|
37
|
+
JSON.generate(schema),
|
|
38
|
+
"Example of the shape (placeholder values):\n#{JSON.generate(placeholder(schema))}",
|
|
39
|
+
"Only call tools when the request needs them. If you can answer directly, reply with the JSON object right away.",
|
|
40
|
+
].join("\n\n")
|
|
36
41
|
end
|
|
37
42
|
|
|
38
43
|
#--
|
|
@@ -46,4 +51,19 @@ class Riffer::Agent::StructuredOutput
|
|
|
46
51
|
rescue Riffer::ValidationError => e
|
|
47
52
|
Result.new(error: "Validation error: #{e.message}")
|
|
48
53
|
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
#--
|
|
58
|
+
#: (Hash[Symbol, untyped]) -> untyped
|
|
59
|
+
def placeholder(schema)
|
|
60
|
+
return schema[:enum].join(" | ") if schema[:enum]
|
|
61
|
+
return placeholder(schema[:anyOf].first) if schema[:anyOf]
|
|
62
|
+
|
|
63
|
+
case schema[:type]
|
|
64
|
+
when "object" then (schema[:properties] || {}).transform_values { |property| placeholder(property) }
|
|
65
|
+
when "array" then schema[:items] ? [placeholder(schema[:items])] : []
|
|
66
|
+
else "<#{schema[:type]}>"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
49
69
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
class Riffer::Params::AnyOf
|
|
5
|
+
attr_reader :variants #: Array[Riffer::Params] # @dynamic variants
|
|
6
|
+
|
|
7
|
+
#--
|
|
8
|
+
#: () -> void
|
|
9
|
+
def initialize
|
|
10
|
+
@variants = []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
#--
|
|
14
|
+
#: () ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> void
|
|
15
|
+
def variant(&block)
|
|
16
|
+
raise Riffer::ArgumentError, "variant requires a block defining its properties" unless block
|
|
17
|
+
|
|
18
|
+
params = Riffer::Params.new
|
|
19
|
+
params.instance_eval(&block)
|
|
20
|
+
@variants << params
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require "date"
|
|
5
|
+
require "ipaddr"
|
|
6
|
+
|
|
7
|
+
module Riffer::Params::Format
|
|
8
|
+
extend self
|
|
9
|
+
|
|
10
|
+
DATE = /\A(\d{4})-(\d{2})-(\d{2})\z/ #: Regexp
|
|
11
|
+
|
|
12
|
+
DATE_TIME = /\A(\d{4})-(\d{2})-(\d{2})[Tt]([01]\d|2[0-3]):[0-5]\d:([0-5]\d|60)(\.\d+)?([Zz]|[+-]([01]\d|2[0-3]):[0-5]\d)\z/ #: Regexp
|
|
13
|
+
|
|
14
|
+
# The offset is optional: Anthropic's grammar emits times without one.
|
|
15
|
+
TIME = /\A([01]\d|2[0-3]):[0-5]\d:([0-5]\d|60)(\.\d+)?([Zz]|[+-]([01]\d|2[0-3]):[0-5]\d)?\z/ #: Regexp
|
|
16
|
+
|
|
17
|
+
DURATION = /\AP(?!\z)((\d+Y)?(\d+M)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?|\d+W)\z/ #: Regexp
|
|
18
|
+
|
|
19
|
+
UUID = /\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/ #: Regexp
|
|
20
|
+
|
|
21
|
+
# The WHATWG HTML pattern: no single pattern matches RFC 5322 exactly, so
|
|
22
|
+
# this one only rules out values that are clearly not an address.
|
|
23
|
+
EMAIL = %r{\A[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+@[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*\z} #: Regexp
|
|
24
|
+
|
|
25
|
+
HOSTNAME = /\A(?=.{1,253}\.?\z)[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*\.?\z/ #: Regexp
|
|
26
|
+
|
|
27
|
+
# The formats OpenAI, Anthropic and Bedrock all accept. OpenAI rejects uri.
|
|
28
|
+
CHECKS = {
|
|
29
|
+
"date-time" => ->(value) { calendar_date?(DATE_TIME, value) },
|
|
30
|
+
"time" => ->(value) { TIME.match?(value) },
|
|
31
|
+
"date" => ->(value) { calendar_date?(DATE, value) },
|
|
32
|
+
"duration" => ->(value) { DURATION.match?(value) },
|
|
33
|
+
"email" => ->(value) { EMAIL.match?(value) },
|
|
34
|
+
"hostname" => ->(value) { HOSTNAME.match?(value) },
|
|
35
|
+
"ipv4" => ->(value) { ip_address?(value, &:ipv4?) },
|
|
36
|
+
"ipv6" => ->(value) { ip_address?(value, &:ipv6?) },
|
|
37
|
+
"uuid" => ->(value) { UUID.match?(value) },
|
|
38
|
+
}.freeze #: Hash[String, ^(String) -> bool]
|
|
39
|
+
|
|
40
|
+
private_constant :DATE, :DATE_TIME, :TIME, :DURATION, :UUID, :EMAIL, :HOSTNAME, :CHECKS
|
|
41
|
+
|
|
42
|
+
NAMES = CHECKS.keys.freeze #: Array[String]
|
|
43
|
+
|
|
44
|
+
#--
|
|
45
|
+
#: (String, String) -> bool
|
|
46
|
+
def valid?(format, value)
|
|
47
|
+
CHECKS.fetch(format).call(value)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
#--
|
|
53
|
+
#: (Regexp, String) -> bool
|
|
54
|
+
def calendar_date?(pattern, value)
|
|
55
|
+
match = pattern.match(value)
|
|
56
|
+
return false unless match
|
|
57
|
+
|
|
58
|
+
# RFC 3339 dates are proleptic Gregorian; Date defaults to the Julian calendar before 1582.
|
|
59
|
+
Date.valid_date?(match[1].to_i, match[2].to_i, match[3].to_i, Date::GREGORIAN)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
#--
|
|
63
|
+
#: (String) { (IPAddr) -> bool } -> bool
|
|
64
|
+
def ip_address?(value)
|
|
65
|
+
# IPAddr also accepts a prefix (10.0.0.0/24), a zone ID (fe80::1%eth0) and
|
|
66
|
+
# brackets ([::1]), none of which are part of an address.
|
|
67
|
+
return false if value.match?(%r{[/%\[\]]})
|
|
68
|
+
|
|
69
|
+
yield IPAddr.new(value)
|
|
70
|
+
rescue IPAddr::Error
|
|
71
|
+
false
|
|
72
|
+
end
|
|
73
|
+
end
|
data/lib/riffer/params/param.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
# rbs_inline: enabled
|
|
3
3
|
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
4
6
|
class Riffer::Params::Param
|
|
5
7
|
TYPE_MAPPINGS = {
|
|
6
8
|
String => "string",
|
|
@@ -34,43 +36,99 @@ class Riffer::Params::Param
|
|
|
34
36
|
|
|
35
37
|
attr_reader :enum #: Array[untyped]? # @dynamic enum
|
|
36
38
|
|
|
39
|
+
attr_reader :format #: String? # @dynamic format
|
|
40
|
+
|
|
37
41
|
attr_reader :default #: untyped # @dynamic default
|
|
38
42
|
|
|
39
43
|
attr_reader :item_type #: Module? # @dynamic item_type
|
|
40
44
|
|
|
45
|
+
attr_reader :dimensions #: Integer # @dynamic dimensions
|
|
46
|
+
|
|
41
47
|
attr_reader :nested_params #: Riffer::Params? # @dynamic nested_params
|
|
42
48
|
|
|
49
|
+
attr_reader :variants #: Array[Riffer::Params]? # @dynamic variants
|
|
50
|
+
|
|
43
51
|
#--
|
|
44
52
|
#: (Symbol, Hash[Symbol, untyped], required: bool) -> Riffer::Params::Param
|
|
45
53
|
def self.from_json_schema(name, schema, required:)
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
variants = resolve_variants(schema[:anyOf]) if schema.key?(:anyOf)
|
|
55
|
+
ruby_type = variants ? Riffer::Params::AnyOf : json_type_to_ruby(schema[:type])
|
|
56
|
+
nested = resolve_nested_params(ruby_type, schema)
|
|
57
|
+
item_type, dimensions, items = resolve_typed_items(schema) if ruby_type == Array && !nested
|
|
48
58
|
|
|
49
59
|
new(
|
|
50
60
|
name: name,
|
|
51
61
|
type: ruby_type,
|
|
52
62
|
required: required,
|
|
53
63
|
description: schema[:description],
|
|
54
|
-
enum: schema[:enum],
|
|
64
|
+
enum: schema[:enum] || items&.dig(:enum),
|
|
65
|
+
format: schema[:format] || items&.dig(:format),
|
|
55
66
|
default: schema[:default],
|
|
56
67
|
item_type: item_type,
|
|
68
|
+
dimensions: dimensions || 1,
|
|
57
69
|
nested_params: nested,
|
|
70
|
+
variants: variants,
|
|
58
71
|
)
|
|
59
72
|
end
|
|
60
73
|
|
|
61
74
|
#--
|
|
62
|
-
#: (
|
|
63
|
-
def self.
|
|
64
|
-
|
|
65
|
-
|
|
75
|
+
#: (Array[Hash[Symbol, untyped]]) -> Array[Riffer::Params]
|
|
76
|
+
def self.resolve_variants(branches)
|
|
77
|
+
# Strict schemas add a null branch to optional fields; nullability comes from +required+.
|
|
78
|
+
variants = branches.reject { |branch| branch[:type] == "null" }.map do |branch|
|
|
79
|
+
raise Riffer::ArgumentError, "Unsupported anyOf branch: #{branch.to_json}" unless branch[:type] == "object"
|
|
66
80
|
|
|
81
|
+
Riffer::Params.from_json_schema(branch)
|
|
82
|
+
end
|
|
83
|
+
raise Riffer::ArgumentError, "anyOf requires at least two object branches" if variants.size < 2
|
|
84
|
+
|
|
85
|
+
variants
|
|
86
|
+
end
|
|
87
|
+
private_class_method :resolve_variants
|
|
88
|
+
|
|
89
|
+
#--
|
|
90
|
+
#: (Module, Hash[Symbol, untyped]) -> Riffer::Params?
|
|
91
|
+
def self.resolve_nested_params(ruby_type, schema)
|
|
92
|
+
if ruby_type == Hash && schema[:properties]
|
|
93
|
+
Riffer::Params.from_json_schema(schema)
|
|
94
|
+
elsif ruby_type == Array && schema[:items].is_a?(Hash) && schema[:items][:properties]
|
|
95
|
+
Riffer::Params.from_json_schema(schema[:items])
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
private_class_method :resolve_nested_params
|
|
99
|
+
|
|
100
|
+
#--
|
|
101
|
+
#: (Hash[Symbol, untyped]) -> [Module, Integer, Hash[Symbol, untyped]]?
|
|
102
|
+
def self.resolve_typed_items(schema)
|
|
67
103
|
items = schema[:items]
|
|
68
|
-
return
|
|
69
|
-
return [nil, Riffer::Params.from_json_schema(items)] if items[:properties]
|
|
104
|
+
return unless items.is_a?(Hash)
|
|
70
105
|
|
|
71
|
-
|
|
106
|
+
dimensions = 1
|
|
107
|
+
# Riffer only writes nested arrays with enum/format and a single primitive type on the innermost
|
|
108
|
+
# items, and no nullable inner arrays; any other shape would read back with a different meaning.
|
|
109
|
+
while json_type_to_ruby(items[:type]) == Array
|
|
110
|
+
inner = items[:items]
|
|
111
|
+
unless items[:type] == "array" && inner.is_a?(Hash) && !items.keys.intersect?(%i[enum format])
|
|
112
|
+
raise Riffer::ArgumentError, "Unsupported nested array: #{items.to_json}"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
items = inner
|
|
116
|
+
dimensions += 1
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
item_type = json_type_to_ruby(items[:type])
|
|
120
|
+
if dimensions > 1
|
|
121
|
+
if schema.keys.intersect?(%i[enum format])
|
|
122
|
+
raise Riffer::ArgumentError, "Unsupported nested array: #{schema.to_json}"
|
|
123
|
+
end
|
|
124
|
+
unless items[:type].is_a?(String) && PRIMITIVE_TYPES.include?(item_type)
|
|
125
|
+
raise Riffer::ArgumentError, "Unsupported nested array items: #{items.to_json}"
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
[item_type, dimensions, items]
|
|
72
130
|
end
|
|
73
|
-
private_class_method :
|
|
131
|
+
private_class_method :resolve_typed_items
|
|
74
132
|
|
|
75
133
|
#--
|
|
76
134
|
#: (untyped) -> Module
|
|
@@ -81,16 +139,27 @@ class Riffer::Params::Param
|
|
|
81
139
|
private_class_method :json_type_to_ruby
|
|
82
140
|
|
|
83
141
|
#--
|
|
84
|
-
#: (name: Symbol, type: Module, required: bool, ?description: String?, ?enum: Array[untyped]?, ?default: untyped, ?item_type: Module?, ?nested_params: Riffer::Params?) -> void
|
|
85
|
-
def initialize(name:, type:, required:, description: nil, enum: nil,
|
|
142
|
+
#: (name: Symbol, type: Module, required: bool, ?description: String?, ?enum: Array[untyped]?, ?format: String?, ?default: untyped, ?item_type: Module?, ?dimensions: Integer, ?nested_params: Riffer::Params?, ?variants: Array[Riffer::Params]?) -> void
|
|
143
|
+
def initialize(name:, type:, required:, description: nil, enum: nil, format: nil, default: nil, item_type: nil,
|
|
144
|
+
dimensions: 1, nested_params: nil, variants: nil)
|
|
145
|
+
if type == Riffer::Params::AnyOf && variants.nil?
|
|
146
|
+
raise Riffer::ArgumentError, "#{name}: an AnyOf param requires variants"
|
|
147
|
+
end
|
|
148
|
+
|
|
86
149
|
@name = name.to_sym
|
|
87
150
|
@type = type
|
|
88
151
|
@required = required
|
|
89
152
|
@description = description
|
|
90
153
|
@enum = enum
|
|
154
|
+
# Frozen and deduplicated, so copies never share a mutable string.
|
|
155
|
+
@format = format.is_a?(String) ? -format : format
|
|
91
156
|
@default = default
|
|
92
157
|
@item_type = item_type
|
|
158
|
+
@dimensions = dimensions
|
|
93
159
|
@nested_params = nested_params
|
|
160
|
+
@variants = variants
|
|
161
|
+
# Checked here rather than in Params so from_json_schema enforces it too.
|
|
162
|
+
validate_format! if format
|
|
94
163
|
end
|
|
95
164
|
|
|
96
165
|
#--
|
|
@@ -102,6 +171,8 @@ class Riffer::Params::Param
|
|
|
102
171
|
[true, false].include?(value)
|
|
103
172
|
elsif type == Float
|
|
104
173
|
value.is_a?(Numeric)
|
|
174
|
+
elsif type == Riffer::Params::AnyOf
|
|
175
|
+
value.is_a?(Hash)
|
|
105
176
|
else
|
|
106
177
|
value.is_a?(type)
|
|
107
178
|
end
|
|
@@ -110,6 +181,8 @@ class Riffer::Params::Param
|
|
|
110
181
|
#--
|
|
111
182
|
#: () -> String
|
|
112
183
|
def type_name
|
|
184
|
+
return "object" if type == Riffer::Params::AnyOf
|
|
185
|
+
|
|
113
186
|
TYPE_MAPPINGS[type] || type.to_s.downcase
|
|
114
187
|
end
|
|
115
188
|
|
|
@@ -120,9 +193,21 @@ class Riffer::Params::Param
|
|
|
120
193
|
# Nullable so strict providers can distinguish absent from present.
|
|
121
194
|
nullable = strict && !required
|
|
122
195
|
|
|
196
|
+
if type == Riffer::Params::AnyOf && variants
|
|
197
|
+
schema = { anyOf: variants.map { |variant| variant.to_json_schema(strict: strict) } } #: Hash[Symbol, untyped]
|
|
198
|
+
schema[:anyOf] << { type: "null" } if nullable
|
|
199
|
+
schema[:description] = description if description
|
|
200
|
+
schema[:default] = default unless strict || default.nil?
|
|
201
|
+
return schema
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# A typed array's enum and format constrain its items, not the array.
|
|
205
|
+
value_enum = enum unless item_type
|
|
206
|
+
value_format = format unless item_type
|
|
207
|
+
|
|
123
208
|
# Providers like Anthropic reject a nullable type union combined with enum.
|
|
124
|
-
if nullable &&
|
|
125
|
-
schema = { anyOf: [{ type: type_name, enum:
|
|
209
|
+
if nullable && value_enum
|
|
210
|
+
schema = { anyOf: [{ type: type_name, enum: value_enum }, { type: "null" }] } #: Hash[Symbol, untyped]
|
|
126
211
|
schema[:description] = description if description
|
|
127
212
|
return schema
|
|
128
213
|
end
|
|
@@ -132,7 +217,8 @@ class Riffer::Params::Param
|
|
|
132
217
|
|
|
133
218
|
schema = { type: type } #: Hash[Symbol, untyped]
|
|
134
219
|
schema[:description] = description if description
|
|
135
|
-
schema[:enum] =
|
|
220
|
+
schema[:enum] = value_enum if value_enum
|
|
221
|
+
schema[:format] = value_format if value_format
|
|
136
222
|
# Strict providers reject the +default+ keyword; emit it only in
|
|
137
223
|
# non-strict mode, where it makes the schema a lossless round-trip
|
|
138
224
|
# source for +from_json_schema+.
|
|
@@ -141,7 +227,9 @@ class Riffer::Params::Param
|
|
|
141
227
|
if self.type == Array && nested_params
|
|
142
228
|
schema[:items] = nested_params.to_json_schema(strict: strict)
|
|
143
229
|
elsif self.type == Array && item_type
|
|
144
|
-
|
|
230
|
+
items = { type: TYPE_MAPPINGS[item_type], enum: enum, format: format }.compact #: Hash[Symbol, untyped]
|
|
231
|
+
(dimensions - 1).times { items = { type: "array", items: items } }
|
|
232
|
+
schema[:items] = items
|
|
145
233
|
elsif self.type == Hash && nested_params
|
|
146
234
|
# The nested schema carries its own type: "object", which would clobber a nullable union.
|
|
147
235
|
schema.merge!(nested_params.to_json_schema(strict: strict).except(:type))
|
|
@@ -160,10 +248,25 @@ class Riffer::Params::Param
|
|
|
160
248
|
def initialize_copy(source)
|
|
161
249
|
super
|
|
162
250
|
@nested_params = source.nested_params&.dup
|
|
251
|
+
@variants = source.variants&.map(&:dup)
|
|
163
252
|
@enum = Riffer::Helpers::DeepDup.call(source.enum)
|
|
164
253
|
@default = Riffer::Helpers::DeepDup.call(source.default)
|
|
165
254
|
end
|
|
166
255
|
|
|
256
|
+
#--
|
|
257
|
+
#: () -> void
|
|
258
|
+
def validate_format!
|
|
259
|
+
unless Riffer::Params::Format::NAMES.include?(format)
|
|
260
|
+
raise Riffer::ArgumentError,
|
|
261
|
+
"#{name}: format: must be a supported format (#{Riffer::Params::Format::NAMES.join(', ')}), " \
|
|
262
|
+
"got #{format.inspect}"
|
|
263
|
+
end
|
|
264
|
+
unless type == String || (type == Array && item_type == String)
|
|
265
|
+
raise Riffer::ArgumentError, "#{name}: format: requires a String or an Array whose innermost type is String"
|
|
266
|
+
end
|
|
267
|
+
raise Riffer::ArgumentError, "#{name}: format: cannot be used with enum:" if enum
|
|
268
|
+
end
|
|
269
|
+
|
|
167
270
|
#--
|
|
168
271
|
#: () -> void
|
|
169
272
|
def validate_strict_shape!
|
data/lib/riffer/params.rb
CHANGED
|
@@ -29,33 +29,45 @@ class Riffer::Params
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
#--
|
|
32
|
-
#: (Symbol,
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
#: (Symbol, singleton(Riffer::Params::AnyOf), ?description: String?) { (Riffer::Params::AnyOf) [self: Riffer::Params::AnyOf] -> void } -> void
|
|
33
|
+
#: (Symbol, Module, ?description: String?, ?enum: Array[untyped]?, ?format: String?, ?of: (Module | Array[untyped])?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> void
|
|
34
|
+
def required(name, type, description: nil, enum: nil, format: nil, of: nil, &)
|
|
35
|
+
item_type, dimensions = unwrap_of(of)
|
|
36
|
+
nested = build_nested(type, item_type, enum, &) unless type == Riffer::Params::AnyOf
|
|
37
|
+
variants = build_variants(of, enum, &) if type == Riffer::Params::AnyOf
|
|
35
38
|
@parameters << Riffer::Params::Param.new(
|
|
36
39
|
name: name,
|
|
37
40
|
type: type,
|
|
38
41
|
required: true,
|
|
39
42
|
description: description,
|
|
40
43
|
enum: enum,
|
|
41
|
-
|
|
44
|
+
format: format,
|
|
45
|
+
item_type: item_type,
|
|
46
|
+
dimensions: dimensions,
|
|
42
47
|
nested_params: nested,
|
|
48
|
+
variants: variants,
|
|
43
49
|
)
|
|
44
50
|
end
|
|
45
51
|
|
|
46
52
|
#--
|
|
47
|
-
#: (Symbol,
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
#: (Symbol, singleton(Riffer::Params::AnyOf), ?description: String?, ?default: untyped) { (Riffer::Params::AnyOf) [self: Riffer::Params::AnyOf] -> void } -> void
|
|
54
|
+
#: (Symbol, Module, ?description: String?, ?enum: Array[untyped]?, ?format: String?, ?default: untyped, ?of: (Module | Array[untyped])?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> void
|
|
55
|
+
def optional(name, type, description: nil, enum: nil, format: nil, default: nil, of: nil, &)
|
|
56
|
+
item_type, dimensions = unwrap_of(of)
|
|
57
|
+
nested = build_nested(type, item_type, enum, &) unless type == Riffer::Params::AnyOf
|
|
58
|
+
variants = build_variants(of, enum, &) if type == Riffer::Params::AnyOf
|
|
50
59
|
@parameters << Riffer::Params::Param.new(
|
|
51
60
|
name: name,
|
|
52
61
|
type: type,
|
|
53
62
|
required: false,
|
|
54
63
|
description: description,
|
|
55
64
|
enum: enum,
|
|
65
|
+
format: format,
|
|
56
66
|
default: default,
|
|
57
|
-
item_type:
|
|
67
|
+
item_type: item_type,
|
|
68
|
+
dimensions: dimensions,
|
|
58
69
|
nested_params: nested,
|
|
70
|
+
variants: variants,
|
|
59
71
|
)
|
|
60
72
|
end
|
|
61
73
|
|
|
@@ -83,11 +95,17 @@ class Riffer::Params
|
|
|
83
95
|
next
|
|
84
96
|
end
|
|
85
97
|
|
|
86
|
-
|
|
98
|
+
# A typed array's enum and format constrain its items, which validate_typed_array checks.
|
|
99
|
+
if param.enum && !param.item_type && !param.enum.include?(value)
|
|
87
100
|
errors << "#{param.name} must be one of: #{param.enum.join(', ')}"
|
|
88
101
|
next
|
|
89
102
|
end
|
|
90
103
|
|
|
104
|
+
if param.format && !param.item_type && !Riffer::Params::Format.valid?(param.format, value)
|
|
105
|
+
errors << "#{param.name} must be a valid #{param.format}"
|
|
106
|
+
next
|
|
107
|
+
end
|
|
108
|
+
|
|
91
109
|
value = validate_nested(param, value, errors)
|
|
92
110
|
|
|
93
111
|
validated[param.name] = coerce_value(param.type, value)
|
|
@@ -131,9 +149,20 @@ class Riffer::Params
|
|
|
131
149
|
end
|
|
132
150
|
|
|
133
151
|
#--
|
|
134
|
-
#: (Module
|
|
135
|
-
def
|
|
152
|
+
#: ((Module | Array[untyped])?, ?Integer) -> [Module?, Integer]
|
|
153
|
+
def unwrap_of(of, dimensions = 1)
|
|
154
|
+
return [of, dimensions] unless of.is_a?(Array)
|
|
155
|
+
raise Riffer::ArgumentError, "of: must nest exactly one type, got #{of.inspect}" unless of.size == 1 && of.first
|
|
156
|
+
|
|
157
|
+
unwrap_of(of.first, dimensions + 1)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
#--
|
|
161
|
+
#: (Module, Module?, Array[untyped]?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> Riffer::Params?
|
|
162
|
+
def build_nested(type, of, enum, &block)
|
|
136
163
|
raise Riffer::ArgumentError, "cannot use both of: and a block" if of && block
|
|
164
|
+
raise Riffer::ArgumentError, "enum: cannot be used with Hash" if enum && type == Hash
|
|
165
|
+
raise Riffer::ArgumentError, "enum: with Array requires of:" if enum && type == Array && !of
|
|
137
166
|
|
|
138
167
|
if of
|
|
139
168
|
raise Riffer::ArgumentError, "of: can only be used with Array type, got #{type}" unless type == Array
|
|
@@ -155,10 +184,26 @@ class Riffer::Params
|
|
|
155
184
|
nested
|
|
156
185
|
end
|
|
157
186
|
|
|
187
|
+
#--
|
|
188
|
+
#: ((Module | Array[untyped])?, Array[untyped]?) ?{ (Riffer::Params::AnyOf) [self: Riffer::Params::AnyOf] -> void } -> Array[Riffer::Params]
|
|
189
|
+
def build_variants(of, enum, &block)
|
|
190
|
+
raise Riffer::ArgumentError, "of: cannot be used with AnyOf" if of
|
|
191
|
+
raise Riffer::ArgumentError, "enum: cannot be used with AnyOf" if enum
|
|
192
|
+
raise Riffer::ArgumentError, "AnyOf requires a block defining its variants" unless block
|
|
193
|
+
|
|
194
|
+
any_of = Riffer::Params::AnyOf.new
|
|
195
|
+
any_of.instance_eval(&block)
|
|
196
|
+
raise Riffer::ArgumentError, "AnyOf requires at least two variants" if any_of.variants.size < 2
|
|
197
|
+
|
|
198
|
+
any_of.variants
|
|
199
|
+
end
|
|
200
|
+
|
|
158
201
|
#--
|
|
159
202
|
#: (Riffer::Params::Param, untyped, Array[String]) -> untyped
|
|
160
203
|
def validate_nested(param, value, errors)
|
|
161
|
-
if param.type ==
|
|
204
|
+
if param.type == Riffer::Params::AnyOf
|
|
205
|
+
validate_any_of(param, value, errors)
|
|
206
|
+
elsif param.type == Hash && param.nested_params
|
|
162
207
|
validate_nested_hash(param, value, errors)
|
|
163
208
|
elsif param.type == Array && param.nested_params
|
|
164
209
|
validate_nested_array_of_objects(param, value, errors)
|
|
@@ -183,6 +228,49 @@ class Riffer::Params
|
|
|
183
228
|
value
|
|
184
229
|
end
|
|
185
230
|
|
|
231
|
+
#--
|
|
232
|
+
#: (Riffer::Params::Param, Hash[Symbol, untyped], Array[String]) -> Hash[Symbol, untyped]
|
|
233
|
+
def validate_any_of(param, value, errors)
|
|
234
|
+
variants = param.variants
|
|
235
|
+
return value unless variants
|
|
236
|
+
|
|
237
|
+
variant_errors = [] #: Array[String]
|
|
238
|
+
variants.each.with_index(1) do |variant, number|
|
|
239
|
+
validated = variant.validate(value)
|
|
240
|
+
# Each variant's schema sets additionalProperties: false on every object
|
|
241
|
+
# it declares, so a key outside this variant rules it out.
|
|
242
|
+
unknown = undeclared_keys(value, validated)
|
|
243
|
+
return validated if unknown.empty?
|
|
244
|
+
|
|
245
|
+
variant_errors << "variant #{number} (#{unknown.map { |path| "#{path} is not allowed" }.join(', ')})"
|
|
246
|
+
rescue Riffer::ValidationError => e
|
|
247
|
+
# Enclosing params split errors on "; " to prefix their path, so one
|
|
248
|
+
# variant's errors must not contain it.
|
|
249
|
+
variant_errors << "variant #{number} (#{e.message.gsub('; ', ', ')})"
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
errors << "#{param.name} must match one of the variants: #{variant_errors.join(', ')}"
|
|
253
|
+
value
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Validation drops keys a Params doesn't declare, at any depth, so they are
|
|
257
|
+
# the keys in the input that are missing from the output.
|
|
258
|
+
#--
|
|
259
|
+
#: (untyped, untyped, ?String?) -> Array[String]
|
|
260
|
+
def undeclared_keys(value, validated, path = nil)
|
|
261
|
+
case value
|
|
262
|
+
when Hash
|
|
263
|
+
value.flat_map do |key, child|
|
|
264
|
+
key_path = [path, key].compact.join(".")
|
|
265
|
+
validated.key?(key) ? undeclared_keys(child, validated[key], key_path) : [key_path]
|
|
266
|
+
end
|
|
267
|
+
when Array
|
|
268
|
+
value.each_with_index.flat_map { |item, i| undeclared_keys(item, validated[i], "#{path}[#{i}]") }
|
|
269
|
+
else
|
|
270
|
+
[]
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
186
274
|
#--
|
|
187
275
|
#: (Riffer::Params::Param, Array[untyped], Array[String]) -> Array[untyped]
|
|
188
276
|
def validate_nested_array_of_objects(param, value, errors)
|
|
@@ -204,22 +292,33 @@ class Riffer::Params
|
|
|
204
292
|
end
|
|
205
293
|
|
|
206
294
|
#--
|
|
207
|
-
#: (Riffer::Params::Param, Array[untyped], Array[String]) -> Array[untyped]
|
|
208
|
-
def validate_typed_array(param, value, errors)
|
|
295
|
+
#: (Riffer::Params::Param, Array[untyped], Array[String], ?String, ?Integer) -> Array[untyped]
|
|
296
|
+
def validate_typed_array(param, value, errors, path = param.name.to_s, dimensions = param.dimensions)
|
|
209
297
|
item_type = param.item_type
|
|
210
298
|
return value unless item_type
|
|
211
299
|
|
|
212
|
-
type_name = Riffer::Params::Param::TYPE_MAPPINGS[item_type]
|
|
213
|
-
valid_item = if [Riffer::Params::Boolean, TrueClass, FalseClass].include?(item_type)
|
|
214
|
-
->(item) { [true, false].include?(item) }
|
|
215
|
-
elsif item_type == Float
|
|
216
|
-
->(item) { item.is_a?(Numeric) }
|
|
217
|
-
else
|
|
218
|
-
->(item) { item.is_a?(item_type) }
|
|
219
|
-
end
|
|
220
300
|
value.map.with_index do |item, i|
|
|
221
|
-
|
|
222
|
-
|
|
301
|
+
item_path = "#{path}[#{i}]"
|
|
302
|
+
|
|
303
|
+
if dimensions > 1
|
|
304
|
+
next validate_typed_array(param, item, errors, item_path, dimensions - 1) if item.is_a?(Array)
|
|
305
|
+
|
|
306
|
+
errors << "#{item_path} must be an array"
|
|
307
|
+
next item
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
unless valid_item?(item_type, item)
|
|
311
|
+
errors << "#{item_path} must be a #{Riffer::Params::Param::TYPE_MAPPINGS[item_type]}"
|
|
312
|
+
next item
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
if param.enum && !param.enum.include?(item)
|
|
316
|
+
errors << "#{item_path} must be one of: #{param.enum.join(', ')}"
|
|
317
|
+
next item
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
if param.format && !Riffer::Params::Format.valid?(param.format, item)
|
|
321
|
+
errors << "#{item_path} must be a valid #{param.format}"
|
|
223
322
|
next item
|
|
224
323
|
end
|
|
225
324
|
|
|
@@ -227,6 +326,18 @@ class Riffer::Params
|
|
|
227
326
|
end
|
|
228
327
|
end
|
|
229
328
|
|
|
329
|
+
#--
|
|
330
|
+
#: (Module, untyped) -> bool
|
|
331
|
+
def valid_item?(type, item)
|
|
332
|
+
if [Riffer::Params::Boolean, TrueClass, FalseClass].include?(type)
|
|
333
|
+
[true, false].include?(item)
|
|
334
|
+
elsif type == Float
|
|
335
|
+
item.is_a?(Numeric)
|
|
336
|
+
else
|
|
337
|
+
item.is_a?(type)
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
230
341
|
#--
|
|
231
342
|
#: (Module, untyped) -> untyped
|
|
232
343
|
def coerce_value(type, value)
|
|
@@ -38,7 +38,7 @@ class Riffer::Providers::Base
|
|
|
38
38
|
messages = normalize_messages(prompt: prompt, system: system, messages: messages, files: files)
|
|
39
39
|
validate_normalized_messages!(messages)
|
|
40
40
|
Riffer::Files::Resolver.new(provider: self).resolve!(messages)
|
|
41
|
-
messages = merge_consecutive_messages(
|
|
41
|
+
messages = with_structured_output_prompt(merge_consecutive_messages(messages), options[:structured_output])
|
|
42
42
|
options = apply_reasoning(model, options)
|
|
43
43
|
params = build_request_params(messages, model, request_options(options))
|
|
44
44
|
|
|
@@ -77,7 +77,7 @@ class Riffer::Providers::Base
|
|
|
77
77
|
messages = normalize_messages(prompt: prompt, system: system, messages: messages, files: files)
|
|
78
78
|
validate_normalized_messages!(messages)
|
|
79
79
|
Riffer::Files::Resolver.new(provider: self).resolve!(messages)
|
|
80
|
-
messages = merge_consecutive_messages(
|
|
80
|
+
messages = with_structured_output_prompt(merge_consecutive_messages(messages), options[:structured_output])
|
|
81
81
|
options = apply_reasoning(model, options)
|
|
82
82
|
params = build_request_params(messages, model, request_options(options))
|
|
83
83
|
|
|
@@ -427,13 +427,13 @@ class Riffer::Providers::Base
|
|
|
427
427
|
def with_structured_output_prompt(messages, structured_output)
|
|
428
428
|
return messages unless structured_output&.prompted?
|
|
429
429
|
|
|
430
|
-
#
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
430
|
+
# Models follow the schema more reliably at the end of each user turn than in the system prompt.
|
|
431
|
+
instructions = structured_output.prompt_instructions
|
|
432
|
+
messages.map do |message|
|
|
433
|
+
next message unless message.is_a?(Riffer::Messages::User)
|
|
434
|
+
|
|
435
|
+
Riffer::Messages::User.new("#{message.content}\n\n#{instructions}", id: message.id, files: message.files)
|
|
436
|
+
end
|
|
437
437
|
end
|
|
438
438
|
|
|
439
439
|
#--
|
|
@@ -340,6 +340,7 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
|
|
|
340
340
|
end
|
|
341
341
|
|
|
342
342
|
schema[:items] = strip_additional_properties(schema[:items]) if schema[:items].is_a?(Hash)
|
|
343
|
+
schema[:anyOf] = schema[:anyOf].map { |branch| strip_additional_properties(branch) } if schema[:anyOf]
|
|
343
344
|
|
|
344
345
|
schema
|
|
345
346
|
end
|
data/lib/riffer/version.rb
CHANGED
data/rbs_collection.lock.yaml
CHANGED
|
@@ -77,6 +77,10 @@ gems:
|
|
|
77
77
|
revision: 3913b7dffb65bdffdfe5cb9095108f932e85433b
|
|
78
78
|
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
79
79
|
repo_dir: gems
|
|
80
|
+
- name: date
|
|
81
|
+
version: '0'
|
|
82
|
+
source:
|
|
83
|
+
type: stdlib
|
|
80
84
|
- name: dbm
|
|
81
85
|
version: '0'
|
|
82
86
|
source:
|
|
@@ -125,6 +129,10 @@ gems:
|
|
|
125
129
|
version: '0'
|
|
126
130
|
source:
|
|
127
131
|
type: stdlib
|
|
132
|
+
- name: ipaddr
|
|
133
|
+
version: '0'
|
|
134
|
+
source:
|
|
135
|
+
type: stdlib
|
|
128
136
|
- name: json
|
|
129
137
|
version: '0'
|
|
130
138
|
source:
|
data/rbs_collection.yaml
CHANGED
|
@@ -25,4 +25,10 @@ class Riffer::Agent::StructuredOutput
|
|
|
25
25
|
# --
|
|
26
26
|
# : (String) -> Riffer::Agent::StructuredOutput::Result
|
|
27
27
|
def parse_and_validate: (String) -> Riffer::Agent::StructuredOutput::Result
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# --
|
|
32
|
+
# : (Hash[Symbol, untyped]) -> untyped
|
|
33
|
+
def placeholder: (Hash[Symbol, untyped]) -> untyped
|
|
28
34
|
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Generated from lib/riffer/params/any_of.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
class Riffer::Params::AnyOf
|
|
4
|
+
attr_reader variants: Array[Riffer::Params]
|
|
5
|
+
|
|
6
|
+
# --
|
|
7
|
+
# : () -> void
|
|
8
|
+
def initialize: () -> void
|
|
9
|
+
|
|
10
|
+
# --
|
|
11
|
+
# : () ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> void
|
|
12
|
+
def variant: () ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> void
|
|
13
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Generated from lib/riffer/params/format.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module Riffer::Params::Format
|
|
4
|
+
DATE: Regexp
|
|
5
|
+
|
|
6
|
+
DATE_TIME: Regexp
|
|
7
|
+
|
|
8
|
+
# The offset is optional: Anthropic's grammar emits times without one.
|
|
9
|
+
TIME: Regexp
|
|
10
|
+
|
|
11
|
+
DURATION: Regexp
|
|
12
|
+
|
|
13
|
+
UUID: Regexp
|
|
14
|
+
|
|
15
|
+
# The WHATWG HTML pattern: no single pattern matches RFC 5322 exactly, so
|
|
16
|
+
# this one only rules out values that are clearly not an address.
|
|
17
|
+
EMAIL: Regexp
|
|
18
|
+
|
|
19
|
+
HOSTNAME: Regexp
|
|
20
|
+
|
|
21
|
+
# The formats OpenAI, Anthropic and Bedrock all accept. OpenAI rejects uri.
|
|
22
|
+
CHECKS: Hash[String, ^(String) -> bool]
|
|
23
|
+
|
|
24
|
+
NAMES: Array[String]
|
|
25
|
+
|
|
26
|
+
# --
|
|
27
|
+
# : (String, String) -> bool
|
|
28
|
+
def valid?: (String, String) -> bool
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# --
|
|
33
|
+
# : (Regexp, String) -> bool
|
|
34
|
+
def calendar_date?: (Regexp, String) -> bool
|
|
35
|
+
|
|
36
|
+
# --
|
|
37
|
+
# : (String) { (IPAddr) -> bool } -> bool
|
|
38
|
+
def ip_address?: (String) { (IPAddr) -> bool } -> bool
|
|
39
|
+
end
|
|
@@ -17,27 +17,41 @@ class Riffer::Params::Param
|
|
|
17
17
|
|
|
18
18
|
attr_reader enum: Array[untyped]?
|
|
19
19
|
|
|
20
|
+
attr_reader format: String?
|
|
21
|
+
|
|
20
22
|
attr_reader default: untyped
|
|
21
23
|
|
|
22
24
|
attr_reader item_type: Module?
|
|
23
25
|
|
|
26
|
+
attr_reader dimensions: Integer
|
|
27
|
+
|
|
24
28
|
attr_reader nested_params: Riffer::Params?
|
|
25
29
|
|
|
30
|
+
attr_reader variants: Array[Riffer::Params]?
|
|
31
|
+
|
|
26
32
|
# --
|
|
27
33
|
# : (Symbol, Hash[Symbol, untyped], required: bool) -> Riffer::Params::Param
|
|
28
34
|
def self.from_json_schema: (Symbol, Hash[Symbol, untyped], required: bool) -> Riffer::Params::Param
|
|
29
35
|
|
|
30
36
|
# --
|
|
31
|
-
# : (
|
|
32
|
-
def self.
|
|
37
|
+
# : (Array[Hash[Symbol, untyped]]) -> Array[Riffer::Params]
|
|
38
|
+
def self.resolve_variants: (Array[Hash[Symbol, untyped]]) -> Array[Riffer::Params]
|
|
39
|
+
|
|
40
|
+
# --
|
|
41
|
+
# : (Module, Hash[Symbol, untyped]) -> Riffer::Params?
|
|
42
|
+
def self.resolve_nested_params: (Module, Hash[Symbol, untyped]) -> Riffer::Params?
|
|
43
|
+
|
|
44
|
+
# --
|
|
45
|
+
# : (Hash[Symbol, untyped]) -> [Module, Integer, Hash[Symbol, untyped]]?
|
|
46
|
+
def self.resolve_typed_items: (Hash[Symbol, untyped]) -> [ Module, Integer, Hash[Symbol, untyped] ]?
|
|
33
47
|
|
|
34
48
|
# --
|
|
35
49
|
# : (untyped) -> Module
|
|
36
50
|
def self.json_type_to_ruby: (untyped) -> Module
|
|
37
51
|
|
|
38
52
|
# --
|
|
39
|
-
# : (name: Symbol, type: Module, required: bool, ?description: String?, ?enum: Array[untyped]?, ?default: untyped, ?item_type: Module?, ?nested_params: Riffer::Params?) -> void
|
|
40
|
-
def initialize: (name: Symbol, type: Module, required: bool, ?description: String?, ?enum: Array[untyped]?, ?default: untyped, ?item_type: Module?, ?nested_params: Riffer::Params?) -> void
|
|
53
|
+
# : (name: Symbol, type: Module, required: bool, ?description: String?, ?enum: Array[untyped]?, ?format: String?, ?default: untyped, ?item_type: Module?, ?dimensions: Integer, ?nested_params: Riffer::Params?, ?variants: Array[Riffer::Params]?) -> void
|
|
54
|
+
def initialize: (name: Symbol, type: Module, required: bool, ?description: String?, ?enum: Array[untyped]?, ?format: String?, ?default: untyped, ?item_type: Module?, ?dimensions: Integer, ?nested_params: Riffer::Params?, ?variants: Array[Riffer::Params]?) -> void
|
|
41
55
|
|
|
42
56
|
# --
|
|
43
57
|
# : (untyped) -> bool
|
|
@@ -60,6 +74,10 @@ class Riffer::Params::Param
|
|
|
60
74
|
# : (Riffer::Params::Param) -> void
|
|
61
75
|
def initialize_copy: (Riffer::Params::Param) -> void
|
|
62
76
|
|
|
77
|
+
# --
|
|
78
|
+
# : () -> void
|
|
79
|
+
def validate_format!: () -> void
|
|
80
|
+
|
|
63
81
|
# --
|
|
64
82
|
# : () -> void
|
|
65
83
|
def validate_strict_shape!: () -> void
|
|
@@ -14,12 +14,16 @@ class Riffer::Params
|
|
|
14
14
|
def self.from_json_schema: (Hash[Symbol, untyped]) -> Riffer::Params
|
|
15
15
|
|
|
16
16
|
# --
|
|
17
|
-
# : (Symbol,
|
|
18
|
-
|
|
17
|
+
# : (Symbol, singleton(Riffer::Params::AnyOf), ?description: String?) { (Riffer::Params::AnyOf) [self: Riffer::Params::AnyOf] -> void } -> void
|
|
18
|
+
# : (Symbol, Module, ?description: String?, ?enum: Array[untyped]?, ?format: String?, ?of: (Module | Array[untyped])?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> void
|
|
19
|
+
def required: (Symbol, singleton(Riffer::Params::AnyOf), ?description: String?) { (Riffer::Params::AnyOf) [self: Riffer::Params::AnyOf] -> void } -> void
|
|
20
|
+
| (Symbol, Module, ?description: String?, ?enum: Array[untyped]?, ?format: String?, ?of: (Module | Array[untyped])?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> void
|
|
19
21
|
|
|
20
22
|
# --
|
|
21
|
-
# : (Symbol,
|
|
22
|
-
|
|
23
|
+
# : (Symbol, singleton(Riffer::Params::AnyOf), ?description: String?, ?default: untyped) { (Riffer::Params::AnyOf) [self: Riffer::Params::AnyOf] -> void } -> void
|
|
24
|
+
# : (Symbol, Module, ?description: String?, ?enum: Array[untyped]?, ?format: String?, ?default: untyped, ?of: (Module | Array[untyped])?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> void
|
|
25
|
+
def optional: (Symbol, singleton(Riffer::Params::AnyOf), ?description: String?, ?default: untyped) { (Riffer::Params::AnyOf) [self: Riffer::Params::AnyOf] -> void } -> void
|
|
26
|
+
| (Symbol, Module, ?description: String?, ?enum: Array[untyped]?, ?format: String?, ?default: untyped, ?of: (Module | Array[untyped])?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> void
|
|
23
27
|
|
|
24
28
|
# --
|
|
25
29
|
# : (Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
@@ -38,8 +42,16 @@ class Riffer::Params
|
|
|
38
42
|
def initialize_copy: (Riffer::Params) -> void
|
|
39
43
|
|
|
40
44
|
# --
|
|
41
|
-
# : (Module
|
|
42
|
-
def
|
|
45
|
+
# : ((Module | Array[untyped])?, ?Integer) -> [Module?, Integer]
|
|
46
|
+
def unwrap_of: ((Module | Array[untyped])?, ?Integer) -> [ Module?, Integer ]
|
|
47
|
+
|
|
48
|
+
# --
|
|
49
|
+
# : (Module, Module?, Array[untyped]?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> Riffer::Params?
|
|
50
|
+
def build_nested: (Module, Module?, Array[untyped]?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> Riffer::Params?
|
|
51
|
+
|
|
52
|
+
# --
|
|
53
|
+
# : ((Module | Array[untyped])?, Array[untyped]?) ?{ (Riffer::Params::AnyOf) [self: Riffer::Params::AnyOf] -> void } -> Array[Riffer::Params]
|
|
54
|
+
def build_variants: ((Module | Array[untyped])?, Array[untyped]?) ?{ (Riffer::Params::AnyOf) [self: Riffer::Params::AnyOf] -> void } -> Array[Riffer::Params]
|
|
43
55
|
|
|
44
56
|
# --
|
|
45
57
|
# : (Riffer::Params::Param, untyped, Array[String]) -> untyped
|
|
@@ -49,13 +61,27 @@ class Riffer::Params
|
|
|
49
61
|
# : (Riffer::Params::Param, Hash[Symbol, untyped], Array[String]) -> Hash[Symbol, untyped]
|
|
50
62
|
def validate_nested_hash: (Riffer::Params::Param, Hash[Symbol, untyped], Array[String]) -> Hash[Symbol, untyped]
|
|
51
63
|
|
|
64
|
+
# --
|
|
65
|
+
# : (Riffer::Params::Param, Hash[Symbol, untyped], Array[String]) -> Hash[Symbol, untyped]
|
|
66
|
+
def validate_any_of: (Riffer::Params::Param, Hash[Symbol, untyped], Array[String]) -> Hash[Symbol, untyped]
|
|
67
|
+
|
|
68
|
+
# Validation drops keys a Params doesn't declare, at any depth, so they are
|
|
69
|
+
# the keys in the input that are missing from the output.
|
|
70
|
+
# --
|
|
71
|
+
# : (untyped, untyped, ?String?) -> Array[String]
|
|
72
|
+
def undeclared_keys: (untyped, untyped, ?String?) -> Array[String]
|
|
73
|
+
|
|
52
74
|
# --
|
|
53
75
|
# : (Riffer::Params::Param, Array[untyped], Array[String]) -> Array[untyped]
|
|
54
76
|
def validate_nested_array_of_objects: (Riffer::Params::Param, Array[untyped], Array[String]) -> Array[untyped]
|
|
55
77
|
|
|
56
78
|
# --
|
|
57
|
-
# : (Riffer::Params::Param, Array[untyped], Array[String]) -> Array[untyped]
|
|
58
|
-
def validate_typed_array: (Riffer::Params::Param, Array[untyped], Array[String]) -> Array[untyped]
|
|
79
|
+
# : (Riffer::Params::Param, Array[untyped], Array[String], ?String, ?Integer) -> Array[untyped]
|
|
80
|
+
def validate_typed_array: (Riffer::Params::Param, Array[untyped], Array[String], ?String, ?Integer) -> Array[untyped]
|
|
81
|
+
|
|
82
|
+
# --
|
|
83
|
+
# : (Module, untyped) -> bool
|
|
84
|
+
def valid_item?: (Module, untyped) -> bool
|
|
59
85
|
|
|
60
86
|
# --
|
|
61
87
|
# : (Module, untyped) -> untyped
|
data/sig/manifest.yaml
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: riffer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.54.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jake Bottrall
|
|
@@ -177,7 +177,9 @@ files:
|
|
|
177
177
|
- lib/riffer/messages/user.rb
|
|
178
178
|
- lib/riffer/messages/user/file_part.rb
|
|
179
179
|
- lib/riffer/params.rb
|
|
180
|
+
- lib/riffer/params/any_of.rb
|
|
180
181
|
- lib/riffer/params/boolean.rb
|
|
182
|
+
- lib/riffer/params/format.rb
|
|
181
183
|
- lib/riffer/params/param.rb
|
|
182
184
|
- lib/riffer/providers.rb
|
|
183
185
|
- lib/riffer/providers/amazon_bedrock.rb
|
|
@@ -327,7 +329,9 @@ files:
|
|
|
327
329
|
- sig/generated/riffer/messages/user.rbs
|
|
328
330
|
- sig/generated/riffer/messages/user/file_part.rbs
|
|
329
331
|
- sig/generated/riffer/params.rbs
|
|
332
|
+
- sig/generated/riffer/params/any_of.rbs
|
|
330
333
|
- sig/generated/riffer/params/boolean.rbs
|
|
334
|
+
- sig/generated/riffer/params/format.rbs
|
|
331
335
|
- sig/generated/riffer/params/param.rbs
|
|
332
336
|
- sig/generated/riffer/providers.rbs
|
|
333
337
|
- sig/generated/riffer/providers/amazon_bedrock.rbs
|
|
@@ -407,6 +411,7 @@ files:
|
|
|
407
411
|
- sig/manual/riffer/mcp/authenticated_tool.rbs
|
|
408
412
|
- sig/manual/riffer/mcp/registry.rbs
|
|
409
413
|
- sig/manual/riffer/mcp/tool_factory.rbs
|
|
414
|
+
- sig/manual/riffer/params/format.rbs
|
|
410
415
|
- sig/manual/riffer/providers.rbs
|
|
411
416
|
- sig/manual/riffer/providers/repository.rbs
|
|
412
417
|
- sig/manual/riffer/testing.rbs
|