use std::fmt;
use starlark::environment::GlobalsBuilder;
use starlark::eval::Evaluator;
use starlark::starlark_module;
use starlark::values::Value;
use starlark::values::none::{NoneOr, NoneType};
use crate::identity;
use crate::paths::ProjectPath;
mod host_types {
#![allow(unsafe_code)]
#![allow(clippy::elidable_lifetime_names)]
use std::cell::RefCell;
use allocative::Allocative;
use starlark::any::ProvidesStaticType;
use starlark::starlark_simple_value;
use starlark::values::{NoSerialize, StarlarkValue, starlark_value};
use super::SchemaRegistration;
#[derive(Debug, Clone, PartialEq, Eq, ProvidesStaticType, NoSerialize, Allocative)]
pub struct Finding {
pub is_error: bool,
pub message: String,
pub resource: Option<String>,
pub line: Option<u32>,
pub rule: Option<String>,
}
starlark_simple_value!(Finding);
#[starlark_value(type = "bearout.finding")]
impl<'v> StarlarkValue<'v> for Finding {}
#[derive(Debug, Clone, PartialEq, Eq, ProvidesStaticType, NoSerialize, Allocative)]
pub struct Output {
pub template: String,
pub path: String,
pub context: String,
}
starlark_simple_value!(Output);
#[starlark_value(type = "bearout.output")]
impl<'v> StarlarkValue<'v> for Output {}
#[derive(Debug, Default, ProvidesStaticType)]
pub struct Registry {
pub schemas: RefCell<Vec<SchemaRegistration>>,
pub checks: RefCell<Vec<(String, String)>>,
pub generators: RefCell<Vec<(String, String)>>,
counter: RefCell<u32>,
}
impl Registry {
pub(super) fn next_slot(&self, prefix: &str) -> String {
let mut counter = self.counter.borrow_mut();
*counter += 1;
format!("__bearout_{prefix}_{}", *counter)
}
}
}
pub use host_types::{Finding, Output, Registry};
impl fmt::Display for Finding {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let kind = if self.is_error { "error" } else { "warning" };
write!(f, "{kind}({:?})", self.message)
}
}
impl fmt::Display for Output {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "output({:?}, {:?})", self.template, self.path)
}
}
#[derive(Debug, Clone)]
pub struct SchemaRegistration {
pub id: String,
pub shape: Option<String>,
pub validate: Option<String>,
}
fn fail(message: String) -> starlark::Error {
starlark::Error::new_other(anyhow::anyhow!(message))
}
fn finding(
is_error: bool,
message: &str,
resource: NoneOr<&str>,
line: NoneOr<i32>,
code: NoneOr<&str>,
) -> starlark::Result<Finding> {
if message.trim().is_empty() {
return Err(fail("finding message must not be empty".to_owned()));
}
let resource = match resource {
NoneOr::None => None,
NoneOr::Other(id) => {
identity::check_id(id).map_err(|error| fail(format!("finding resource: {error}")))?;
Some(id.to_owned())
}
};
let line = match line {
NoneOr::None => None,
NoneOr::Other(line) => Some(
u32::try_from(line)
.ok()
.filter(|line| *line > 0)
.ok_or_else(|| {
fail(format!(
"finding line must be a positive integer, found {line}"
))
})?,
),
};
let rule = match code {
NoneOr::None => None,
NoneOr::Other(code) => {
identity::check_kind(code).map_err(|error| fail(format!("finding code: {error}")))?;
Some(code.to_owned())
}
};
Ok(Finding {
is_error,
message: message.to_owned(),
resource,
line,
rule,
})
}
#[starlark_module]
pub fn library(builder: &mut GlobalsBuilder) {
fn error(
#[starlark(require = pos)] message: &str,
#[starlark(require = named, default = NoneOr::None)] resource: NoneOr<&str>,
#[starlark(require = named, default = NoneOr::None)] line: NoneOr<i32>,
#[starlark(require = named, default = NoneOr::None)] code: NoneOr<&str>,
) -> starlark::Result<Finding> {
finding(true, message, resource, line, code)
}
fn warning(
#[starlark(require = pos)] message: &str,
#[starlark(require = named, default = NoneOr::None)] resource: NoneOr<&str>,
#[starlark(require = named, default = NoneOr::None)] line: NoneOr<i32>,
#[starlark(require = named, default = NoneOr::None)] code: NoneOr<&str>,
) -> starlark::Result<Finding> {
finding(false, message, resource, line, code)
}
fn output<'v>(
#[starlark(require = pos)] template: &str,
#[starlark(require = pos)] path: &str,
#[starlark(require = named, default = NoneOr::None)] context: NoneOr<Value<'v>>,
) -> starlark::Result<Output> {
let template = ProjectPath::parse(template)
.map_err(|error| fail(format!("output template: {error}")))?;
if template.as_str().is_empty() {
return Err(fail("output template must not be empty".to_owned()));
}
let path =
ProjectPath::parse(path).map_err(|error| fail(format!("output path: {error}")))?;
if path.as_str().is_empty() {
return Err(fail("output path must not be empty".to_owned()));
}
let context = match context {
NoneOr::None => serde_json::Value::Object(serde_json::Map::new()),
NoneOr::Other(value) => {
let json = value.to_json_value().map_err(|error| {
fail(format!("output context must be JSON-compatible: {error}"))
})?;
if !json.is_object() {
return Err(fail("output context must be a dict".to_owned()));
}
json
}
};
Ok(Output {
template: template.as_str().to_owned(),
path: path.as_str().to_owned(),
context: context.to_string(),
})
}
}
fn registry<'a>(eval: &Evaluator<'_, 'a, '_>) -> starlark::Result<&'a Registry> {
eval.extra
.and_then(|extra| extra.downcast_ref::<Registry>())
.ok_or_else(|| {
fail(
"schema(), check(), and generator() may only be called from the entry module"
.to_owned(),
)
})
}
fn require_callable(value: Value<'_>, label: &str) -> starlark::Result<()> {
let kind = value.get_type();
if kind == "function" {
Ok(())
} else {
Err(fail(format!("{label} must be a function, found {kind}")))
}
}
#[starlark_module]
pub fn registration(builder: &mut GlobalsBuilder) {
fn schema<'v>(
#[starlark(require = pos)] id: &str,
#[starlark(require = named, default = NoneOr::None)] shape: NoneOr<&str>,
#[starlark(require = named, default = NoneOr::None)] validate: NoneOr<Value<'v>>,
eval: &mut Evaluator<'v, '_, '_>,
) -> starlark::Result<NoneType> {
identity::check_schema_id(id).map_err(fail)?;
let registry = registry(eval)?;
if registry
.schemas
.borrow()
.iter()
.any(|existing| existing.id == id)
{
return Err(fail(format!("schema `{id}` is registered twice")));
}
let shape = match shape {
NoneOr::None => None,
NoneOr::Other(text) => {
let path = ProjectPath::parse(text)
.map_err(|error| fail(format!("schema shape: {error}")))?;
if path
.file_name()
.strip_suffix(".schema.toml")
.is_none_or(str::is_empty)
{
return Err(fail(format!(
"schema shape `{text}` must be a `.schema.toml` file"
)));
}
Some(path.as_str().to_owned())
}
};
let validate = match validate {
NoneOr::None => None,
NoneOr::Other(function) => {
require_callable(function, "schema validate")?;
let slot = registry.next_slot("validate");
eval.module().set(&slot, function);
Some(slot)
}
};
registry.schemas.borrow_mut().push(SchemaRegistration {
id: id.to_owned(),
shape,
validate,
});
Ok(NoneType)
}
fn check<'v>(
#[starlark(require = pos)] name: &str,
#[starlark(require = pos)] function: Value<'v>,
eval: &mut Evaluator<'v, '_, '_>,
) -> starlark::Result<NoneType> {
identity::check_kind(name).map_err(|error| fail(format!("check name: {error}")))?;
require_callable(function, "check function")?;
let registry = registry(eval)?;
if registry
.checks
.borrow()
.iter()
.any(|(existing, _)| existing == name)
{
return Err(fail(format!("check `{name}` is registered twice")));
}
let slot = registry.next_slot("check");
eval.module().set(&slot, function);
registry.checks.borrow_mut().push((name.to_owned(), slot));
Ok(NoneType)
}
fn generator<'v>(
#[starlark(require = pos)] name: &str,
#[starlark(require = pos)] function: Value<'v>,
eval: &mut Evaluator<'v, '_, '_>,
) -> starlark::Result<NoneType> {
identity::check_kind(name).map_err(|error| fail(format!("generator name: {error}")))?;
require_callable(function, "generator function")?;
let registry = registry(eval)?;
if registry
.generators
.borrow()
.iter()
.any(|(existing, _)| existing == name)
{
return Err(fail(format!("generator `{name}` is registered twice")));
}
let slot = registry.next_slot("generator");
eval.module().set(&slot, function);
registry
.generators
.borrow_mut()
.push((name.to_owned(), slot));
Ok(NoneType)
}
}