forked from 1b-agent/opencadstudio-upstream
feat(mcp): describe complete CAD record schemas
This commit is contained in:
1 parent
8f574c8388
commit
4ddd19f6e8
10 files changed
+1329
-32
No files matched your search
@@ -219,7 +219,7 @@ Broadcasting is V4-gated inside `PluginManager::broadcast_notification`: V2/V3 p
|
||||
|
||||
The crate embeds two JSON blobs at build time:
|
||||
|
||||
- **Type registry** (`OUT_DIR/type_registry.json`) — a language-binding-friendly schema for a curated allow-list of `acadrust` types, generated by `serde-reflection` in `build.rs`. See [`src/type_registry.rs`](src/type_registry.rs).
|
||||
- **Type registry** (`OUT_DIR/type_registry.json`) — a language-binding-friendly schema for every editor record root and its dependent types, generated by `serde-reflection` in `build.rs`. See [`src/type_registry.rs`](src/type_registry.rs).
|
||||
- **Version info** (`OUT_DIR/version_info.json`) — host version, `ocs_plugin_api` version, `acadrust` version and source, `rustc` version, API versions, build timestamp. See [`src/version_info.rs`](src/version_info.rs).
|
||||
|
||||
Both are accessible without enabling the `host` feature.
|
||||
|
||||
@@ -243,7 +243,7 @@ session belongs to.
|
||||
`ocs_plugin_api` embeds two JSON blobs at build time:
|
||||
|
||||
- **Type registry** — a stable, language-binding-friendly schema generated by
|
||||
tracing a curated allow-list of `acadrust` types with `serde-reflection`.
|
||||
tracing every editor record root and its dependent types with `serde-reflection`.
|
||||
- **Version info** — `ocs_version`, `ocs_plugin_api_version`, `acadrust_version`,
|
||||
`api_version`, etc.
|
||||
|
||||
|
||||
@@ -34,11 +34,42 @@ fn generate_type_registry(out_dir: &Path) {
|
||||
let mut samples = Samples::new();
|
||||
add_enum_samples(&mut tracer, &mut samples);
|
||||
|
||||
// Curated allow-list of acadrust types. We intentionally exclude
|
||||
// EntityType here to avoid pulling in all 41+ entity variants and any
|
||||
// custom-serializer edge cases.
|
||||
// Trace the complete record roots used by the editor and MCP API. Nested
|
||||
// entity/object variants and their enums are added to the same registry.
|
||||
type TraceFn = fn(&mut Tracer, &Samples);
|
||||
let types: Vec<(&str, TraceFn)> = vec![
|
||||
("EntityType", trace::<acadrust::EntityType>),
|
||||
("ObjectType", trace::<acadrust::objects::ObjectType>),
|
||||
(
|
||||
"HeaderVariables",
|
||||
trace::<acadrust::document::HeaderVariables>,
|
||||
),
|
||||
("SummaryInfo", trace::<acadrust::document::SummaryInfo>),
|
||||
("LineType", trace::<acadrust::LineType>),
|
||||
("TextStyle", trace::<acadrust::TextStyle>),
|
||||
("BlockRecord", trace::<acadrust::BlockRecord>),
|
||||
("DimStyle", trace::<acadrust::DimStyle>),
|
||||
("AppId", trace::<acadrust::AppId>),
|
||||
("View", trace::<acadrust::View>),
|
||||
("VPort", trace::<acadrust::VPort>),
|
||||
("Ucs", trace::<acadrust::Ucs>),
|
||||
("VxTableRecord", trace::<acadrust::VxTableRecord>),
|
||||
("DxfClass", trace::<acadrust::classes::DxfClass>),
|
||||
(
|
||||
"BlockVisibilityParameter",
|
||||
trace::<acadrust::objects::BlockVisibilityParameter>,
|
||||
),
|
||||
("FieldDef", trace::<acadrust::document::FieldDef>),
|
||||
(
|
||||
"DgnLsDefinition",
|
||||
trace::<acadrust::objects::DgnLsDefinition>,
|
||||
),
|
||||
("DgnLsComponent", trace::<acadrust::objects::DgnLsComponent>),
|
||||
(
|
||||
"NotificationCollection",
|
||||
trace::<acadrust::notification::NotificationCollection>,
|
||||
),
|
||||
("Preview", trace::<acadrust::document::Preview>),
|
||||
("Point", trace::<acadrust::Point>),
|
||||
("Line", trace::<acadrust::Line>),
|
||||
("Circle", trace::<acadrust::Circle>),
|
||||
@@ -93,10 +124,366 @@ fn generate_type_registry(out_dir: &Path) {
|
||||
);
|
||||
}
|
||||
|
||||
type TraceSimpleFn = fn(&mut Tracer);
|
||||
let enums: Vec<(&str, TraceSimpleFn)> = vec![
|
||||
(
|
||||
"AssocAnnotationKind",
|
||||
trace_simple::<acadrust::objects::AssocAnnotationKind>,
|
||||
),
|
||||
(
|
||||
"AssocConstraintNodeData",
|
||||
trace_simple::<acadrust::objects::AssocConstraintNodeData>,
|
||||
),
|
||||
(
|
||||
"AssocEvalValue",
|
||||
trace_simple::<acadrust::objects::AssocEvalValue>,
|
||||
),
|
||||
(
|
||||
"AssocSubcurveKind",
|
||||
trace_simple::<acadrust::objects::AssocSubcurveKind>,
|
||||
),
|
||||
(
|
||||
"AssocSurfaceActionKind",
|
||||
trace_simple::<acadrust::objects::AssocSurfaceActionKind>,
|
||||
),
|
||||
(
|
||||
"AssocViewObjectActionParamKind",
|
||||
trace_simple::<acadrust::objects::AssocViewObjectActionParamKind>,
|
||||
),
|
||||
(
|
||||
"AcisVersion",
|
||||
trace_simple::<acadrust::entities::AcisVersion>,
|
||||
),
|
||||
(
|
||||
"AssociativeData",
|
||||
trace_simple::<acadrust::objects::AssociativeData>,
|
||||
),
|
||||
(
|
||||
"AttachmentPointType",
|
||||
trace_simple::<acadrust::entities::AttachmentPointType>,
|
||||
),
|
||||
(
|
||||
"BlockContentConnectionType",
|
||||
trace_simple::<acadrust::entities::BlockContentConnectionType>,
|
||||
),
|
||||
(
|
||||
"BlockEvalValue",
|
||||
trace_simple::<acadrust::objects::BlockEvalValue>,
|
||||
),
|
||||
("BorderType", trace_simple::<acadrust::entities::BorderType>),
|
||||
(
|
||||
"BoundaryEdge",
|
||||
trace_simple::<acadrust::entities::BoundaryEdge>,
|
||||
),
|
||||
(
|
||||
"BreakFlowDirection",
|
||||
trace_simple::<acadrust::entities::BreakFlowDirection>,
|
||||
),
|
||||
(
|
||||
"CellAlignment",
|
||||
trace_simple::<acadrust::objects::CellAlignment>,
|
||||
),
|
||||
(
|
||||
"CellStyleType",
|
||||
trace_simple::<acadrust::entities::CellStyleType>,
|
||||
),
|
||||
("CellType", trace_simple::<acadrust::entities::CellType>),
|
||||
(
|
||||
"CellValueType",
|
||||
trace_simple::<acadrust::entities::CellValueType>,
|
||||
),
|
||||
(
|
||||
"ClassObjectData",
|
||||
trace_simple::<acadrust::objects::ClassObjectData>,
|
||||
),
|
||||
("ClipMode", trace_simple::<acadrust::entities::ClipMode>),
|
||||
("ClipType", trace_simple::<acadrust::entities::ClipType>),
|
||||
(
|
||||
"CompoundEntry",
|
||||
trace_simple::<acadrust::compound_file::CompoundEntry>,
|
||||
),
|
||||
(
|
||||
"CompoundPropertyValue",
|
||||
trace_simple::<acadrust::compound_file::CompoundPropertyValue>,
|
||||
),
|
||||
(
|
||||
"CompoundStreamContent",
|
||||
trace_simple::<acadrust::compound_file::CompoundStreamContent>,
|
||||
),
|
||||
(
|
||||
"DataObjectData",
|
||||
trace_simple::<acadrust::objects::DataObjectData>,
|
||||
),
|
||||
(
|
||||
"DgnLineStyleData",
|
||||
trace_simple::<acadrust::objects::DgnLineStyleData>,
|
||||
),
|
||||
(
|
||||
"DgnLsComponentData",
|
||||
trace_simple::<acadrust::objects::DgnLsComponentData>,
|
||||
),
|
||||
(
|
||||
"DgnLsComponentType",
|
||||
trace_simple::<acadrust::objects::DgnLsComponentType>,
|
||||
),
|
||||
(
|
||||
"DgnLsPhaseMode",
|
||||
trace_simple::<acadrust::objects::DgnLsPhaseMode>,
|
||||
),
|
||||
("DimSubtype", trace_simple::<acadrust::objects::DimSubtype>),
|
||||
("Dimension", trace_simple::<acadrust::entities::Dimension>),
|
||||
(
|
||||
"DimensionType",
|
||||
trace_simple::<acadrust::entities::DimensionType>,
|
||||
),
|
||||
(
|
||||
"DynamicBlockData",
|
||||
trace_simple::<acadrust::objects::DynamicBlockData>,
|
||||
),
|
||||
(
|
||||
"EmbeddedEntity",
|
||||
trace_simple::<acadrust::entities::EmbeddedEntity>,
|
||||
),
|
||||
(
|
||||
"ExtendedEntityData",
|
||||
trace_simple::<acadrust::entities::ExtendedEntityData>,
|
||||
),
|
||||
(
|
||||
"FlowDirectionType",
|
||||
trace_simple::<acadrust::entities::FlowDirectionType>,
|
||||
),
|
||||
(
|
||||
"HatchPatternType",
|
||||
trace_simple::<acadrust::entities::HatchPatternType>,
|
||||
),
|
||||
(
|
||||
"HatchStyleType",
|
||||
trace_simple::<acadrust::entities::HatchStyleType>,
|
||||
),
|
||||
(
|
||||
"HelixConstraint",
|
||||
trace_simple::<acadrust::entities::HelixConstraint>,
|
||||
),
|
||||
(
|
||||
"HooklineDirection",
|
||||
trace_simple::<acadrust::entities::HooklineDirection>,
|
||||
),
|
||||
(
|
||||
"HorizontalAlignment",
|
||||
trace_simple::<acadrust::entities::HorizontalAlignment>,
|
||||
),
|
||||
(
|
||||
"LeaderContentType",
|
||||
trace_simple::<acadrust::entities::LeaderContentType>,
|
||||
),
|
||||
(
|
||||
"LeaderCreationType",
|
||||
trace_simple::<acadrust::entities::LeaderCreationType>,
|
||||
),
|
||||
(
|
||||
"LeaderDrawOrderType",
|
||||
trace_simple::<acadrust::objects::LeaderDrawOrderType>,
|
||||
),
|
||||
(
|
||||
"LeaderPathType",
|
||||
trace_simple::<acadrust::entities::LeaderPathType>,
|
||||
),
|
||||
(
|
||||
"LineTypeComplexContent",
|
||||
trace_simple::<acadrust::tables::LineTypeComplexContent>,
|
||||
),
|
||||
(
|
||||
"LegacyEntityData",
|
||||
trace_simple::<acadrust::entities::LegacyEntityData>,
|
||||
),
|
||||
(
|
||||
"MLineJustification",
|
||||
trace_simple::<acadrust::entities::MLineJustification>,
|
||||
),
|
||||
("MTextFlag", trace_simple::<acadrust::entities::MTextFlag>),
|
||||
(
|
||||
"MaterialProceduralValue",
|
||||
trace_simple::<acadrust::objects::MaterialProceduralValue>,
|
||||
),
|
||||
(
|
||||
"MultiLeaderDrawOrderType",
|
||||
trace_simple::<acadrust::objects::MultiLeaderDrawOrderType>,
|
||||
),
|
||||
(
|
||||
"MultiLeaderPathType",
|
||||
trace_simple::<acadrust::entities::MultiLeaderPathType>,
|
||||
),
|
||||
(
|
||||
"ObjectContextKind",
|
||||
trace_simple::<acadrust::objects::ObjectContextKind>,
|
||||
),
|
||||
(
|
||||
"NotificationType",
|
||||
trace_simple::<acadrust::notification::NotificationType>,
|
||||
),
|
||||
(
|
||||
"OleFrameEnvelope",
|
||||
trace_simple::<acadrust::entities::OleFrameEnvelope>,
|
||||
),
|
||||
(
|
||||
"OleObjectType",
|
||||
trace_simple::<acadrust::entities::OleObjectType>,
|
||||
),
|
||||
(
|
||||
"PlotPaperUnits",
|
||||
trace_simple::<acadrust::objects::PlotPaperUnits>,
|
||||
),
|
||||
(
|
||||
"PlotRotation",
|
||||
trace_simple::<acadrust::objects::PlotRotation>,
|
||||
),
|
||||
("PlotType", trace_simple::<acadrust::objects::PlotType>),
|
||||
(
|
||||
"PreviewFormat",
|
||||
trace_simple::<acadrust::document::PreviewFormat>,
|
||||
),
|
||||
(
|
||||
"PolyfaceSmoothType",
|
||||
trace_simple::<acadrust::entities::PolyfaceSmoothType>,
|
||||
),
|
||||
(
|
||||
"ProxyPayloadEncoding",
|
||||
trace_simple::<acadrust::objects::ProxyPayloadEncoding>,
|
||||
),
|
||||
(
|
||||
"ResolutionUnit",
|
||||
trace_simple::<acadrust::objects::ResolutionUnit>,
|
||||
),
|
||||
("ScaledType", trace_simple::<acadrust::objects::ScaledType>),
|
||||
(
|
||||
"SemanticPropertyValue",
|
||||
trace_simple::<acadrust::objects::SemanticPropertyValue>,
|
||||
),
|
||||
(
|
||||
"ShadePlotMode",
|
||||
trace_simple::<acadrust::objects::ShadePlotMode>,
|
||||
),
|
||||
(
|
||||
"ShadePlotResolutionLevel",
|
||||
trace_simple::<acadrust::objects::ShadePlotResolutionLevel>,
|
||||
),
|
||||
(
|
||||
"SolidHistoryOperation",
|
||||
trace_simple::<acadrust::objects::SolidHistoryOperation>,
|
||||
),
|
||||
(
|
||||
"SurfaceData",
|
||||
trace_simple::<acadrust::entities::SurfaceData>,
|
||||
),
|
||||
(
|
||||
"SurfaceKind",
|
||||
trace_simple::<acadrust::entities::SurfaceKind>,
|
||||
),
|
||||
(
|
||||
"SurfaceSmoothType",
|
||||
trace_simple::<acadrust::entities::SurfaceSmoothType>,
|
||||
),
|
||||
(
|
||||
"TableBorderType",
|
||||
trace_simple::<acadrust::objects::TableBorderType>,
|
||||
),
|
||||
(
|
||||
"TableCellContentType",
|
||||
trace_simple::<acadrust::entities::TableCellContentType>,
|
||||
),
|
||||
(
|
||||
"TableFlowDirection",
|
||||
trace_simple::<acadrust::objects::TableFlowDirection>,
|
||||
),
|
||||
(
|
||||
"TextAlignmentType",
|
||||
trace_simple::<acadrust::entities::TextAlignmentType>,
|
||||
),
|
||||
(
|
||||
"TextAngleType",
|
||||
trace_simple::<acadrust::entities::TextAngleType>,
|
||||
),
|
||||
(
|
||||
"TextAttachmentDirectionType",
|
||||
trace_simple::<acadrust::entities::TextAttachmentDirectionType>,
|
||||
),
|
||||
(
|
||||
"TextAttachmentPointType",
|
||||
trace_simple::<acadrust::entities::TextAttachmentPointType>,
|
||||
),
|
||||
(
|
||||
"TextAttachmentType",
|
||||
trace_simple::<acadrust::entities::TextAttachmentType>,
|
||||
),
|
||||
(
|
||||
"TextHorizontalAlignment",
|
||||
trace_simple::<acadrust::entities::TextHorizontalAlignment>,
|
||||
),
|
||||
(
|
||||
"TextVerticalAlignment",
|
||||
trace_simple::<acadrust::entities::TextVerticalAlignment>,
|
||||
),
|
||||
(
|
||||
"UnderlayType",
|
||||
trace_simple::<acadrust::entities::UnderlayType>,
|
||||
),
|
||||
(
|
||||
"ValueUnitType",
|
||||
trace_simple::<acadrust::entities::ValueUnitType>,
|
||||
),
|
||||
(
|
||||
"VbaDirectoryValue",
|
||||
trace_simple::<acadrust::vba::VbaDirectoryValue>,
|
||||
),
|
||||
(
|
||||
"VerticalAlignment",
|
||||
trace_simple::<acadrust::entities::VerticalAlignment>,
|
||||
),
|
||||
(
|
||||
"ViewportRenderMode",
|
||||
trace_simple::<acadrust::entities::ViewportRenderMode>,
|
||||
),
|
||||
(
|
||||
"VisualStylePropertyValue",
|
||||
trace_simple::<acadrust::objects::VisualStylePropertyValue>,
|
||||
),
|
||||
(
|
||||
"ViewRepSketchGeometry",
|
||||
trace_simple::<acadrust::objects::ViewRepSketchGeometry>,
|
||||
),
|
||||
(
|
||||
"WipeoutClipMode",
|
||||
trace_simple::<acadrust::entities::WipeoutClipMode>,
|
||||
),
|
||||
(
|
||||
"WipeoutClipType",
|
||||
trace_simple::<acadrust::entities::WipeoutClipType>,
|
||||
),
|
||||
("WireType", trace_simple::<acadrust::entities::WireType>),
|
||||
];
|
||||
for (name, trace) in enums {
|
||||
trace(&mut tracer);
|
||||
eprintln!("[ocs_plugin_api build] traced enum variants: {name}");
|
||||
}
|
||||
|
||||
let traced = tracer
|
||||
.registry()
|
||||
.expect("type registry tracing failed; see stderr for individual errors");
|
||||
let registry = map_to_custom_schema(&traced);
|
||||
let mut registry = map_to_custom_schema(&traced);
|
||||
let mut section_style_tracer = Tracer::new(TracerConfig::default());
|
||||
section_style_tracer
|
||||
.trace_simple_type::<acadrust::entities::SectionViewStyle>()
|
||||
.expect("section view style tracing failed");
|
||||
let section_style_registry = section_style_tracer
|
||||
.registry()
|
||||
.expect("section view style registry failed");
|
||||
let section_style = section_style_registry
|
||||
.get("SectionViewStyle")
|
||||
.expect("section view style registry entry");
|
||||
registry.types.insert(
|
||||
TypeId::new("EntitySectionViewStyle"),
|
||||
map_container("EntitySectionViewStyle", section_style),
|
||||
);
|
||||
let json = serde_json::to_string_pretty(®istry).unwrap();
|
||||
fs::write(out_dir.join("type_registry.json"), json).unwrap();
|
||||
}
|
||||
@@ -114,10 +501,22 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn trace_simple<T>(tracer: &mut Tracer)
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
if let Err(error) = tracer.trace_simple_type::<T>() {
|
||||
panic!(
|
||||
"type registry enum tracing failed for {}: {error}",
|
||||
std::any::type_name::<T>()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_enum_samples(tracer: &mut Tracer, samples: &mut Samples) {
|
||||
// serde-reflection needs at least one sample value per enum variant in
|
||||
// order to reconstruct the full schema. Provide samples for the enums that
|
||||
// appear inside the allow-list types below.
|
||||
// need concrete serialized values.
|
||||
let _ = tracer.trace_value(samples, &acadrust::LineWeight::ByLayer);
|
||||
let _ = tracer.trace_value(samples, &acadrust::LineWeight::ByBlock);
|
||||
let _ = tracer.trace_value(samples, &acadrust::LineWeight::Default);
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
//! Embedded type registry generated at build time.
|
||||
//!
|
||||
//! The JSON embedded here is produced by tracing a curated allow-list of
|
||||
//! `acadrust` types with `serde-reflection` and mapping the result into a
|
||||
//! stable, language-binding-friendly schema defined in
|
||||
//! The JSON embedded here is produced by tracing every editor record root and
|
||||
//! its dependent `acadrust` types with `serde-reflection`, then mapping the
|
||||
//! result into a stable, language-binding-friendly schema defined in
|
||||
//! [`crate::type_registry_types`]. The registry is embedded via
|
||||
//! `include_str!(concat!(env!("OUT_DIR"), "/type_registry.json"))` so the
|
||||
//! `host` feature is not required to read it.
|
||||
//!
|
||||
//! The allow-list is intentionally narrow: it excludes generic containers and
|
||||
//! internal enums such as `EntityType` in favor of concrete serializable structs
|
||||
//! like `Point`, `Line`, `Circle`, and `MText`. Language bindings and tooling
|
||||
//! can depend on this schema without pulling the full `acadrust` dependency
|
||||
//! tree.
|
||||
//! The registry covers the complete entity, object, symbol-table, header, and
|
||||
//! document-record roots used by the editor. Language bindings and tooling can
|
||||
//! inspect every variant without pulling the full `acadrust` dependency tree.
|
||||
|
||||
pub use crate::type_registry_types::*;
|
||||
|
||||
@@ -121,13 +119,45 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedded_type_registry_does_not_contain_entity_type() {
|
||||
fn embedded_type_registry_contains_every_entity_and_object_variant() {
|
||||
let registry: TypeRegistry = serde_json::from_str(get_embedded_type_registry_json())
|
||||
.expect("valid TypeRegistry JSON");
|
||||
assert!(
|
||||
!registry.types.contains_key(&TypeId::new("EntityType")),
|
||||
"EntityType should not be in the allow-list registry"
|
||||
);
|
||||
let entities = registry
|
||||
.types
|
||||
.get(&TypeId::new("EntityType"))
|
||||
.expect("EntityType in complete registry");
|
||||
let objects = registry
|
||||
.types
|
||||
.get(&TypeId::new("ObjectType"))
|
||||
.expect("ObjectType in complete registry");
|
||||
assert_eq!(entities.variants.len(), 48);
|
||||
assert_eq!(objects.variants.len(), 36);
|
||||
for root in [
|
||||
"HeaderVariables",
|
||||
"SummaryInfo",
|
||||
"LineType",
|
||||
"TextStyle",
|
||||
"BlockRecord",
|
||||
"DimStyle",
|
||||
"AppId",
|
||||
"View",
|
||||
"VPort",
|
||||
"Ucs",
|
||||
"VxTableRecord",
|
||||
"DxfClass",
|
||||
"BlockVisibilityParameter",
|
||||
"FieldDef",
|
||||
"DgnLsDefinition",
|
||||
"DgnLsComponent",
|
||||
"NotificationCollection",
|
||||
"Preview",
|
||||
"EntitySectionViewStyle",
|
||||
] {
|
||||
assert!(
|
||||
registry.types.contains_key(&TypeId::new(root)),
|
||||
"missing {root}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -33,6 +33,21 @@ The normal flow is to call `ocs_sessions`, pass its returned `session_id` as `oc
|
||||
|
||||
Call `ocs_read` with `op: "capabilities"` before unfamiliar work. It reports the command, geometry, transaction, capture, and database facilities supported by the running build. The `records.collections` list gives every available database collection, its record count, and whether it can be edited.
|
||||
|
||||
Call `ocs_read` with `op: "record_schema"` before editing an unfamiliar record. With no parameters it lists the complete generated type registry. A collection returns the record types accepted by that collection even when the current drawing has no instance of a type. Supplying both `collection` and `type` returns the type's complete dependency graph, flattened property paths, JSON types, optional and sequence markers, enum variants, integer bounds, unambiguous unit annotations, identity fields, and write rules:
|
||||
|
||||
```json
|
||||
{
|
||||
"ocs_session_id": "SESSION_FROM_OCS_SESSIONS",
|
||||
"op": "record_schema",
|
||||
"parameters": {
|
||||
"collection": "entities",
|
||||
"type": "Insert"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Sequence fields include an `item_path_template` such as `/attributes/{index}`. Replace `{index}` with an index returned by `records` before using the path in `set_properties`. When a matching record exists, `runtime_schema` adds its exact JSON shape and bounded scalar examples. The embedded registry is generated from all 48 entity variants, all 36 non-graphical object variants, symbol-table records, header data, summary data, and the remaining stored record roots at build time.
|
||||
|
||||
`op: "records"` returns every serializable property of entities, non-graphical objects, symbol-table records, the complete header, document metadata, and derived database views. Omitting `collection` returns the same collection manifest; use `collection: "all"` for a paged query across the complete database. Records have a stable collection plus a handle or name, a type, and `properties`. Entity records also include their display and file record type names.
|
||||
|
||||
Property filters and projections use RFC 6901 JSON Pointer paths relative to `properties`. Multiple `where` filters are combined with AND. Supported operators are `eq`, `ne`, `lt`, `lte`, `gt`, `gte`, `contains`, `starts_with`, `ends_with`, `in`, `exists`, and `not_exists`:
|
||||
|
||||
@@ -56,6 +56,7 @@ def legacy(server: Path) -> None:
|
||||
assert execute_request["properties"]["steps"]["maxItems"] == 64
|
||||
assert execute_request["properties"]["cmd"]["examples"][0] == "LINE 0,0 10,10"
|
||||
assert "set_properties" in execute_request["properties"]["op"]["enum"]
|
||||
assert "record_schema" in definitions["ocs_read"]["inputSchema"]["properties"]["op"]["enum"]
|
||||
read_parameters = definitions["ocs_read"]["inputSchema"]["properties"]["parameters"]["properties"]
|
||||
assert {"collection", "where", "paths"} <= set(read_parameters)
|
||||
assert execute_request["properties"]["kind"]["enum"] == [
|
||||
|
||||
@@ -367,6 +367,7 @@ impl OpenCADStudio {
|
||||
"entities" => self.entity_summary(),
|
||||
"query" => self.entity_query(&req),
|
||||
"records" => self.record_query(&req),
|
||||
"record_schema" => self.record_schema(&req),
|
||||
"capabilities" => self.record_capabilities(),
|
||||
"layers" => {
|
||||
let i = self.active_tab;
|
||||
|
||||
@@ -361,7 +361,7 @@ impl OpenCADStudio {
|
||||
"mtext_editor":self.mtext_editor.as_ref().map(|e|json!({"text":e.content.text(),"height":e.height,"style":e.style})),
|
||||
"text_editor":self.text_inline.is_some(),"event_cursor":self.control.serial,
|
||||
"operation":self.control.pending.as_ref().map(|p| &p.id),
|
||||
"capabilities":["commands","command_manifest","step_input","batch","compact_results","entity_pick","structure_pick","selection","properties","records","record_filters","atomic_record_updates","layers","history","documents","events","capture","viewport_capture","measure","spatial_query"]
|
||||
"capabilities":["commands","command_manifest","step_input","batch","compact_results","entity_pick","structure_pick","selection","properties","records","record_schemas","record_filters","atomic_record_updates","layers","history","documents","events","capture","viewport_capture","measure","spatial_query"]
|
||||
})
|
||||
}
|
||||
|
||||
@@ -378,6 +378,7 @@ impl OpenCADStudio {
|
||||
| "measure"
|
||||
| "query"
|
||||
| "records"
|
||||
| "record_schema"
|
||||
| "capabilities"
|
||||
| "entities"
|
||||
| "layers"
|
||||
|
||||
+856
-8
@@ -2,6 +2,8 @@ use acadrust::tables::{Table, TableEntry};
|
||||
use iced::Task;
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use serde_json::{Map, Value, json};
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use super::{Message, OpenCADStudio};
|
||||
|
||||
@@ -36,6 +38,135 @@ const COLLECTIONS: &[(&str, bool)] = &[
|
||||
("document", false),
|
||||
];
|
||||
|
||||
fn collection_root_type(collection: &str) -> Option<&'static str> {
|
||||
Some(match collection {
|
||||
"entities" => "EntityType",
|
||||
"objects" => "ObjectType",
|
||||
"layers" => "Layer",
|
||||
"line_types" => "LineType",
|
||||
"text_styles" => "TextStyle",
|
||||
"block_records" => "BlockRecord",
|
||||
"dim_styles" => "DimStyle",
|
||||
"app_ids" => "AppId",
|
||||
"views" => "View",
|
||||
"vports" => "VPort",
|
||||
"ucss" => "Ucs",
|
||||
"vx_table" => "VxTableRecord",
|
||||
"header" => "HeaderVariables",
|
||||
"summary_info" => "SummaryInfo",
|
||||
"classes" => "DxfClass",
|
||||
"block_visibility" => "BlockVisibilityParameter",
|
||||
"context_scales"
|
||||
| "block_representations"
|
||||
| "vx_control_entries"
|
||||
| "section_view_representations"
|
||||
| "view_rep_references" => "Handle",
|
||||
"fields" => "FieldDef",
|
||||
"dgn_line_style_definitions" => "DgnLsDefinition",
|
||||
"dgn_line_style_components" => "DgnLsComponent",
|
||||
"notifications" => "NotificationCollection",
|
||||
"preview" => "Preview",
|
||||
"section_view_style" => "EntitySectionViewStyle",
|
||||
// This aggregate is assembled by OCS and has a live inferred schema.
|
||||
"document" => "Value",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn collection_is_mutable(collection: &str) -> bool {
|
||||
COLLECTIONS
|
||||
.iter()
|
||||
.find(|(name, _)| *name == collection)
|
||||
.is_some_and(|(_, mutable)| *mutable)
|
||||
}
|
||||
|
||||
fn collection_record_name(collection: &str) -> Option<&'static str> {
|
||||
Some(match collection {
|
||||
"classes" => "Class",
|
||||
"fields" => "Field",
|
||||
"dgn_line_style_definitions" => "LineStyleDefinition",
|
||||
"dgn_line_style_components" => "LineStyleComponent",
|
||||
"section_view_style" => "SectionViewStyle",
|
||||
"view_rep_references" => "HandleReferences",
|
||||
"notifications" => "Notifications",
|
||||
"document" => "Document",
|
||||
_ => collection_root_type(collection)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn identity_paths(collection: &str) -> &'static [&'static str] {
|
||||
match collection {
|
||||
"entities" => &["/common/handle", "/common/owner_handle"],
|
||||
"objects" => &[
|
||||
"/handle",
|
||||
"/owner",
|
||||
"/common/handle",
|
||||
"/common/owner_handle",
|
||||
],
|
||||
"header" => &["/handle_seed"],
|
||||
"summary_info" => &[],
|
||||
"layers" | "line_types" | "text_styles" | "block_records" | "dim_styles" | "app_ids"
|
||||
| "views" | "vports" | "ucss" | "vx_table" => &["/handle", "/name"],
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
|
||||
fn path_changes_identity(collection: &str, path: &str) -> bool {
|
||||
path_is_identity_field(collection, path)
|
||||
|| identity_paths(collection).iter().any(|identity| {
|
||||
path == *identity
|
||||
|| identity
|
||||
.strip_prefix(path)
|
||||
.is_some_and(|suffix| suffix.starts_with('/'))
|
||||
})
|
||||
}
|
||||
|
||||
fn path_is_identity_field(collection: &str, path: &str) -> bool {
|
||||
match collection {
|
||||
"entities" => path.ends_with("/common/handle") || path.ends_with("/common/owner_handle"),
|
||||
"objects" => {
|
||||
matches!(path, "/handle" | "/owner")
|
||||
|| path.ends_with("/common/handle")
|
||||
|| path.ends_with("/common/owner_handle")
|
||||
}
|
||||
_ => identity_paths(collection).contains(&path),
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_identity_values(value: &Value, collection: &str, path: &str, output: &mut Vec<Value>) {
|
||||
if path_is_identity_field(collection, path) {
|
||||
output.push(json!({"path":path,"value":value}));
|
||||
}
|
||||
match value {
|
||||
Value::Object(object) => {
|
||||
for (name, value) in object {
|
||||
collect_identity_values(
|
||||
value,
|
||||
collection,
|
||||
&format!("{path}/{}", json_pointer_name(name)),
|
||||
output,
|
||||
);
|
||||
}
|
||||
}
|
||||
Value::Array(values) => {
|
||||
for (index, value) in values.iter().enumerate() {
|
||||
collect_identity_values(value, collection, &format!("{path}/{index}"), output);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn registry_types() -> &'static Map<String, Value> {
|
||||
static REGISTRY: OnceLock<Value> = OnceLock::new();
|
||||
REGISTRY.get_or_init(|| {
|
||||
serde_json::from_str(ocs_plugin_api::get_embedded_type_registry_json())
|
||||
.expect("embedded type registry")
|
||||
})["types"]
|
||||
.as_object()
|
||||
.expect("type registry object")
|
||||
}
|
||||
|
||||
fn failure(code: &str, message: impl Into<String>) -> Value {
|
||||
json!({"ok":false,"status":"failed","code":code,"error":message.into()})
|
||||
}
|
||||
@@ -504,6 +635,467 @@ fn project_paths(mut record: Value, paths: Option<&Vec<Value>>) -> Result<Value,
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
fn resolved_type_name(requested: &str) -> Option<String> {
|
||||
let types = registry_types();
|
||||
types
|
||||
.get_key_value(requested)
|
||||
.or_else(|| {
|
||||
types
|
||||
.iter()
|
||||
.find(|(name, _)| name.eq_ignore_ascii_case(requested))
|
||||
})
|
||||
.map(|(name, _)| name.clone())
|
||||
}
|
||||
|
||||
fn collect_type_references(value: &Value, pending: &mut VecDeque<String>) {
|
||||
match value {
|
||||
Value::Object(object) => {
|
||||
for (key, value) in object {
|
||||
if key == "type_id" {
|
||||
if let Some(type_id) = value.as_str() {
|
||||
for token in type_id.split(|character: char| {
|
||||
!character.is_ascii_alphanumeric() && character != '_'
|
||||
}) {
|
||||
if registry_types().contains_key(token) {
|
||||
pending.push_back(token.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
collect_type_references(value, pending);
|
||||
}
|
||||
}
|
||||
Value::Array(values) => {
|
||||
for value in values {
|
||||
collect_type_references(value, pending);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn referenced_definitions(root: &str) -> Map<String, Value> {
|
||||
let mut definitions = Map::new();
|
||||
let mut pending = VecDeque::from([root.to_string()]);
|
||||
while let Some(name) = pending.pop_front() {
|
||||
if definitions.contains_key(&name) {
|
||||
continue;
|
||||
}
|
||||
let Some(info) = registry_types().get(&name) else {
|
||||
continue;
|
||||
};
|
||||
collect_type_references(info, &mut pending);
|
||||
definitions.insert(name, info.clone());
|
||||
}
|
||||
definitions
|
||||
}
|
||||
|
||||
fn json_pointer_name(name: &str) -> String {
|
||||
name.replace('~', "~0").replace('/', "~1")
|
||||
}
|
||||
|
||||
fn field_unit(name: &str, type_id: &str) -> Option<&'static str> {
|
||||
let numeric = matches!(
|
||||
type_id,
|
||||
"f32"
|
||||
| "f64"
|
||||
| "i8"
|
||||
| "i16"
|
||||
| "i32"
|
||||
| "i64"
|
||||
| "u8"
|
||||
| "u16"
|
||||
| "u32"
|
||||
| "u64"
|
||||
| "Vector2"
|
||||
| "Vector3"
|
||||
);
|
||||
if !numeric {
|
||||
return None;
|
||||
}
|
||||
let name = name.to_ascii_lowercase();
|
||||
if name.contains("angle") || name == "rotation" || name.contains("twist") {
|
||||
return Some("radians");
|
||||
}
|
||||
if name.contains("scale") || name.contains("ratio") || name.contains("factor") {
|
||||
return Some("unitless");
|
||||
}
|
||||
if [
|
||||
"point",
|
||||
"position",
|
||||
"origin",
|
||||
"center",
|
||||
"location",
|
||||
"elevation",
|
||||
"height",
|
||||
"width",
|
||||
"length",
|
||||
"radius",
|
||||
"diameter",
|
||||
"offset",
|
||||
"thickness",
|
||||
"distance",
|
||||
"size",
|
||||
"spacing",
|
||||
]
|
||||
.iter()
|
||||
.any(|part| name.contains(part))
|
||||
{
|
||||
return Some("drawing_units");
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn type_constraints(type_id: &str) -> Option<Value> {
|
||||
let integer_bounds = match type_id {
|
||||
"i8" => Some(json!({"minimum":i8::MIN,"maximum":i8::MAX})),
|
||||
"i16" => Some(json!({"minimum":i16::MIN,"maximum":i16::MAX})),
|
||||
"i32" => Some(json!({"minimum":i32::MIN,"maximum":i32::MAX})),
|
||||
"i64" => Some(json!({"minimum":i64::MIN,"maximum":i64::MAX})),
|
||||
"u8" => Some(json!({"minimum":0,"maximum":u8::MAX})),
|
||||
"u16" => Some(json!({"minimum":0,"maximum":u16::MAX})),
|
||||
"u32" => Some(json!({"minimum":0,"maximum":u32::MAX})),
|
||||
"u64" | "Handle" => Some(json!({"minimum":0,"maximum":u64::MAX})),
|
||||
_ => None,
|
||||
};
|
||||
if integer_bounds.is_some() {
|
||||
return integer_bounds;
|
||||
}
|
||||
let info = registry_types().get(type_id)?;
|
||||
if info["kind"] != "Enum" {
|
||||
return None;
|
||||
}
|
||||
Some(json!({
|
||||
"allowed_variants":info["variants"].as_array().into_iter().flatten()
|
||||
.filter_map(|variant|variant["name"].as_str()).collect::<Vec<_>>()
|
||||
}))
|
||||
}
|
||||
|
||||
fn append_schema_fields(
|
||||
owner_type: &str,
|
||||
type_fields: &[Value],
|
||||
prefix: &str,
|
||||
collection: Option<&str>,
|
||||
depth: usize,
|
||||
stack: &mut Vec<String>,
|
||||
fields: &mut Vec<Value>,
|
||||
) {
|
||||
for field in type_fields {
|
||||
let Some(name) = field["name"].as_str() else {
|
||||
continue;
|
||||
};
|
||||
let type_id = field["type_id"].as_str().unwrap_or("Value");
|
||||
let path = format!("{prefix}/{}", json_pointer_name(name));
|
||||
let sequence = field["is_sequence"].as_bool().unwrap_or(false);
|
||||
let writable = collection.is_some_and(collection_is_mutable)
|
||||
&& collection.is_none_or(|collection| !path_changes_identity(collection, &path));
|
||||
let mut description = json!({
|
||||
"path":path,
|
||||
"name":name,
|
||||
"description":format!("{} property on {owner_type}", name.replace('_', " ")),
|
||||
"type":type_id,
|
||||
"optional":field["optional"].as_bool().unwrap_or(false),
|
||||
"sequence":sequence,
|
||||
"writable":writable,
|
||||
});
|
||||
let object = description.as_object_mut().expect("field description");
|
||||
if sequence {
|
||||
object.insert(
|
||||
"item_path_template".into(),
|
||||
json!(format!("{path}/{{index}}")),
|
||||
);
|
||||
}
|
||||
if let Some(unit) = field_unit(name, type_id) {
|
||||
object.insert("unit".into(), json!(unit));
|
||||
}
|
||||
if let Some(constraints) = type_constraints(type_id) {
|
||||
object.insert("constraints".into(), constraints);
|
||||
}
|
||||
fields.push(description);
|
||||
|
||||
let nested_prefix = if sequence {
|
||||
format!("{path}/{{index}}")
|
||||
} else {
|
||||
path
|
||||
};
|
||||
if registry_types().contains_key(type_id) {
|
||||
append_field_descriptions(
|
||||
type_id,
|
||||
&nested_prefix,
|
||||
collection,
|
||||
depth + 1,
|
||||
stack,
|
||||
fields,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn append_field_descriptions(
|
||||
type_name: &str,
|
||||
prefix: &str,
|
||||
collection: Option<&str>,
|
||||
depth: usize,
|
||||
stack: &mut Vec<String>,
|
||||
fields: &mut Vec<Value>,
|
||||
) {
|
||||
if depth > 12 || stack.iter().any(|name| name == type_name) {
|
||||
return;
|
||||
}
|
||||
let Some(info) = registry_types().get(type_name) else {
|
||||
return;
|
||||
};
|
||||
stack.push(type_name.to_string());
|
||||
match info["kind"].as_str() {
|
||||
Some("Struct") => append_schema_fields(
|
||||
type_name,
|
||||
info["fields"].as_array().map(Vec::as_slice).unwrap_or(&[]),
|
||||
prefix,
|
||||
collection,
|
||||
depth,
|
||||
stack,
|
||||
fields,
|
||||
),
|
||||
Some("Enum") => append_enum_descriptions(
|
||||
type_name,
|
||||
info["variants"]
|
||||
.as_array()
|
||||
.map(Vec::as_slice)
|
||||
.unwrap_or(&[]),
|
||||
prefix,
|
||||
collection,
|
||||
depth,
|
||||
stack,
|
||||
fields,
|
||||
),
|
||||
_ => {}
|
||||
}
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
fn append_enum_descriptions(
|
||||
enum_type: &str,
|
||||
variants: &[Value],
|
||||
prefix: &str,
|
||||
collection: Option<&str>,
|
||||
depth: usize,
|
||||
stack: &mut Vec<String>,
|
||||
fields: &mut Vec<Value>,
|
||||
) {
|
||||
for variant in variants {
|
||||
let Some(variant_name) = variant["name"].as_str() else {
|
||||
continue;
|
||||
};
|
||||
let Some(variant_fields) = variant["fields"]
|
||||
.as_array()
|
||||
.filter(|fields| !fields.is_empty())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let path = format!("{prefix}/{}", json_pointer_name(variant_name));
|
||||
let newtype = (variant_fields.len() == 1 && variant_fields[0]["name"] == "0")
|
||||
.then(|| &variant_fields[0]);
|
||||
let type_id = newtype
|
||||
.and_then(|field| field["type_id"].as_str())
|
||||
.unwrap_or("struct_variant");
|
||||
let sequence = newtype
|
||||
.and_then(|field| field["is_sequence"].as_bool())
|
||||
.unwrap_or(false);
|
||||
let writable = collection.is_some_and(collection_is_mutable)
|
||||
&& collection.is_none_or(|collection| !path_changes_identity(collection, &path));
|
||||
let mut description = json!({
|
||||
"path":path,
|
||||
"name":variant_name,
|
||||
"description":format!("{variant_name} variant on {enum_type}"),
|
||||
"type":type_id,
|
||||
"enum":enum_type,
|
||||
"variant":variant_name,
|
||||
"optional":false,
|
||||
"sequence":sequence,
|
||||
"writable":writable,
|
||||
});
|
||||
if sequence {
|
||||
description
|
||||
.as_object_mut()
|
||||
.expect("variant description")
|
||||
.insert(
|
||||
"item_path_template".into(),
|
||||
json!(format!("{path}/{{index}}")),
|
||||
);
|
||||
}
|
||||
if let Some(unit) = field_unit(variant_name, type_id) {
|
||||
description
|
||||
.as_object_mut()
|
||||
.expect("variant description")
|
||||
.insert("unit".into(), json!(unit));
|
||||
}
|
||||
if let Some(constraints) = type_constraints(type_id) {
|
||||
description
|
||||
.as_object_mut()
|
||||
.expect("variant description")
|
||||
.insert("constraints".into(), constraints);
|
||||
}
|
||||
fields.push(description);
|
||||
|
||||
if newtype.is_some() {
|
||||
if registry_types().contains_key(type_id) {
|
||||
let nested_prefix = if sequence {
|
||||
format!("{path}/{{index}}")
|
||||
} else {
|
||||
path
|
||||
};
|
||||
append_field_descriptions(
|
||||
type_id,
|
||||
&nested_prefix,
|
||||
collection,
|
||||
depth + 1,
|
||||
stack,
|
||||
fields,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
append_schema_fields(
|
||||
variant_name,
|
||||
variant_fields,
|
||||
&path,
|
||||
collection,
|
||||
depth + 1,
|
||||
stack,
|
||||
fields,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn append_variant_field_descriptions(
|
||||
enum_type: &str,
|
||||
variant_name: &str,
|
||||
collection: Option<&str>,
|
||||
fields: &mut Vec<Value>,
|
||||
) {
|
||||
let Some(variant_fields) = registry_types()
|
||||
.get(enum_type)
|
||||
.and_then(|info| info["variants"].as_array())
|
||||
.and_then(|variants| {
|
||||
variants.iter().find(|variant| {
|
||||
variant["name"]
|
||||
.as_str()
|
||||
.is_some_and(|name| name.eq_ignore_ascii_case(variant_name))
|
||||
})
|
||||
})
|
||||
.and_then(|variant| variant["fields"].as_array())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let mut stack = vec![enum_type.to_string()];
|
||||
append_schema_fields(
|
||||
variant_name,
|
||||
variant_fields,
|
||||
"",
|
||||
collection,
|
||||
0,
|
||||
&mut stack,
|
||||
fields,
|
||||
);
|
||||
}
|
||||
|
||||
fn runtime_value_schema(value: &Value, collection: &str, path: &str) -> Value {
|
||||
let mut schema = match value {
|
||||
Value::Null => json!({"type":"null"}),
|
||||
Value::Bool(value) => json!({"type":"boolean","example":value}),
|
||||
Value::Number(value) if value.is_i64() || value.is_u64() => {
|
||||
json!({"type":"integer","example":value})
|
||||
}
|
||||
Value::Number(value) => json!({"type":"number","example":value}),
|
||||
Value::String(value) => {
|
||||
let example: String = value.chars().take(160).collect();
|
||||
json!({"type":"string","example":example})
|
||||
}
|
||||
Value::Array(values) => {
|
||||
let item = values
|
||||
.first()
|
||||
.map(|value| runtime_value_schema(value, collection, &format!("{path}/0")))
|
||||
.unwrap_or_else(|| json!({}));
|
||||
json!({"type":"array","items":item})
|
||||
}
|
||||
Value::Object(values) => {
|
||||
let properties = values
|
||||
.iter()
|
||||
.map(|(name, value)| {
|
||||
let child_path = format!("{path}/{}", json_pointer_name(name));
|
||||
(
|
||||
name.clone(),
|
||||
runtime_value_schema(value, collection, &child_path),
|
||||
)
|
||||
})
|
||||
.collect::<Map<_, _>>();
|
||||
json!({
|
||||
"type":"object",
|
||||
"properties":properties,
|
||||
"required":values.keys().collect::<Vec<_>>(),
|
||||
"additionalProperties":false,
|
||||
})
|
||||
}
|
||||
};
|
||||
if !path.is_empty() {
|
||||
let object = schema.as_object_mut().expect("runtime schema");
|
||||
object.insert(
|
||||
"readOnly".into(),
|
||||
json!(!collection_is_mutable(collection) || path_changes_identity(collection, path)),
|
||||
);
|
||||
let name = path.rsplit('/').next().unwrap_or("");
|
||||
let scalar_type = if value.is_number() { "f64" } else { "Value" };
|
||||
if let Some(unit) = field_unit(name, scalar_type) {
|
||||
object.insert("x-unit".into(), json!(unit));
|
||||
}
|
||||
}
|
||||
schema
|
||||
}
|
||||
|
||||
fn editing_description(collection: Option<&str>) -> Value {
|
||||
let mutable = collection.is_some_and(collection_is_mutable);
|
||||
json!({
|
||||
"read_operation":"records",
|
||||
"write_operation":mutable.then_some("set_properties"),
|
||||
"selector":match collection {
|
||||
Some("entities" | "objects") => "handle",
|
||||
Some("header" | "summary_info") => "singleton",
|
||||
Some(_) if mutable => "handle or name",
|
||||
Some(_) => "read-only derived record",
|
||||
None => "collection plus handle or name",
|
||||
},
|
||||
"path":"RFC 6901 JSON Pointer relative to record.properties",
|
||||
"sequence_path":"Replace {index} in item_path_template with a current zero-based array index",
|
||||
"identity_paths":collection.map(identity_paths).unwrap_or(&[]),
|
||||
"atomic":true,
|
||||
"compare_and_set":"Add expected to an update to reject stale field values",
|
||||
"validation":["document revision","request id","JSON type","enum variant","integer bounds","record identity","layer lock"],
|
||||
})
|
||||
}
|
||||
|
||||
fn collection_record_types(collection: &str) -> Vec<Value> {
|
||||
let root = collection_root_type(collection).unwrap_or("Value");
|
||||
if matches!(collection, "entities" | "objects") {
|
||||
return registry_types()[root]["variants"]
|
||||
.as_array()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|variant| {
|
||||
let fields = variant["fields"].as_array();
|
||||
let schema_type = fields
|
||||
.filter(|fields| fields.len() == 1 && fields[0]["name"] == "0")
|
||||
.and_then(|fields| fields[0]["type_id"].as_str())
|
||||
.unwrap_or(root);
|
||||
json!({"type":variant["name"],"schema_type":schema_type})
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
vec![json!({
|
||||
"type":collection_record_name(collection).unwrap_or(root),
|
||||
"schema_type":root,
|
||||
})]
|
||||
}
|
||||
|
||||
fn requested_handle(request: &Value) -> Result<acadrust::Handle, String> {
|
||||
request["handle"]
|
||||
.as_str()
|
||||
@@ -523,6 +1115,8 @@ fn apply_updates(
|
||||
if updates.is_empty() {
|
||||
return Err("set_properties requires a non-empty updates array".into());
|
||||
}
|
||||
let mut original_identity = Vec::new();
|
||||
collect_identity_values(properties, collection, "", &mut original_identity);
|
||||
let mut paths = Vec::with_capacity(updates.len());
|
||||
for update in updates {
|
||||
let path = update["path"]
|
||||
@@ -531,14 +1125,7 @@ fn apply_updates(
|
||||
if path.is_empty() || !path.starts_with('/') {
|
||||
return Err("updates require a non-empty JSON Pointer path".into());
|
||||
}
|
||||
let identity = match collection {
|
||||
"entities" => matches!(path, "/common/handle" | "/common/owner_handle"),
|
||||
"objects" => matches!(path, "/handle" | "/common/handle"),
|
||||
"header" => path == "/handle_seed",
|
||||
"summary_info" => false,
|
||||
_ => matches!(path, "/handle" | "/name"),
|
||||
};
|
||||
if identity {
|
||||
if path_changes_identity(collection, path) {
|
||||
return Err(format!("{path} is read-only identity data"));
|
||||
}
|
||||
let value = update
|
||||
@@ -556,6 +1143,11 @@ fn apply_updates(
|
||||
*target = value;
|
||||
paths.push(path.to_string());
|
||||
}
|
||||
let mut edited_identity = Vec::new();
|
||||
collect_identity_values(properties, collection, "", &mut edited_identity);
|
||||
if edited_identity != original_identity {
|
||||
return Err("record identity is read-only".into());
|
||||
}
|
||||
Ok(paths)
|
||||
}
|
||||
|
||||
@@ -611,11 +1203,13 @@ impl OpenCADStudio {
|
||||
"records":{
|
||||
"read":"records",
|
||||
"write":"set_properties",
|
||||
"describe":"record_schema",
|
||||
"path":"RFC 6901 JSON Pointer relative to properties",
|
||||
"filters":["eq","ne","lt","lte","gt","gte","contains","starts_with","ends_with","in","exists","not_exists"],
|
||||
"collections":COLLECTIONS.iter().map(|(name, mutable)|json!({
|
||||
"name":name,
|
||||
"mutable":mutable,
|
||||
"root_type":collection_root_type(name),
|
||||
"count":collection_count(document, name),
|
||||
})).collect::<Vec<_>>()
|
||||
},
|
||||
@@ -623,6 +1217,151 @@ impl OpenCADStudio {
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn record_schema(&self, request: &Value) -> Value {
|
||||
let collection = request["collection"].as_str();
|
||||
if collection.is_some_and(|name| collection_root_type(name).is_none()) {
|
||||
return failure(
|
||||
"unknown_collection",
|
||||
format!("unknown record collection: {}", collection.unwrap()),
|
||||
);
|
||||
}
|
||||
let collections = COLLECTIONS
|
||||
.iter()
|
||||
.map(|(name, mutable)| {
|
||||
json!({
|
||||
"name":name,
|
||||
"mutable":mutable,
|
||||
"root_type":collection_root_type(name),
|
||||
"identity_paths":identity_paths(name),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let Some(requested_type) = request["type"].as_str() else {
|
||||
let mut record_types = if let Some(collection) = collection {
|
||||
collection_record_types(collection)
|
||||
} else {
|
||||
registry_types()
|
||||
.iter()
|
||||
.map(|(name, info)| json!({"type":name,"schema_type":name,"kind":info["kind"]}))
|
||||
.collect()
|
||||
};
|
||||
if let Some(search) = request["search"].as_str().filter(|value| !value.is_empty()) {
|
||||
let search = search.to_ascii_lowercase();
|
||||
record_types.retain(|entry| {
|
||||
entry["type"]
|
||||
.as_str()
|
||||
.is_some_and(|name| name.to_ascii_lowercase().contains(&search))
|
||||
});
|
||||
}
|
||||
let count = record_types.len();
|
||||
let offset = request["offset"].as_u64().unwrap_or(0) as usize;
|
||||
let limit = request["limit"].as_u64().unwrap_or(1000).min(10_000) as usize;
|
||||
let record_types = record_types
|
||||
.into_iter()
|
||||
.skip(offset)
|
||||
.take(limit)
|
||||
.collect::<Vec<_>>();
|
||||
return json!({
|
||||
"ok":true,
|
||||
"schema_version":1,
|
||||
"collection":collection,
|
||||
"root_type":collection.and_then(collection_root_type),
|
||||
"collections":collections,
|
||||
"count":count,
|
||||
"returned":record_types.len(),
|
||||
"next_offset":(offset + record_types.len() < count).then_some(offset + record_types.len()),
|
||||
"record_types":record_types,
|
||||
"editing":editing_description(collection),
|
||||
"next":"Call record_schema again with type set to a record type or schema_type returned here."
|
||||
});
|
||||
};
|
||||
|
||||
let root_type = collection
|
||||
.and_then(|collection| {
|
||||
collection_record_types(collection)
|
||||
.into_iter()
|
||||
.find(|entry| {
|
||||
["type", "schema_type"].iter().any(|key| {
|
||||
entry[*key]
|
||||
.as_str()
|
||||
.is_some_and(|name| name.eq_ignore_ascii_case(requested_type))
|
||||
})
|
||||
})
|
||||
.and_then(|entry| entry["schema_type"].as_str().map(str::to_string))
|
||||
})
|
||||
.or_else(|| resolved_type_name(requested_type))
|
||||
.or_else(|| {
|
||||
(requested_type == "Value"
|
||||
|| collection.and_then(collection_root_type) == Some("Value"))
|
||||
.then(|| "Value".to_string())
|
||||
});
|
||||
let Some(root_type) = root_type else {
|
||||
return failure(
|
||||
"unknown_record_type",
|
||||
format!("unknown record type: {requested_type}"),
|
||||
);
|
||||
};
|
||||
let mut fields = Vec::new();
|
||||
let direct_collection_variant =
|
||||
collection
|
||||
.and_then(collection_root_type)
|
||||
.is_some_and(|collection_root| {
|
||||
collection_root == root_type && !requested_type.eq_ignore_ascii_case(&root_type)
|
||||
})
|
||||
&& registry_types()
|
||||
.get(&root_type)
|
||||
.is_some_and(|info| info["kind"] == "Enum");
|
||||
if direct_collection_variant {
|
||||
append_variant_field_descriptions(&root_type, requested_type, collection, &mut fields);
|
||||
} else {
|
||||
append_field_descriptions(&root_type, "", collection, 0, &mut Vec::new(), &mut fields);
|
||||
}
|
||||
let definitions = referenced_definitions(&root_type);
|
||||
let runtime_schema = collection.and_then(|collection| {
|
||||
collection_records(&self.tabs[self.active_tab].scene.document, collection)
|
||||
.ok()?
|
||||
.into_iter()
|
||||
.find(|record| {
|
||||
let type_matches = record["type"]
|
||||
.as_str()
|
||||
.is_some_and(|name| name.eq_ignore_ascii_case(requested_type))
|
||||
|| collection_root_type(collection)
|
||||
.is_some_and(|name| name.eq_ignore_ascii_case(requested_type));
|
||||
type_matches
|
||||
&& request["handle"].as_str().is_none_or(|handle| {
|
||||
record["handle"].as_str().is_some_and(|actual| {
|
||||
actual.eq_ignore_ascii_case(without_hex_prefix(handle))
|
||||
})
|
||||
})
|
||||
&& request["name"].as_str().is_none_or(|name| {
|
||||
record["name"]
|
||||
.as_str()
|
||||
.is_some_and(|actual| actual.eq_ignore_ascii_case(name))
|
||||
})
|
||||
})
|
||||
.map(|record| runtime_value_schema(&record["properties"], collection, ""))
|
||||
});
|
||||
json!({
|
||||
"ok":true,
|
||||
"schema_version":1,
|
||||
"collection":collection,
|
||||
"record_type":requested_type,
|
||||
"schema_type":root_type,
|
||||
"schema":registry_types().get(&root_type),
|
||||
"definitions":definitions,
|
||||
"fields":fields,
|
||||
"runtime_schema":runtime_schema,
|
||||
"editing":editing_description(collection),
|
||||
"notes":[
|
||||
"Enum constraints list every accepted serialized variant.",
|
||||
"Integer constraints reflect the complete serialized range.",
|
||||
"Unit annotations are supplied only where the serialized field name and type are unambiguous.",
|
||||
"runtime_schema describes an existing matching record when one is available."
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn record_query(&self, request: &Value) -> Value {
|
||||
let document = &self.tabs[self.active_tab].scene.document;
|
||||
let Some(collection) = request["collection"].as_str() else {
|
||||
@@ -990,6 +1729,104 @@ mod tests {
|
||||
assert!(app.tabs[i].scene.document.summary_info.author.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_schema_describes_absent_types_fields_enums_units_and_write_rules() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
app.automation_op(r#"{"op":"new"}"#);
|
||||
|
||||
let catalog = app.record_schema(&json!({"collection":"entities"}));
|
||||
assert_eq!(catalog["ok"], true, "{catalog}");
|
||||
assert_eq!(catalog["root_type"], "EntityType");
|
||||
assert_eq!(catalog["count"], 48);
|
||||
assert!(catalog["record_types"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|record| record["type"] == "Insert" && record["schema_type"] == "Insert"));
|
||||
|
||||
let schema = app.record_schema(&json!({
|
||||
"collection":"entities",
|
||||
"type":"Insert"
|
||||
}));
|
||||
assert_eq!(schema["ok"], true, "{schema}");
|
||||
assert!(schema["definitions"].get("Insert").is_some());
|
||||
assert!(schema["definitions"].get("AttributeEntity").is_some());
|
||||
assert!(schema["runtime_schema"].is_null());
|
||||
let fields = schema["fields"].as_array().unwrap();
|
||||
let field = |path: &str| {
|
||||
fields
|
||||
.iter()
|
||||
.find(|field| field["path"] == path)
|
||||
.unwrap_or_else(|| panic!("missing schema field {path}"))
|
||||
};
|
||||
assert_eq!(field("/common/handle")["writable"], false);
|
||||
assert_eq!(field("/insert_point")["unit"], "drawing_units");
|
||||
assert_eq!(field("/rotation")["unit"], "radians");
|
||||
assert_eq!(
|
||||
field("/attributes")["item_path_template"],
|
||||
"/attributes/{index}"
|
||||
);
|
||||
assert_eq!(field("/attributes/{index}/value")["writable"], true);
|
||||
|
||||
let text = app.record_schema(&json!({"collection":"entities","type":"Text"}));
|
||||
let alignment = text["fields"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|field| field["path"] == "/horizontal_alignment")
|
||||
.expect("horizontal alignment schema");
|
||||
assert!(alignment["constraints"]["allowed_variants"]
|
||||
.as_array()
|
||||
.is_some_and(|variants| variants.len() >= 3));
|
||||
|
||||
let unknown = app.record_schema(&json!({"collection":"objects","type":"Unknown"}));
|
||||
assert_eq!(unknown["schema_type"], "ObjectType");
|
||||
let fields = unknown["fields"].as_array().unwrap();
|
||||
let field = |path: &str| {
|
||||
fields
|
||||
.iter()
|
||||
.find(|field| field["path"] == path)
|
||||
.unwrap_or_else(|| panic!("missing enum variant field {path}"))
|
||||
};
|
||||
assert_eq!(field("/type_name")["writable"], true);
|
||||
assert_eq!(field("/handle")["writable"], false);
|
||||
assert_eq!(field("/owner")["writable"], false);
|
||||
assert_eq!(field("/raw_dwg_handle_bits")["writable"], true);
|
||||
|
||||
let dimension = app.record_schema(&json!({"collection":"entities","type":"Dimension"}));
|
||||
let fields = dimension["fields"].as_array().unwrap();
|
||||
assert!(fields.iter().any(|field| field["path"] == "/Linear"));
|
||||
assert!(fields.iter().any(
|
||||
|field| field["path"] == "/Linear/base/common/handle" && field["writable"] == false
|
||||
));
|
||||
assert!(fields.iter().any(|field| field["path"] == "/Radius"));
|
||||
|
||||
let section_style = app
|
||||
.record_schema(&json!({"collection":"section_view_style","type":"SectionViewStyle"}));
|
||||
assert_eq!(section_style["schema_type"], "EntitySectionViewStyle");
|
||||
assert!(section_style["fields"]
|
||||
.as_array()
|
||||
.is_some_and(|fields| fields
|
||||
.iter()
|
||||
.any(|field| field["path"] == "/arrow_size" && field["unit"] == "drawing_units")));
|
||||
|
||||
let class = app.record_schema(&json!({"collection":"classes","type":"Class"}));
|
||||
assert_eq!(class["schema_type"], "DxfClass");
|
||||
assert!(class["fields"]
|
||||
.as_array()
|
||||
.is_some_and(|fields| fields.iter().any(|field| field["path"] == "/dxf_name")));
|
||||
|
||||
for (collection, root) in [("entities", "EntityType"), ("objects", "ObjectType")] {
|
||||
let complete = app.record_schema(&json!({"collection":collection,"type":root}));
|
||||
assert_eq!(complete["ok"], true, "{root}: {complete}");
|
||||
assert!(complete["fields"]
|
||||
.as_array()
|
||||
.is_some_and(|fields| !fields.is_empty()));
|
||||
let bytes = serde_json::to_vec(&complete).unwrap().len();
|
||||
assert!(bytes < 8 * 1024 * 1024, "{root} schema is {bytes} bytes");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_updates_reject_type_identity_and_compare_failures() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
@@ -1016,6 +1853,17 @@ mod tests {
|
||||
);
|
||||
assert_eq!(identity["code"], "invalid_update", "{identity}");
|
||||
|
||||
let identity_parent = execute(
|
||||
&mut app,
|
||||
json!({"op":"set_properties","collection":"entities","handle":handle,
|
||||
"updates":[{"path":"/common","value":{}}]}),
|
||||
"identity-parent",
|
||||
);
|
||||
assert_eq!(
|
||||
identity_parent["code"], "invalid_update",
|
||||
"{identity_parent}"
|
||||
);
|
||||
|
||||
let compare = execute(
|
||||
&mut app,
|
||||
json!({"op":"set_properties","collection":"entities","handle":handle,
|
||||
|
||||
+5
-3
@@ -22,12 +22,13 @@ const MODERN_PROTOCOL_VERSION: &str = "2026-07-28";
|
||||
const MAX_REQUEST: usize = 1_048_576;
|
||||
const MAX_RESPONSE: u64 = 16 * 1024 * 1024;
|
||||
const CACHE_TTL_MS: u64 = 3_600_000;
|
||||
const INSTRUCTIONS: &str = "Call ocs_sessions, then pass its session_id as ocs_session_id to ocs_read, ocs_execute and ocs_capture. Read capabilities to discover the complete CAD automation surface. Use records to inspect every serializable entity, object, table, header and document record; filter with RFC 6901 JSON Pointer paths. Use set_properties for atomic, type-checked record edits and preserve document_id, revision and request_id. Use commands with parameters.name for a command manifest. Use batch when several steps are known, and request changed_entities when resulting geometry is needed. For interactive work, call start and follow state.command.accepts, options and input_example. A run.cmd contains the command name followed by prompt answers separated by spaces; points use x,y or x,y,z. After a timeout, query the existing operation and never replay a mutation with a new request_id. waiting_input and running are not completion. Let OCS and its geometry kernel calculate geometry; use query near, contains_point and intersections for exact relationships. Verify important results with queries and a viewport capture, and save only to an explicit path.";
|
||||
const INSTRUCTIONS: &str = "Call ocs_sessions, then pass its session_id as ocs_session_id to ocs_read, ocs_execute and ocs_capture. Read capabilities to discover the complete CAD automation surface. Call record_schema to discover every record type, property path, JSON type, enum, unit, constraint and write rule before editing unfamiliar data. Use records to inspect every serializable entity, object, table, header and document record; filter with RFC 6901 JSON Pointer paths. Use set_properties for atomic, type-checked record edits and preserve document_id, revision and request_id. Use commands with parameters.name for a command manifest. Use batch when several steps are known, and request changed_entities when resulting geometry is needed. For interactive work, call start and follow state.command.accepts, options and input_example. A run.cmd contains the command name followed by prompt answers separated by spaces; points use x,y or x,y,z. After a timeout, query the existing operation and never replay a mutation with a new request_id. waiting_input and running are not completion. Let OCS and its geometry kernel calculate geometry; use query near, contains_point and intersections for exact relationships. Verify important results with queries and a viewport capture, and save only to an explicit path.";
|
||||
const READ_OPS: &[&str] = &[
|
||||
"state",
|
||||
"hello",
|
||||
"query",
|
||||
"records",
|
||||
"record_schema",
|
||||
"capabilities",
|
||||
"entities",
|
||||
"layers",
|
||||
@@ -986,8 +987,8 @@ fn tool_definitions() -> Value {
|
||||
},
|
||||
{
|
||||
"name":"ocs_read",
|
||||
"description":"Discover capabilities or read state, complete database records, command manifests, entities, properties, kernel measurements and spatial relationships, history, events or operation status from a live OCS session.",
|
||||
"inputSchema":{"type":"object","properties":{"ocs_session_id":{"type":"string","minLength":1,"description":"Value of session_id returned by ocs_sessions."},"op":{"type":"string","enum":READ_OPS,"default":"state"},"parameters":{"type":"object","description":"Operation-specific filters.","properties":{"name":{"type":"string","description":"Command name or record name."},"search":{"type":"string","description":"Case-insensitive command-name search."},"document_id":{"type":"integer","minimum":0},"collection":{"type":"string","description":"Record collection, all, or omit to discover collections."},"handle":{"type":"string"},"handles":{"type":"array","items":{"type":"string"},"description":"Exact entity or record handles."},"type":{"type":"string","description":"Entity or record type filter."},"layer":{"type":"string","description":"Layer name filter for query."},"detail":{"type":"string","enum":["summary","geometry","full"],"default":"geometry","description":"Entity detail returned by query."},"fields":{"type":"array","items":{"type":"string"},"description":"Return only these entity fields plus handle."},"paths":{"type":"array","items":{"type":"string"},"description":"Project RFC 6901 JSON Pointer paths relative to record.properties."},"where":{"type":"array","description":"All property filters must match.","items":{"type":"object","properties":{"path":{"type":"string"},"op":{"type":"string","enum":["eq","ne","lt","lte","gt","gte","contains","starts_with","ends_with","in","exists","not_exists"],"default":"eq"},"value":{}},"required":["path"],"additionalProperties":false}},"near":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"Rank planar curves by exact kernel distance to this world XY point."},"contains_point":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"Return closed planar curves containing this world XY point."},"bounds":{"type":"array","items":{"type":"number"},"minItems":4,"maxItems":4,"description":"Filter entities whose world XY bounds overlap [min_x,min_y,max_x,max_y]."},"intersections":{"type":"array","items":{"type":"string"},"minItems":2,"maxItems":2,"description":"Return exact kernel intersections between two planar curve handles."},"after":{"type":"integer","minimum":0,"description":"Event cursor."},"request_id":{"type":"string","description":"Operation id to query."},"offset":{"type":"integer","minimum":0},"limit":{"type":"integer","minimum":1,"maximum":10000}},"additionalProperties":false}},"required":["ocs_session_id"],"additionalProperties":false},
|
||||
"description":"Discover capabilities and record schemas, or read state, complete database records, command manifests, entities, properties, kernel measurements and spatial relationships, history, events or operation status from a live OCS session.",
|
||||
"inputSchema":{"type":"object","properties":{"ocs_session_id":{"type":"string","minLength":1,"description":"Value of session_id returned by ocs_sessions."},"op":{"type":"string","enum":READ_OPS,"default":"state"},"parameters":{"type":"object","description":"Operation-specific filters.","properties":{"name":{"type":"string","description":"Command name or record name."},"search":{"type":"string","description":"Case-insensitive command or record-type search."},"document_id":{"type":"integer","minimum":0},"collection":{"type":"string","description":"Record collection, all for records, or omit to discover collections and schema types."},"handle":{"type":"string"},"handles":{"type":"array","items":{"type":"string"},"description":"Exact entity or record handles."},"type":{"type":"string","description":"Entity or record type filter; for record_schema, returns its complete type graph and writable field paths."},"layer":{"type":"string","description":"Layer name filter for query."},"detail":{"type":"string","enum":["summary","geometry","full"],"default":"geometry","description":"Entity detail returned by query."},"fields":{"type":"array","items":{"type":"string"},"description":"Return only these entity fields plus handle."},"paths":{"type":"array","items":{"type":"string"},"description":"Project RFC 6901 JSON Pointer paths relative to record.properties."},"where":{"type":"array","description":"All property filters must match.","items":{"type":"object","properties":{"path":{"type":"string"},"op":{"type":"string","enum":["eq","ne","lt","lte","gt","gte","contains","starts_with","ends_with","in","exists","not_exists"],"default":"eq"},"value":{}},"required":["path"],"additionalProperties":false}},"near":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"Rank planar curves by exact kernel distance to this world XY point."},"contains_point":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"Return closed planar curves containing this world XY point."},"bounds":{"type":"array","items":{"type":"number"},"minItems":4,"maxItems":4,"description":"Filter entities whose world XY bounds overlap [min_x,min_y,max_x,max_y]."},"intersections":{"type":"array","items":{"type":"string"},"minItems":2,"maxItems":2,"description":"Return exact kernel intersections between two planar curve handles."},"after":{"type":"integer","minimum":0,"description":"Event cursor."},"request_id":{"type":"string","description":"Operation id to query."},"offset":{"type":"integer","minimum":0},"limit":{"type":"integer","minimum":1,"maximum":10000}},"additionalProperties":false}},"required":["ocs_session_id"],"additionalProperties":false},
|
||||
"outputSchema":read_output_schema(),
|
||||
"annotations":{"title":"Read OCS state","readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false}
|
||||
},
|
||||
@@ -1356,6 +1357,7 @@ mod tests {
|
||||
);
|
||||
assert!(READ_OPS.contains(&"capabilities"));
|
||||
assert!(READ_OPS.contains(&"records"));
|
||||
assert!(READ_OPS.contains(&"record_schema"));
|
||||
assert!(EXECUTE_OPS.contains(&"set_properties"));
|
||||
assert_eq!(
|
||||
tools[1]["inputSchema"]["properties"]["parameters"]["properties"]["where"]["items"]["properties"]
|
||||
|
||||
Reference in new issue
Block a user