blob: fec29420ad0b3f386fa0e250e84e5df543829a50 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//! Parser for `core/core/src/types/capability.rs`.
//!
//! Reads the `Capability` struct from opendal core and exposes its fields as
//! a flat list, so that binding generators can render language mirrors from
//! the single source of truth instead of hand-maintaining copies.
use anyhow::{Context, Result, bail};
use serde::Serialize;
use std::fs;
use std::path::Path;
use syn::{GenericArgument, Item, ItemStruct, PathArguments, Type, TypePath};
use super::options::doc_lines;
/// A single field of the core `Capability` struct.
///
/// Only the two shapes that actually occur in core are supported: plain
/// `bool` flags and `Option<usize>` limits. Anything else fails parsing so
/// that a new field shape upstream breaks the generator loudly instead of
/// producing a silently wrong mirror.
#[derive(Debug, Serialize)]
pub struct CapabilityField {
/// Field name in snake_case, exactly as written in core.
pub name: String,
/// Whether the field is a plain `bool` flag (`false` means `Option<usize>`).
pub is_bool: bool,
/// Doc comment of the field, joined to a single line.
pub doc: String,
}
/// Parse the `Capability` struct out of the given core source file.
pub fn parse(path: &Path) -> Result<Vec<CapabilityField>> {
let content =
fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?;
let ast = syn::parse_file(&content)?;
for item in ast.items {
if let Item::Struct(struct_) = item
&& struct_.ident == "Capability"
{
return parse_struct(&struct_);
}
}
bail!("struct Capability not found in {}", path.display())
}
fn parse_struct(struct_: &ItemStruct) -> Result<Vec<CapabilityField>> {
let syn::Fields::Named(named) = &struct_.fields else {
bail!("struct Capability must have named fields");
};
let mut fields = Vec::with_capacity(named.named.len());
for field in &named.named {
let name = field
.ident
.as_ref()
.expect("named field always has an ident")
.to_string();
let is_bool = match parse_type(&field.ty) {
Some(is_bool) => is_bool,
None => bail!(
"capability field `{name}` has an unsupported type; \
teach dev/src/generate/capability.rs about it before regenerating"
),
};
let doc = doc_lines(&field.attrs).join(" ").trim().to_string();
fields.push(CapabilityField { name, is_bool, doc });
}
Ok(fields)
}
/// Returns `Some(true)` for `bool`, `Some(false)` for `Option<usize>`, and
/// `None` for anything else.
fn parse_type(ty: &Type) -> Option<bool> {
let Type::Path(TypePath { path, .. }) = ty else {
return None;
};
let segment = path.segments.last()?;
if segment.ident == "bool" {
return Some(true);
}
if segment.ident == "Option"
&& let PathArguments::AngleBracketed(args) = &segment.arguments
&& let Some(GenericArgument::Type(Type::Path(inner))) = args.args.first()
&& inner.path.is_ident("usize")
{
return Some(false);
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_core_capability() {
let path = crate::workspace_dir().join("core/core/src/types/capability.rs");
let fields = parse(&path).expect("core capability must parse");
// The struct is large and every field must be either a flag or a
// limit; spot-check both shapes and that docs came through.
assert!(fields.len() > 50);
let stat = fields.iter().find(|f| f.name == "stat").unwrap();
assert!(stat.is_bool);
assert!(!stat.doc.is_empty());
let limit = fields
.iter()
.find(|f| f.name == "write_multi_max_size")
.unwrap();
assert!(!limit.is_bool);
}
}