xidl_parser/rest_hir/
semantics.rs1mod annotation_parse;
2mod annotations;
3mod cors;
4mod security;
5mod stream;
6
7#[cfg(test)]
8mod tests;
9
10use crate::hir;
11use jiff::{Timestamp, civil, tz::TimeZone};
12use serde::{Deserialize, Serialize};
13
14pub use self::annotations::{
15 annotation_name, annotation_params, effective_media_type, find_annotation, has_annotation,
16 has_optional_annotation, normalize_annotation_params,
17};
18pub use self::cors::{HttpCorsProfile, effective_cors};
19pub use self::security::{
20 HttpApiKeyLocation, HttpSecurityOrigin, HttpSecurityProfile, HttpSecurityRequirement,
21 effective_security, effective_security_with_origin,
22};
23pub use self::stream::{
24 HttpStreamCodec, HttpStreamConfig, HttpStreamKind, HttpStreamTargetSupport, http_stream_config,
25 validate_http_stream_method, validate_http_stream_target,
26};
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29pub struct DeprecatedInfo {
30 pub deprecated: bool,
31 pub since: Option<String>,
32 pub after: Option<String>,
33}
34
35pub fn deprecated_info(annotations: &[hir::Annotation]) -> Result<Option<DeprecatedInfo>, String> {
36 let annotation = annotations.iter().find(|annotation| {
37 annotation_name(annotation)
38 .map(|name| name.eq_ignore_ascii_case("deprecated"))
39 .unwrap_or(false)
40 });
41 let Some(annotation) = annotation else {
42 return Ok(None);
43 };
44 let mut since = None;
45 let mut after = None;
46 if let Some(params) = annotation_params(annotation) {
47 let params = normalize_annotation_params(params);
48 if let Some(value) = params.get("value") {
49 since = Some(normalize_deprecated_timestamp(value, false)?);
50 }
51 if let Some(value) = params.get("since") {
52 since = Some(normalize_deprecated_timestamp(value, false)?);
53 }
54 if let Some(value) = params.get("after") {
55 after = Some(normalize_deprecated_timestamp(value, true)?);
56 }
57 }
58 if let (Some(since), Some(after)) = (&since, &after) {
59 validate_deprecated_range(since, after)?;
60 }
61 Ok(Some(DeprecatedInfo {
62 deprecated: true,
63 since,
64 after,
65 }))
66}
67
68pub fn validate_http_annotations(
69 target: &str,
70 annotations: &[hir::Annotation],
71) -> Result<(), String> {
72 let _ = cors::collect_cors(annotations).map_err(|err| format!("{target}: {err}"))?;
73 let _ = deprecated_info(annotations).map_err(|err| format!("{target}: {err}"))?;
74 let _ = security::collect_security(annotations).map_err(|err| format!("{target}: {err}"))?;
75 validate_rest_media_types(target, annotations)?;
76 Ok(())
77}
78
79fn validate_rest_media_types(target: &str, annotations: &[hir::Annotation]) -> Result<(), String> {
80 for annotation in annotations {
81 let Some(name) = annotation_name(annotation) else {
82 continue;
83 };
84 let canonical = if annotations::media_type_annotation_aliases("Consumes")
85 .iter()
86 .any(|alias| name.eq_ignore_ascii_case(alias))
87 {
88 "Consumes"
89 } else if annotations::media_type_annotation_aliases("Produces")
90 .iter()
91 .any(|alias| name.eq_ignore_ascii_case(alias))
92 {
93 "Produces"
94 } else {
95 continue;
96 };
97 let Some(value) =
98 annotations::annotation_value(std::slice::from_ref(annotation), canonical)
99 else {
100 continue;
101 };
102 if is_supported_http_media_type(&value) {
103 continue;
104 }
105 return Err(format!(
106 "{target}: unsupported @{name}(\"{value}\") media type"
107 ));
108 }
109 Ok(())
110}
111
112fn is_supported_http_media_type(value: &str) -> bool {
113 value.eq_ignore_ascii_case("application/json")
114 || value.eq_ignore_ascii_case("application/x-www-form-urlencoded")
115 || value.eq_ignore_ascii_case("application/msgpack")
116 || value.eq_ignore_ascii_case("text/plain")
117}
118
119fn validate_deprecated_range(since: &str, after: &str) -> Result<(), String> {
120 let since_ts: Timestamp = since
121 .parse()
122 .map_err(|_| format!("invalid @deprecated(since) timestamp '{since}'"))?;
123 let after_ts: Timestamp = after
124 .parse()
125 .map_err(|_| format!("invalid @deprecated(after) timestamp '{after}'"))?;
126 if since_ts > after_ts {
127 return Err("@deprecated(since=..., after=...) requires since <= after".to_string());
128 }
129 Ok(())
130}
131
132fn normalize_deprecated_timestamp(value: &str, end_of_day: bool) -> Result<String, String> {
133 if let Ok(ts) = value.parse::<Timestamp>() {
134 return Ok(ts.to_zoned(TimeZone::UTC).timestamp().to_string());
135 }
136 let date: civil::Date = value
137 .parse()
138 .map_err(|_| format!("invalid @deprecated timestamp literal '{value}'"))?;
139 let dt = if end_of_day {
140 date.at(23, 59, 59, 0)
141 } else {
142 date.to_datetime(civil::Time::midnight())
143 };
144 let zoned = dt.to_zoned(TimeZone::UTC).map_err(|err| err.to_string())?;
145 Ok(zoned.timestamp().to_string())
146}