use std::{
collections::{btree_map::Values, BTreeMap},
fs,
path::{Path, PathBuf},
slice::Iter,
};
use schemars::schema::*;
use super::{Error, PERMISSION_SCHEMAS_FOLDER_NAME};
use crate::{platform::Target, write_if_changed};
use super::{
capability::CapabilityFile,
manifest::{Manifest, PermissionFile},
Permission, PermissionSet, PERMISSION_SCHEMA_FILE_NAME,
};
pub const CAPABILITIES_SCHEMA_FILE_NAME: &str = "schema.json";
pub const CAPABILITIES_SCHEMA_FOLDER_PATH: &str = "gen/schemas";
pub trait PermissionSchemaGenerator<
'a,
Ps: Iterator<Item = &'a PermissionSet>,
P: Iterator<Item = &'a Permission>,
>
{
fn has_default_permission_set(&self) -> bool;
fn default_set_description(&self) -> Option<&str>;
fn default_set_permissions(&self) -> Option<&Vec<String>>;
fn permission_sets(&'a self) -> Ps;
fn permissions(&'a self) -> P;
fn perm_id_schema(name: Option<&str>, id: &str, description: Option<&str>) -> Schema {
let command_name = match name {
Some(name) if name == super::APP_ACL_KEY => id.to_string(),
Some(name) => format!("{name}:{id}"),
_ => id.to_string(),
};
let extensions = if let Some(description) = description {
[(
"markdownDescription".to_string(),
serde_json::Value::String(description.to_string()),
)]
.into()
} else {
Default::default()
};
Schema::Object(SchemaObject {
metadata: Some(Box::new(Metadata {
description: description.map(ToString::to_string),
..Default::default()
})),
instance_type: Some(InstanceType::String.into()),
const_value: Some(serde_json::Value::String(command_name)),
extensions,
..Default::default()
})
}
fn gen_possible_permission_schemas(&'a self, name: Option<&str>) -> Vec<Schema> {
let mut permission_schemas = Vec::new();
if self.has_default_permission_set() {
let description = self.default_set_description().unwrap_or_default();
let description = if let Some(permissions) = self.default_set_permissions() {
add_permissions_to_description(description, permissions, true)
} else {
description.to_string()
};
if !description.is_empty() {
let default = Self::perm_id_schema(name, "default", Some(&description));
permission_schemas.push(default);
}
}
for set in self.permission_sets() {
let description = add_permissions_to_description(&set.description, &set.permissions, false);
let schema = Self::perm_id_schema(name, &set.identifier, Some(&description));
permission_schemas.push(schema);
}
for perm in self.permissions() {
let schema = Self::perm_id_schema(name, &perm.identifier, perm.description.as_deref());
permission_schemas.push(schema);
}
permission_schemas
}
}
fn add_permissions_to_description(
description: &str,
permissions: &[String],
is_default: bool,
) -> String {
if permissions.is_empty() {
return description.to_string();
}
let permissions_list = permissions
.iter()
.map(|permission| format!("- `{permission}`"))
.collect::<Vec<_>>()
.join("\n");
let default_permission_set = if is_default {
"default permission set"
} else {
"permission set"
};
format!("{description}\n#### This {default_permission_set} includes:\n\n{permissions_list}")
}
impl<'a>
PermissionSchemaGenerator<
'a,
Values<'a, std::string::String, PermissionSet>,
Values<'a, std::string::String, Permission>,
> for Manifest
{
fn has_default_permission_set(&self) -> bool {
self.default_permission.is_some()
}
fn default_set_description(&self) -> Option<&str> {
self
.default_permission
.as_ref()
.map(|d| d.description.as_str())
}
fn default_set_permissions(&self) -> Option<&Vec<String>> {
self.default_permission.as_ref().map(|d| &d.permissions)
}
fn permission_sets(&'a self) -> Values<'a, std::string::String, PermissionSet> {
self.permission_sets.values()
}
fn permissions(&'a self) -> Values<'a, std::string::String, Permission> {
self.permissions.values()
}
}
impl<'a> PermissionSchemaGenerator<'a, Iter<'a, PermissionSet>, Iter<'a, Permission>>
for PermissionFile
{
fn has_default_permission_set(&self) -> bool {
self.default.is_some()
}
fn default_set_description(&self) -> Option<&str> {
self.default.as_ref().and_then(|d| d.description.as_deref())
}
fn default_set_permissions(&self) -> Option<&Vec<String>> {
self.default.as_ref().map(|d| &d.permissions)
}
fn permission_sets(&'a self) -> Iter<'a, PermissionSet> {
self.set.iter()
}
fn permissions(&'a self) -> Iter<'a, Permission> {
self.permission.iter()
}
}
fn extend_identifier_schema(schema: &mut RootSchema, acl: &BTreeMap<String, Manifest>) {
if let Some(Schema::Object(identifier_schema)) = schema.definitions.get_mut("Identifier") {
let permission_schemas = acl
.iter()
.flat_map(|(name, manifest)| manifest.gen_possible_permission_schemas(Some(name)))
.collect::<Vec<_>>();
let new_subschemas = Box::new(SubschemaValidation {
one_of: Some(permission_schemas),
..Default::default()
});
identifier_schema.subschemas = Some(new_subschemas);
identifier_schema.object = None;
identifier_schema.instance_type = None;
identifier_schema.metadata().description = Some("Permission identifier".to_string());
}
}
fn extend_permission_entry_schema(root_schema: &mut RootSchema, acl: &BTreeMap<String, Manifest>) {
const IDENTIFIER: &str = "identifier";
const ALLOW: &str = "allow";
const DENY: &str = "deny";
let mut collected_defs = vec![];
if let Some(Schema::Object(obj)) = root_schema.definitions.get_mut("PermissionEntry") {
let any_of = obj.subschemas().any_of.as_mut().unwrap();
let Schema::Object(extend_permission_entry) = any_of.last_mut().unwrap() else {
unreachable!("PermissionsEntry should be an object not a boolean");
};
let obj = extend_permission_entry.object.as_mut().unwrap();
let default_properties = std::mem::take(&mut obj.properties);
let default_identifier = default_properties.get(IDENTIFIER).cloned().unwrap();
let default_identifier = (IDENTIFIER.to_string(), default_identifier);
let mut all_of = vec![];
let schemas = acl.iter().filter_map(|(name, manifest)| {
manifest
.global_scope_schema()
.unwrap_or_else(|e| panic!("invalid JSON schema for plugin {name}: {e}"))
.map(|s| (s, manifest.gen_possible_permission_schemas(Some(name))))
});
for ((scope_schema, defs), acl_perm_schema) in schemas {
let mut perm_schema = SchemaObject::default();
perm_schema.subschemas().any_of = Some(acl_perm_schema);
let mut if_schema = SchemaObject::default();
if_schema.object().properties = [(IDENTIFIER.to_string(), perm_schema.into())].into();
let mut then_schema = SchemaObject::default();
then_schema.object().properties = [
(ALLOW.to_string(), scope_schema.clone()),
(DENY.to_string(), scope_schema.clone()),
]
.into();
let mut obj = SchemaObject::default();
obj.object().properties = [default_identifier.clone()].into();
obj.subschemas().if_schema = Some(Box::new(if_schema.into()));
obj.subschemas().then_schema = Some(Box::new(then_schema.into()));
all_of.push(Schema::Object(obj));
collected_defs.extend(defs);
}
let mut default_obj = SchemaObject::default();
default_obj.object().properties = default_properties;
all_of.push(Schema::Object(default_obj));
extend_permission_entry.subschemas().all_of = Some(all_of);
}
root_schema.definitions.extend(collected_defs);
}
pub fn generate_capability_schema(
acl: &BTreeMap<String, Manifest>,
target: Target,
) -> crate::Result<()> {
let mut schema = schemars::schema_for!(CapabilityFile);
extend_identifier_schema(&mut schema, acl);
extend_permission_entry_schema(&mut schema, acl);
let schema_str = serde_json::to_string_pretty(&schema).unwrap();
let schema_str = schema_str.replace("\\r\\n", "\\n");
let out_dir = PathBuf::from(CAPABILITIES_SCHEMA_FOLDER_PATH);
fs::create_dir_all(&out_dir)?;
let schema_path = out_dir.join(format!("{target}-{CAPABILITIES_SCHEMA_FILE_NAME}"));
if schema_str != fs::read_to_string(&schema_path).unwrap_or_default() {
fs::write(&schema_path, schema_str)?;
fs::copy(
schema_path,
out_dir.join(format!(
"{}-{CAPABILITIES_SCHEMA_FILE_NAME}",
if target.is_desktop() {
"desktop"
} else {
"mobile"
}
)),
)?;
}
Ok(())
}
fn extend_permission_file_schema(schema: &mut RootSchema, permissions: &[PermissionFile]) {
let permission_schemas = permissions
.iter()
.flat_map(|p| p.gen_possible_permission_schemas(None))
.collect();
if let Some(Schema::Object(obj)) = schema.definitions.get_mut("PermissionSet") {
let permissions_obj = obj.object().properties.get_mut("permissions");
if let Some(Schema::Object(permissions_obj)) = permissions_obj {
permissions_obj.array().items.replace(
Schema::Object(SchemaObject {
reference: Some("#/definitions/PermissionKind".into()),
..Default::default()
})
.into(),
);
schema.definitions.insert(
"PermissionKind".into(),
Schema::Object(SchemaObject {
instance_type: Some(InstanceType::String.into()),
subschemas: Some(Box::new(SubschemaValidation {
one_of: Some(permission_schemas),
..Default::default()
})),
..Default::default()
}),
);
}
}
}
pub fn generate_permissions_schema<P: AsRef<Path>>(
permissions: &[PermissionFile],
out_dir: P,
) -> Result<(), Error> {
let mut schema = schemars::schema_for!(PermissionFile);
extend_permission_file_schema(&mut schema, permissions);
let schema_str = serde_json::to_string_pretty(&schema)?;
let schema_str = schema_str.replace("\\r\\n", "\\n");
let out_dir = out_dir.as_ref().join(PERMISSION_SCHEMAS_FOLDER_NAME);
fs::create_dir_all(&out_dir).map_err(|e| Error::CreateDir(e, out_dir.clone()))?;
let schema_path = out_dir.join(PERMISSION_SCHEMA_FILE_NAME);
write_if_changed(&schema_path, schema_str).map_err(|e| Error::WriteFile(e, schema_path))?;
Ok(())
}