Skip to main content

xidl_parser/rest_hir/
semantics.rs

1mod 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}