use serde_json::{Map, Value};
use url::Url;
use crate::KitError;
use crate::error::{JsonValueKind, NativeErrorDetails, ResponseDetails, ResponseViolation};
use crate::generation::{Artifact, FileReference};
use crate::transport::CheckedRequestSpec;
pub(crate) struct HostedJsonCursor<'a> {
context: String,
body: HostedJsonBody<'a>,
value: HostedJsonValue<'a>,
}
enum HostedJsonBody<'a> {
Owned(Box<NativeErrorDetails>),
Object(&'a Map<String, Value>),
}
enum HostedJsonValue<'a> {
Value(&'a Value),
Object(&'a Map<String, Value>),
}
impl<'a> HostedJsonCursor<'a> {
pub(crate) fn new(
context: impl Into<String>,
body: NativeErrorDetails,
value: &'a Value,
) -> Self {
Self {
context: context.into(),
body: HostedJsonBody::Owned(Box::new(body)),
value: HostedJsonValue::Value(value),
}
}
pub(crate) fn for_object(
context: impl Into<String>,
body: NativeErrorDetails,
object: &'a Map<String, Value>,
) -> Self {
Self {
context: context.into(),
body: HostedJsonBody::Owned(Box::new(body)),
value: HostedJsonValue::Object(object),
}
}
pub(crate) fn for_borrowed_object(
context: impl Into<String>,
object: &'a Map<String, Value>,
) -> Self {
Self {
context: context.into(),
body: HostedJsonBody::Object(object),
value: HostedJsonValue::Object(object),
}
}
pub(crate) fn object(&self) -> Result<&'a Map<String, Value>, KitError> {
match self.value {
HostedJsonValue::Object(object) => Ok(object),
HostedJsonValue::Value(value) => value.as_object().ok_or_else(|| {
self.invalid(
ResponseViolation::InvalidField {
field: "hosted_output",
},
"must be an object",
)
.with_response_details(ResponseDetails::FieldType {
expected: JsonValueKind::Object,
actual: JsonValueKind::of(Some(value)),
})
}),
}
}
pub(crate) fn optional_array_field(
&self,
field: &'static str,
) -> Result<Option<&'a [Value]>, KitError> {
self.object()?
.get(field)
.map(|value| {
value.as_array().map(Vec::as_slice).ok_or_else(|| {
self.invalid(
ResponseViolation::InvalidField { field },
format!(".{field} must be an array"),
)
.with_response_details(ResponseDetails::FieldType {
expected: JsonValueKind::Array,
actual: JsonValueKind::of(Some(value)),
})
})
})
.transpose()
}
pub(crate) fn required_str_field(&self, field: &'static str) -> Result<&'a str, KitError> {
match self.object()?.get(field) {
Some(Value::String(value)) => Ok(value),
Some(value) => Err(self
.invalid(
ResponseViolation::InvalidField { field },
format!("{field} must be a string"),
)
.with_response_details(ResponseDetails::FieldType {
expected: JsonValueKind::String,
actual: JsonValueKind::of(Some(value)),
})),
None => Err(self
.invalid(
ResponseViolation::MissingField { field },
format!("did not include {field}"),
)
.with_response_details(ResponseDetails::FieldType {
expected: JsonValueKind::String,
actual: JsonValueKind::Absent,
})),
}
}
pub(crate) fn optional_str_field(
&self,
field: &'static str,
) -> Result<Option<String>, KitError> {
match self.object()?.get(field) {
Some(Value::String(value)) => Ok(Some(value.clone())),
Some(Value::Null) | None => Ok(None),
Some(value) => Err(self
.invalid(
ResponseViolation::InvalidField { field },
format!(".{field} must be a string"),
)
.with_response_details(ResponseDetails::FieldType {
expected: JsonValueKind::String,
actual: JsonValueKind::of(Some(value)),
})),
}
}
pub(crate) fn required_string_field(&self, field: &'static str) -> Result<String, KitError> {
self.required_str_field(field).map(ToOwned::to_owned)
}
pub(crate) fn optional_number_field(
&self,
field: &'static str,
) -> Result<Option<f64>, KitError> {
match self.object()?.get(field) {
Some(Value::Number(value)) => value
.as_f64()
.filter(|value| value.is_finite())
.ok_or_else(|| {
self.invalid(
ResponseViolation::InvalidField { field },
format!(".{field} must be a finite number"),
)
})
.map(Some),
Some(Value::Null) | None => Ok(None),
Some(value) => Err(self
.invalid(
ResponseViolation::InvalidField { field },
format!(".{field} must be a number"),
)
.with_response_details(ResponseDetails::FieldType {
expected: JsonValueKind::Number,
actual: JsonValueKind::of(Some(value)),
})),
}
}
pub(crate) fn optional_i64_field(&self, field: &'static str) -> Result<Option<i64>, KitError> {
match self.object()?.get(field) {
Some(Value::Number(value)) => value
.as_i64()
.ok_or_else(|| {
self.invalid(
ResponseViolation::InvalidField { field },
format!(".{field} must be an integer"),
)
})
.map(Some),
Some(Value::Null) | None => Ok(None),
Some(value) => Err(self
.invalid(
ResponseViolation::InvalidField { field },
format!(".{field} must be an integer"),
)
.with_response_details(ResponseDetails::FieldType {
expected: JsonValueKind::Number,
actual: JsonValueKind::of(Some(value)),
})),
}
}
pub(crate) fn string_array_field(&self, field: &'static str) -> Result<Vec<String>, KitError> {
let Some(values) = self.optional_array_field(field)? else {
return Ok(Vec::new());
};
values
.iter()
.enumerate()
.map(|(index, value)| {
value.as_str().map(ToOwned::to_owned).ok_or_else(|| {
self.invalid(
ResponseViolation::InvalidField { field },
format!(".{field}[{index}] must be a string"),
)
.with_response_details(ResponseDetails::FieldType {
expected: JsonValueKind::String,
actual: JsonValueKind::of(Some(value)),
})
})
})
.collect()
}
pub(crate) fn objects_or_single(&self) -> Result<Vec<&'a Map<String, Value>>, KitError> {
match self.value {
HostedJsonValue::Object(object) | HostedJsonValue::Value(Value::Object(object)) => {
Ok(vec![object])
}
HostedJsonValue::Value(Value::Array(items)) => items
.iter()
.enumerate()
.map(|(index, item)| {
item.as_object().ok_or_else(|| {
self.invalid(
ResponseViolation::InvalidField {
field: "hosted_output",
},
format!("[{index}] must be an object"),
)
.with_response_details(
ResponseDetails::FieldType {
expected: JsonValueKind::Object,
actual: JsonValueKind::of(Some(item)),
},
)
})
})
.collect(),
_ => Err(self.invalid(
ResponseViolation::InvalidField {
field: "hosted_output",
},
"must be an object or array",
)),
}
}
pub(crate) fn invalid(
&self,
reason: ResponseViolation,
message: impl Into<String>,
) -> KitError {
let message = message.into();
let separator = if message.starts_with(['[', '.']) {
""
} else {
" "
};
KitError::invalid_response(reason)
.with_message(format!("{}{separator}{message}", self.context))
.with_diagnostic(self.body.render())
}
}
impl HostedJsonBody<'_> {
fn render(&self) -> Option<NativeErrorDetails> {
match self {
Self::Owned(body) => Some((**body).clone()),
Self::Object(object) => {
CheckedRequestSpec::diagnostic_json(&Value::Object((*object).clone()))
}
}
}
}
pub(crate) fn hosted_url_artifact(
url: &str,
body: NativeErrorDetails,
) -> Result<Artifact, KitError> {
let reference = FileReference::Url {
url: Url::parse(url).map_err(|err| {
KitError::invalid_response(ResponseViolation::InvalidField {
field: "artifact.url",
})
.with_message(format!("hosted tool artifact URL was invalid: {err}"))
.with_diagnostic(Some(body.clone()))
})?,
media_type: None,
};
reference.validate().map_err(|error| {
KitError::invalid_response(ResponseViolation::InvalidField {
field: "artifact.url",
})
.with_message("hosted tool artifact URL was not a reusable HTTP(S) reference".to_string())
.with_diagnostic(Some(body.clone()))
.with_supplementary(error)
})?;
Ok(Artifact::File {
reference,
file_name: None,
description: None,
})
}