Skip to content

Commit 5cd68b9

Browse files
authored
tsparser: additional api schema validation (#2149)
- Validate that request and response schema is an interface - Validate that Query parameters are of supported types - Validate that max one http status is defined per response type (moved from describe_resp)
1 parent 8540756 commit 5cd68b9

File tree

4 files changed

+107
-20
lines changed

4 files changed

+107
-20
lines changed

parser/encoding/rpc.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ func DescribeRPC(appMetaData *meta.Data, rpc *meta.RPC, options *Options) (*RPCE
295295
// Work out the response encoding
296296
encoding.ResponseEncoding, err = DescribeResponse(appMetaData, rpc.ResponseSchema, options)
297297
if err != nil {
298-
return nil, errors.Wrap(err, "request encoding")
298+
return nil, errors.Wrap(err, "response encoding")
299299
}
300300

301301
if encoding.RequestEncoding != nil {

tsparser/src/app/mod.rs

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::collections::{HashMap, HashSet};
22

3+
use itertools::Itertools;
34
use matchit::InsertError;
45
use swc_common::errors::HANDLER;
56
use swc_common::Span;
@@ -8,10 +9,14 @@ use crate::encore::parser::meta::v1;
89
use crate::legacymeta::compute_meta;
910
use crate::parser::parser::{ParseContext, ParseResult};
1011
use crate::parser::resources::apis::api::{Endpoint, Method, Methods};
12+
use crate::parser::resources::apis::encoding::{Param, ParamData};
1113
use crate::parser::resources::Resource;
1214
use crate::parser::respath::Path;
1315
use crate::parser::types::visitor::VisitWith;
14-
use crate::parser::types::{validation, visitor, ObjectId, ResolveState, Type, Validated};
16+
use crate::parser::types::{
17+
validation, visitor, Basic, Custom, Interface, ObjectId, ResolveState, Type, Validated,
18+
WireLocation, WireSpec,
19+
};
1520
use crate::parser::Range;
1621
use crate::span_err::ErrReporter;
1722
use litparser::Sp;
@@ -125,14 +130,99 @@ impl AppValidator<'_> {
125130
}
126131

127132
fn validate_endpoint(&self, ep: &Endpoint) {
133+
if let Some(req_enc) = &ep.encoding.handshake {
134+
self.validate_req_params(&req_enc.params);
135+
}
136+
for req_enc in &ep.encoding.req {
137+
self.validate_req_params(&req_enc.params);
138+
}
139+
self.validate_resp_params(&ep.encoding.resp.params);
140+
if let Some(schema) = &ep.encoding.raw_handshake_schema {
141+
self.validate_schema_type(schema);
142+
self.validate_validations(schema);
143+
}
128144
if let Some(schema) = &ep.encoding.raw_req_schema {
145+
self.validate_schema_type(schema);
129146
self.validate_validations(schema);
130147
}
131148
if let Some(schema) = &ep.encoding.raw_resp_schema {
149+
self.validate_schema_type(schema);
132150
self.validate_validations(schema);
133151
}
134152
}
135153

154+
fn validate_req_params(&self, params: &Vec<Param>) {
155+
for param in params {
156+
if let ParamData::Query { .. } = param.loc {
157+
fn is_valid_query_type(state: &ResolveState, typ: &Type) -> bool {
158+
match resolve_to_concrete(state, typ) {
159+
Type::Basic(_) | Type::Literal(_) => true,
160+
Type::Enum(_) => true,
161+
Type::Array(ref t) => is_valid_query_type(state, &t.0),
162+
Type::Union(ref u) => u.types.iter().all(|t| is_valid_query_type(state, t)),
163+
Type::Custom(Custom::Decimal) => true,
164+
Type::Custom(Custom::WireSpec(WireSpec {
165+
location: WireLocation::Query,
166+
underlying: typ,
167+
..
168+
})) => is_valid_query_type(state, &typ),
169+
_ => false,
170+
}
171+
}
172+
173+
if !is_valid_query_type(self.pc.type_checker.state(), &param.typ) {
174+
HANDLER.with(|handler| {
175+
handler.span_err(param.range, "type not supported for query parameters")
176+
});
177+
}
178+
};
179+
}
180+
}
181+
182+
fn validate_resp_params(&self, params: &[Param]) {
183+
let http_status_params: Vec<_> = params
184+
.iter()
185+
.filter(|p| matches!(p.loc, ParamData::HTTPStatus))
186+
.sorted_by(|a, b| a.range.cmp(&b.range))
187+
.collect();
188+
189+
if http_status_params.len() > 1 {
190+
let first = http_status_params[0];
191+
HANDLER.with(|handler| {
192+
let mut err = handler.struct_span_err(
193+
first.range,
194+
"http status can only be defined once per response type",
195+
);
196+
197+
for param in &http_status_params[1..] {
198+
err.span_note(param.range, "also defined here");
199+
}
200+
201+
err.emit();
202+
});
203+
}
204+
}
205+
206+
fn validate_schema_type(&self, schema: &Sp<Type>) {
207+
let state = self.pc.type_checker.state();
208+
let concrete = resolve_to_concrete(state, schema.get());
209+
210+
let error_msg = match concrete {
211+
Type::Interface(Interface { index: Some(_), .. }) => {
212+
Some("type index is not supported in schema types")
213+
}
214+
Type::Interface(Interface { call: Some(_), .. }) => {
215+
Some("call signatures are not supported in schema types")
216+
}
217+
Type::Interface(_) | Type::Basic(Basic::Void) => None,
218+
_ => Some("request and response types must be interfaces or void"),
219+
};
220+
221+
if let Some(msg) = error_msg {
222+
HANDLER.with(|handler| handler.span_err(schema.span(), msg));
223+
}
224+
}
225+
136226
fn validate_validations(&self, schema: &Sp<Type>) {
137227
struct Visitor<'a> {
138228
state: &'a ResolveState,
@@ -204,3 +294,15 @@ impl AppValidator<'_> {
204294
}
205295
}
206296
}
297+
298+
fn resolve_to_concrete(state: &ResolveState, typ: &Type) -> Type {
299+
match typ {
300+
Type::Optional(opt) => resolve_to_concrete(state, &opt.0),
301+
Type::Validated(v) => resolve_to_concrete(state, &v.typ),
302+
Type::Named(named) => {
303+
let underlying = named.underlying(state);
304+
resolve_to_concrete(state, &underlying)
305+
}
306+
_ => typ.clone(),
307+
}
308+
}

tsparser/src/parser/resources/apis/encoding.rs

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -341,23 +341,6 @@ fn describe_resp(
341341
let fields =
342342
iface_fields(tc, resp_schema).map_err(|err| err.span.parse_err(err.error.to_string()))?;
343343

344-
// Validate that maximum one field has HttpStatus location
345-
let http_status_count = fields
346-
.values()
347-
.filter(|f| {
348-
matches!(
349-
f.custom.as_ref().map(|s| &s.location),
350-
Some(&WireLocation::HttpStatus)
351-
)
352-
})
353-
.count();
354-
355-
if http_status_count > 1 {
356-
return Err(resp_schema
357-
.span()
358-
.parse_err("only one field can be of type HttpStatus in a response"));
359-
}
360-
361344
let params = extract_loc_params(&fields, ParamLocation::Body)?;
362345

363346
let fields = if fields.is_empty() {

tsparser/tests/testdata/builtins.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
-- foo/foo.ts --
22
import { api } from "encore.dev/api";
33

4-
type Params = Record<string, string>;
4+
type Params = {
5+
field: Record<string, string>;
6+
};
57

68
export const ping = api<Params, void>({}, () => {});
79

0 commit comments

Comments
 (0)