Fail closed on unsupported Rust schema constructs
This commit is contained in:
parent
d0b745950b
commit
a61ba4d365
6 changed files with 366 additions and 11 deletions
|
|
@ -1,11 +1,17 @@
|
||||||
import {
|
import {
|
||||||
|
createTypeSpecLibrary,
|
||||||
emitFile,
|
emitFile,
|
||||||
getDiscriminatedUnion,
|
getDiscriminatedUnion,
|
||||||
|
getMaxItems,
|
||||||
getMaxLength,
|
getMaxLength,
|
||||||
getMaxValue,
|
getMaxValue,
|
||||||
|
getMaxValueExclusive,
|
||||||
|
getMinItems,
|
||||||
getMinLength,
|
getMinLength,
|
||||||
getMinValue,
|
getMinValue,
|
||||||
|
getMinValueExclusive,
|
||||||
getPattern,
|
getPattern,
|
||||||
|
paramMessage,
|
||||||
resolvePath,
|
resolvePath,
|
||||||
walkPropertiesInherited,
|
walkPropertiesInherited,
|
||||||
} from '@typespec/compiler'
|
} from '@typespec/compiler'
|
||||||
|
|
@ -13,6 +19,18 @@ import {
|
||||||
export const namespace = 'Remora.Rust'
|
export const namespace = 'Remora.Rust'
|
||||||
|
|
||||||
const roots = ['JobSubmission', 'EventFrame', 'JobFailure']
|
const roots = ['JobSubmission', 'EventFrame', 'JobFailure']
|
||||||
|
const $lib = createTypeSpecLibrary({
|
||||||
|
name: '@remora/typespec-rust',
|
||||||
|
diagnostics: {
|
||||||
|
'unsupported-schema': {
|
||||||
|
severity: 'error',
|
||||||
|
messages: {
|
||||||
|
default: paramMessage`Rust schema emission does not support ${'detail'}.`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const { reportDiagnostic } = $lib
|
||||||
|
|
||||||
export async function $onEmit(context) {
|
export async function $onEmit(context) {
|
||||||
const remora = context.program.getGlobalNamespaceType().namespaces.get('RemoraAPI')
|
const remora = context.program.getGlobalNamespaceType().namespaces.get('RemoraAPI')
|
||||||
|
|
@ -28,6 +46,9 @@ export async function $onEmit(context) {
|
||||||
}
|
}
|
||||||
collect(type, reachable)
|
collect(type, reachable)
|
||||||
}
|
}
|
||||||
|
if (!assertSupported(context.program, reachable)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const lines = [supportTypes()]
|
const lines = [supportTypes()]
|
||||||
for (const scalar of remora.scalars.values()) {
|
for (const scalar of remora.scalars.values()) {
|
||||||
|
|
@ -80,6 +101,91 @@ function collect(type, reachable) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function assertSupported(program, reachable) {
|
||||||
|
let supported = true
|
||||||
|
for (const type of reachable) {
|
||||||
|
if (type.kind === 'Scalar') {
|
||||||
|
const pattern = getPattern(program, type)
|
||||||
|
const hasLengthBounds =
|
||||||
|
getMinLength(program, type) !== undefined ||
|
||||||
|
getMaxLength(program, type) !== undefined
|
||||||
|
if (pattern !== undefined && hasLengthBounds) {
|
||||||
|
reportUnsupported(program, type, `combined pattern and length bounds on ${type.name}`)
|
||||||
|
supported = false
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
getMinValueExclusive(program, type) !== undefined ||
|
||||||
|
getMaxValueExclusive(program, type) !== undefined
|
||||||
|
) {
|
||||||
|
reportUnsupported(program, type, `exclusive numeric bounds on ${type.name}`)
|
||||||
|
supported = false
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (type.kind !== 'Model') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (type.indexer && type.name !== 'Array' && type.name !== 'Record') {
|
||||||
|
reportUnsupported(program, type, `indexed model ${type.name}`)
|
||||||
|
supported = false
|
||||||
|
}
|
||||||
|
for (const property of walkPropertiesInherited(type)) {
|
||||||
|
const name = `${type.name}.${property.name}`
|
||||||
|
const valueType = nullableValueType(property.type) ?? property.type
|
||||||
|
if (
|
||||||
|
getMinValueExclusive(program, property) !== undefined ||
|
||||||
|
getMaxValueExclusive(program, property) !== undefined
|
||||||
|
) {
|
||||||
|
reportUnsupported(program, property, `exclusive numeric bounds on ${name}`)
|
||||||
|
supported = false
|
||||||
|
}
|
||||||
|
if (getPattern(program, property) !== undefined) {
|
||||||
|
reportUnsupported(program, property, `property pattern on ${name}`)
|
||||||
|
supported = false
|
||||||
|
}
|
||||||
|
const minLength = getMinLength(program, property)
|
||||||
|
const maxLength = getMaxLength(program, property)
|
||||||
|
if (
|
||||||
|
(minLength !== undefined || maxLength !== undefined) &&
|
||||||
|
!supportsLengthBounds(valueType)
|
||||||
|
) {
|
||||||
|
reportUnsupported(program, property, `string length bounds on ${name}`)
|
||||||
|
supported = false
|
||||||
|
}
|
||||||
|
const minValue = getMinValue(program, property)
|
||||||
|
const maxValue = getMaxValue(program, property)
|
||||||
|
if (
|
||||||
|
(minValue !== undefined || maxValue !== undefined) &&
|
||||||
|
!supportsNumericBounds(valueType)
|
||||||
|
) {
|
||||||
|
reportUnsupported(program, property, `numeric bounds on ${name}`)
|
||||||
|
supported = false
|
||||||
|
} else if ((minValue ?? 0) < 0) {
|
||||||
|
reportUnsupported(program, property, `signed numeric bounds on ${name}`)
|
||||||
|
supported = false
|
||||||
|
}
|
||||||
|
const minItems = getMinItems(program, property)
|
||||||
|
const maxItems = getMaxItems(program, property)
|
||||||
|
if (
|
||||||
|
(minItems !== undefined || maxItems !== undefined) &&
|
||||||
|
!isArray(valueType)
|
||||||
|
) {
|
||||||
|
reportUnsupported(program, property, `collection bounds on ${name}`)
|
||||||
|
supported = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return supported
|
||||||
|
}
|
||||||
|
|
||||||
|
function reportUnsupported(program, target, detail) {
|
||||||
|
reportDiagnostic(program, {
|
||||||
|
code: 'unsupported-schema',
|
||||||
|
target,
|
||||||
|
format: { detail },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function emitScalar(program, scalar) {
|
function emitScalar(program, scalar) {
|
||||||
if (scalar.name === 'OpaqueId') {
|
if (scalar.name === 'OpaqueId') {
|
||||||
return `
|
return `
|
||||||
|
|
@ -266,6 +372,14 @@ function contextlessReport(program, diagnostics) {
|
||||||
|
|
||||||
function propertyType(program, model, property) {
|
function propertyType(program, model, property) {
|
||||||
const type = property.type
|
const type = property.type
|
||||||
|
const nullable = nullableValueType(type)
|
||||||
|
if (nullable) {
|
||||||
|
return `Option<${constrainedPropertyType(program, model, property, nullable)}>`
|
||||||
|
}
|
||||||
|
return constrainedPropertyType(program, model, property, type)
|
||||||
|
}
|
||||||
|
|
||||||
|
function constrainedPropertyType(program, model, property, type) {
|
||||||
if (type.kind === 'Number') {
|
if (type.kind === 'Number') {
|
||||||
return `ConstU64<${type.valueAsString}>`
|
return `ConstU64<${type.valueAsString}>`
|
||||||
}
|
}
|
||||||
|
|
@ -279,29 +393,65 @@ function propertyType(program, model, property) {
|
||||||
return `${model.name}${pascal(property.name)}`
|
return `${model.name}${pascal(property.name)}`
|
||||||
}
|
}
|
||||||
if (type.kind === 'Scalar') {
|
if (type.kind === 'Scalar') {
|
||||||
if (type.name === 'url' || type.name === 'utcDateTime') {
|
if (type.name === 'utcDateTime') {
|
||||||
return rustType(type)
|
return rustType(type)
|
||||||
}
|
}
|
||||||
const minLength = getMinLength(program, property)
|
const minLength = getMinLength(program, property)
|
||||||
const maxLength = getMaxLength(program, property)
|
const maxLength = getMaxLength(program, property)
|
||||||
if (minLength !== undefined || maxLength !== undefined) {
|
if (minLength !== undefined || maxLength !== undefined) {
|
||||||
|
if (type.name === 'url') {
|
||||||
|
return `BoundedUrl<${minLength ?? 0}, ${maxLength ?? '18446744073709551615'}>`
|
||||||
|
}
|
||||||
return `BoundedString<${minLength ?? 0}, ${maxLength ?? '18446744073709551615'}>`
|
return `BoundedString<${minLength ?? 0}, ${maxLength ?? '18446744073709551615'}>`
|
||||||
}
|
}
|
||||||
const minValue = getMinValue(program, property)
|
const minValue = getMinValue(program, property)
|
||||||
const maxValue = getMaxValue(program, property)
|
const maxValue = getMaxValue(program, property)
|
||||||
if (minValue !== undefined || maxValue !== undefined) {
|
if (minValue !== undefined || maxValue !== undefined) {
|
||||||
if ((minValue ?? 0) < 0) {
|
|
||||||
throw new Error(`unsupported signed bound on ${model.name}.${property.name}`)
|
|
||||||
}
|
|
||||||
return `BoundedU64<${minValue ?? 0}, ${maxValue ?? Number.MAX_SAFE_INTEGER}>`
|
return `BoundedU64<${minValue ?? 0}, ${maxValue ?? Number.MAX_SAFE_INTEGER}>`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (isArray(type)) {
|
||||||
|
const minItems = getMinItems(program, property)
|
||||||
|
const maxItems = getMaxItems(program, property)
|
||||||
|
if (minItems !== undefined || maxItems !== undefined) {
|
||||||
|
return `BoundedVec<${rustType(type.indexer.value)}, ${minItems ?? 0}, ${maxItems ?? '18446744073709551615'}>`
|
||||||
|
}
|
||||||
|
}
|
||||||
return rustType(type)
|
return rustType(type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function nullableValueType(type) {
|
||||||
|
if (type.kind !== 'Union') {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const variants = [...type.variants.values()].map((variant) => variant.type)
|
||||||
|
const values = variants.filter(
|
||||||
|
(variant) => variant.kind !== 'Intrinsic' || variant.name !== 'null',
|
||||||
|
)
|
||||||
|
return values.length === 1 && variants.length === 2 ? values[0] : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function isArray(type) {
|
||||||
|
return type.kind === 'Model' && type.name === 'Array' && type.indexer
|
||||||
|
}
|
||||||
|
|
||||||
|
function supportsLengthBounds(type) {
|
||||||
|
return type.kind === 'Scalar' && type.name !== 'utcDateTime'
|
||||||
|
}
|
||||||
|
|
||||||
|
function supportsNumericBounds(type) {
|
||||||
|
if (type.kind !== 'Scalar') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return ['integer', 'int64', 'uint64'].includes(type.name) ||
|
||||||
|
inheritsFrom(type, 'integer') ||
|
||||||
|
inheritsFrom(type, 'int64') ||
|
||||||
|
inheritsFrom(type, 'uint64')
|
||||||
|
}
|
||||||
|
|
||||||
function rustType(type) {
|
function rustType(type) {
|
||||||
if (type.kind === 'Model') {
|
if (type.kind === 'Model') {
|
||||||
if (type.name === 'Array' && type.indexer) {
|
if (isArray(type)) {
|
||||||
return `Vec<${rustType(type.indexer.value)}>`
|
return `Vec<${rustType(type.indexer.value)}>`
|
||||||
}
|
}
|
||||||
if (type.name === 'Record' && type.indexer) {
|
if (type.name === 'Record' && type.indexer) {
|
||||||
|
|
@ -477,5 +627,48 @@ impl<'de, const MIN: usize, const MAX: usize> serde::Deserialize<'de>
|
||||||
Err(serde::de::Error::custom("string length outside bounds"))
|
Err(serde::de::Error::custom("string length outside bounds"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct BoundedUrl<const MIN: usize, const MAX: usize>(url::Url);
|
||||||
|
|
||||||
|
impl<'de, const MIN: usize, const MAX: usize> serde::Deserialize<'de>
|
||||||
|
for BoundedUrl<MIN, MAX>
|
||||||
|
{
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let value = <String as serde::Deserialize>::deserialize(deserializer)?;
|
||||||
|
if !(MIN..=MAX).contains(&value.chars().count()) {
|
||||||
|
return Err(serde::de::Error::custom("URL length outside bounds"));
|
||||||
|
}
|
||||||
|
url::Url::parse(&value)
|
||||||
|
.map(Self)
|
||||||
|
.map_err(serde::de::Error::custom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct BoundedVec<T, const MIN: usize, const MAX: usize>(Vec<T>);
|
||||||
|
|
||||||
|
impl<'de, T, const MIN: usize, const MAX: usize> serde::Deserialize<'de>
|
||||||
|
for BoundedVec<T, MIN, MAX>
|
||||||
|
where
|
||||||
|
T: serde::Deserialize<'de>,
|
||||||
|
{
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let value = <Vec<T> as serde::Deserialize>::deserialize(deserializer)?;
|
||||||
|
if (MIN..=MAX).contains(&value.len()) {
|
||||||
|
Ok(Self(value))
|
||||||
|
} else {
|
||||||
|
Err(serde::de::Error::custom("array length outside bounds"))
|
||||||
|
}
|
||||||
|
}
|
||||||
}`.trim()
|
}`.trim()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@
|
||||||
"default": "./index.js"
|
"default": "./index.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "node --test test.mjs"
|
||||||
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@typespec/compiler": "1.14.0"
|
"@typespec/compiler": "1.14.0"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
105
contracts/typespec-rust-emitter/test.mjs
Normal file
105
contracts/typespec-rust-emitter/test.mjs
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { spawnSync } from 'node:child_process'
|
||||||
|
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||||
|
import { tmpdir } from 'node:os'
|
||||||
|
import { dirname, resolve } from 'node:path'
|
||||||
|
import test from 'node:test'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
const root = dirname(fileURLToPath(import.meta.url))
|
||||||
|
const compiler = resolve(root, '../typespec/node_modules/.bin/tsp')
|
||||||
|
const emitter = resolve(root, 'index.js')
|
||||||
|
|
||||||
|
test('emits bounded arrays', () => {
|
||||||
|
const result = compile(`
|
||||||
|
namespace RemoraAPI;
|
||||||
|
model JobSubmission {
|
||||||
|
@maxItems(2) values: string[];
|
||||||
|
}
|
||||||
|
model EventFrame { kind: string; }
|
||||||
|
model JobFailure { message: string; }
|
||||||
|
`)
|
||||||
|
assert.equal(result.status, 0, result.stderr)
|
||||||
|
assert.match(
|
||||||
|
readFileSync(resolve(result.directory, 'remora_wire.rs'), 'utf8'),
|
||||||
|
/pub values: BoundedVec<String, 0, 2>/,
|
||||||
|
)
|
||||||
|
rmSync(result.directory, { recursive: true, force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('preserves URL and nullable numeric bounds', () => {
|
||||||
|
const result = compile(`
|
||||||
|
namespace RemoraAPI;
|
||||||
|
model JobSubmission {
|
||||||
|
@maxLength(20) url: url;
|
||||||
|
@minValue(0) count: integer | null;
|
||||||
|
}
|
||||||
|
model EventFrame { kind: string; }
|
||||||
|
model JobFailure { message: string; }
|
||||||
|
`)
|
||||||
|
assert.equal(result.status, 0, result.stderr)
|
||||||
|
const generated = readFileSync(resolve(result.directory, 'remora_wire.rs'), 'utf8')
|
||||||
|
assert.match(generated, /pub url: BoundedUrl<0, 20>/)
|
||||||
|
assert.match(generated, /pub count: Option<BoundedU64<0, 9007199254740991>>/)
|
||||||
|
rmSync(result.directory, { recursive: true, force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('rejects arbitrary indexed models', () => {
|
||||||
|
const result = compile(`
|
||||||
|
namespace RemoraAPI;
|
||||||
|
model UnsupportedMap is Record<string>;
|
||||||
|
model JobSubmission { values: UnsupportedMap; }
|
||||||
|
model EventFrame { kind: string; }
|
||||||
|
model JobFailure { message: string; }
|
||||||
|
`)
|
||||||
|
assert.notEqual(result.status, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('rejects unsupported property patterns', () => {
|
||||||
|
const result = compile(`
|
||||||
|
namespace RemoraAPI;
|
||||||
|
model JobSubmission {
|
||||||
|
@pattern("^https://") value: string;
|
||||||
|
}
|
||||||
|
model EventFrame { kind: string; }
|
||||||
|
model JobFailure { message: string; }
|
||||||
|
`)
|
||||||
|
assert.notEqual(result.status, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('rejects unsupported exclusive bounds', () => {
|
||||||
|
const result = compile(`
|
||||||
|
namespace RemoraAPI;
|
||||||
|
model JobSubmission {
|
||||||
|
@minValueExclusive(0) count: int64;
|
||||||
|
}
|
||||||
|
model EventFrame { kind: string; }
|
||||||
|
model JobFailure { message: string; }
|
||||||
|
`)
|
||||||
|
assert.notEqual(result.status, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
function compile(source) {
|
||||||
|
const directory = mkdtempSync(resolve(tmpdir(), 'remora-typespec-rust-'))
|
||||||
|
const main = resolve(directory, 'main.tsp')
|
||||||
|
writeFileSync(main, source)
|
||||||
|
const result = spawnSync(
|
||||||
|
compiler,
|
||||||
|
[
|
||||||
|
'compile',
|
||||||
|
main,
|
||||||
|
'--emit',
|
||||||
|
emitter,
|
||||||
|
'--output-dir',
|
||||||
|
directory,
|
||||||
|
'--pretty',
|
||||||
|
'false',
|
||||||
|
],
|
||||||
|
{ encoding: 'utf8' },
|
||||||
|
)
|
||||||
|
const stderr = `${result.stdout}${result.stderr}`
|
||||||
|
if (result.status !== 0) {
|
||||||
|
rmSync(directory, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
return { ...result, directory, stderr }
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"generate": "node generate.mjs",
|
"generate": "node generate.mjs",
|
||||||
"check": "node generate.mjs --check",
|
"check": "npm --prefix ../typespec-rust-emitter test && node generate.mjs --check",
|
||||||
"compile": "tsp compile ."
|
"compile": "tsp compile ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,46 @@ impl<'de, const MIN: usize, const MAX: usize> serde::Deserialize<'de> for Bounde
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct BoundedUrl<const MIN: usize, const MAX: usize>(url::Url);
|
||||||
|
|
||||||
|
impl<'de, const MIN: usize, const MAX: usize> serde::Deserialize<'de> for BoundedUrl<MIN, MAX> {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let value = <String as serde::Deserialize>::deserialize(deserializer)?;
|
||||||
|
if !(MIN..=MAX).contains(&value.chars().count()) {
|
||||||
|
return Err(serde::de::Error::custom("URL length outside bounds"));
|
||||||
|
}
|
||||||
|
url::Url::parse(&value)
|
||||||
|
.map(Self)
|
||||||
|
.map_err(serde::de::Error::custom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct BoundedVec<T, const MIN: usize, const MAX: usize>(Vec<T>);
|
||||||
|
|
||||||
|
impl<'de, T, const MIN: usize, const MAX: usize> serde::Deserialize<'de> for BoundedVec<T, MIN, MAX>
|
||||||
|
where
|
||||||
|
T: serde::Deserialize<'de>,
|
||||||
|
{
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let value = <Vec<T> as serde::Deserialize>::deserialize(deserializer)?;
|
||||||
|
if (MIN..=MAX).contains(&value.len()) {
|
||||||
|
Ok(Self(value))
|
||||||
|
} else {
|
||||||
|
Err(serde::de::Error::custom("array length outside bounds"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct OpaqueId(String);
|
pub struct OpaqueId(String);
|
||||||
|
|
@ -212,7 +252,7 @@ pub struct TargetOverride {
|
||||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct HttpSourceSubmission {
|
pub struct HttpSourceSubmission {
|
||||||
pub url: url::Url,
|
pub url: BoundedUrl<0, 8192>,
|
||||||
pub result_path: ResultPath,
|
pub result_path: ResultPath,
|
||||||
pub credential_id: Option<CredentialId>,
|
pub credential_id: Option<CredentialId>,
|
||||||
}
|
}
|
||||||
|
|
@ -231,8 +271,8 @@ pub struct ObjectStoreSourceSubmission {
|
||||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct HuggingFaceSelection {
|
pub struct HuggingFaceSelection {
|
||||||
pub paths: Option<Vec<ResultPath>>,
|
pub paths: Option<BoundedVec<ResultPath, 0, 10000>>,
|
||||||
pub prefixes: Option<Vec<ResultPath>>,
|
pub prefixes: Option<BoundedVec<ResultPath, 0, 10000>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
|
|
@ -324,9 +364,9 @@ pub struct EventProgress {
|
||||||
pub attempt_id: OpaqueId,
|
pub attempt_id: OpaqueId,
|
||||||
pub fence: BoundedU64<1, 9007199254740991>,
|
pub fence: BoundedU64<1, 9007199254740991>,
|
||||||
pub completed_bytes: BoundedU64<0, 9007199254740991>,
|
pub completed_bytes: BoundedU64<0, 9007199254740991>,
|
||||||
pub total_bytes: Option<Option<i64>>,
|
pub total_bytes: Option<Option<BoundedU64<0, 9007199254740991>>>,
|
||||||
pub completed_members: BoundedU64<0, 9007199254740991>,
|
pub completed_members: BoundedU64<0, 9007199254740991>,
|
||||||
pub total_members: Option<Option<i64>>,
|
pub total_members: Option<Option<BoundedU64<0, 9007199254740991>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,20 @@ fn generated_roots_reject_governance_regressions() {
|
||||||
"occurred_at": "2026-07-15T00:00:00Z",
|
"occurred_at": "2026-07-15T00:00:00Z",
|
||||||
"unexpected": true
|
"unexpected": true
|
||||||
}));
|
}));
|
||||||
|
let paths: Vec<Value> = (0..=10_000)
|
||||||
|
.map(|index| Value::String(format!("file-{index}")))
|
||||||
|
.collect();
|
||||||
|
assert_invalid::<JobSubmission>(json!({
|
||||||
|
"source": {
|
||||||
|
"kind": "hugging_face",
|
||||||
|
"repository_type": "model",
|
||||||
|
"repository_id": "owner/repository",
|
||||||
|
"revision": "main",
|
||||||
|
"selection": {
|
||||||
|
"paths": paths
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn assert_all<T: DeserializeOwned>(values: &Value) {
|
fn assert_all<T: DeserializeOwned>(values: &Value) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue