Skip to content

Commit 8180f0d

Browse files
committed
work in progress.
1 parent 17fd638 commit 8180f0d

3 files changed

Lines changed: 618 additions & 257 deletions

File tree

rust/otap-dataflow/crates/engine/src/capability/mod.rs

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -205,27 +205,31 @@ impl ExtensionCapabilities {
205205

206206
/// Declares which capabilities an extension provides.
207207
///
208+
/// The left side names the extension type(s); the right side is a single
209+
/// capability list that applies to both sides (no per-side divergence).
208210
/// Three forms:
209211
///
210212
/// ```rust,ignore
211-
/// // Shared-only (with automatic local fallback via SharedAsLocal)
212-
/// extension_capabilities!(shared: MyExt => [BearerTokenProvider, KeyValueStore])
213+
/// // Shared-only (local consumers served via SharedAsLocal fallback).
214+
/// extension_capabilities!(shared: MyExt => [BearerTokenProvider, KeyValueStore]);
213215
///
214-
/// // Local-only
215-
/// extension_capabilities!(local: MyLocalExt => [KeyValueStore])
216+
/// // Local-only.
217+
/// extension_capabilities!(local: MyLocalExt => [KeyValueStore]);
216218
///
217-
/// // Dual — different types for local and shared, same capability
219+
/// // Dual-typedistinct shared/local types, same capability list.
218220
/// extension_capabilities!(
219-
/// shared: MySharedKvStore => [KeyValueStore],
220-
/// local: MyLocalKvStore => [KeyValueStore],
221-
/// )
221+
/// (shared: MySharedKv, local: MyLocalKv) => [KeyValueStore]
222+
/// );
222223
/// ```
223224
///
224-
/// Each capability `$cap` must have a `#[capability]`-generated
225-
/// `shared_entry::<E>(ext_id, factory)` (and/or `local_entry::<E>(...)`)
226-
/// associated fn. The macro calls these per listed capability, passing
227-
/// a clone of the extension's instance factory, and pushes the
228-
/// resulting entry into the registry.
225+
/// Each capability `$cap` in the list must have a `#[capability]`-generated
226+
/// `shared_entry::<E>` and/or `local_entry::<E>` associated fn. The macro
227+
/// invokes them per listed capability, passing a clone of the extension's
228+
/// instance factory, and inserts the result into the registry.
229+
///
230+
/// In the dual form, `S` must implement `shared::$cap` and `L` must
231+
/// implement `local::$cap` for every capability in the list — mismatches
232+
/// surface as standard trait-bound errors at the macro call site.
229233
#[macro_export]
230234
macro_rules! extension_capabilities {
231235
// Shared-only extension (automatic local fallback via SharedAsLocal).
@@ -262,29 +266,33 @@ macro_rules! extension_capabilities {
262266
},
263267
}
264268
};
265-
// Dual extension — different types for shared and local.
266-
(shared: $sext:ty => [$($scap:ty),+ $(,)?], local: $lext:ty => [$($lcap:ty),+ $(,)?] $(,)?) => {
269+
// Dual-type extension — distinct shared/local types, same capability list.
270+
((shared: $sext:ty, local: $lext:ty) => [$($cap:ty),+ $(,)?]) => {
267271
$crate::capability::ExtensionCapabilities {
268-
shared: &[$(<$scap as $crate::capability::ExtensionCapability>::NAME),+],
269-
local: &[$(<$lcap as $crate::capability::ExtensionCapability>::NAME),+],
272+
shared: &[$(<$cap as $crate::capability::ExtensionCapability>::NAME),+],
273+
local: &[$(<$cap as $crate::capability::ExtensionCapability>::NAME),+],
270274
register_shared: |ext_id, factory, registry| {
271275
$(
272276
registry.register_shared(
273-
::std::any::TypeId::of::<$scap>(),
274-
<$scap>::shared_entry::<$sext>(ext_id.clone(), factory.clone()),
277+
::std::any::TypeId::of::<$cap>(),
278+
<$cap>::shared_entry::<$sext>(ext_id.clone(), factory.clone()),
275279
)?;
276280
)+
277281
Ok(())
278282
},
279283
register_local: |ext_id, factory, registry| {
280284
$(
281285
registry.register_local(
282-
::std::any::TypeId::of::<$lcap>(),
283-
<$lcap>::local_entry::<$lext>(ext_id.clone(), factory.clone()),
286+
::std::any::TypeId::of::<$cap>(),
287+
<$cap>::local_entry::<$lext>(ext_id.clone(), factory.clone()),
284288
)?;
285289
)+
286290
Ok(())
287291
},
288292
}
289293
};
290294
}
295+
296+
#[cfg(test)]
297+
mod tests;
298+
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! End-to-end tests for the [`extension_capabilities!`] macro.
5+
//!
6+
//! Each test defines a hand-rolled test capability (the `#[capability]`
7+
//! proc macro cannot expand inside the engine crate itself — proc
8+
//! macros can't target paths in their host crate), invokes one arm of
9+
//! the macro, exercises the resulting `register_*` fn pointers against
10+
//! a real `CapabilityRegistry`, then resolves bindings and consumes the
11+
//! capability.
12+
13+
use super::registry::{
14+
Capabilities, CapabilityRegistry, ConsumedTracker, LocalCapabilityEntry,
15+
SharedCapabilityEntry, resolve_bindings,
16+
};
17+
use super::{ExtensionCapability, KNOWN_CAPABILITIES, KnownCapability};
18+
use crate::extension::{LocalInstanceFactory, SharedInstanceFactory};
19+
use otap_df_config::{CapabilityId, ExtensionId};
20+
use std::any::{Any, TypeId};
21+
use std::collections::{HashMap, HashSet};
22+
use std::rc::Rc;
23+
24+
// ── Test capability (mirrors what `#[capability]` generates) ─────────
25+
26+
trait MacroTestCapLocal {
27+
fn value(&self) -> &str;
28+
}
29+
trait MacroTestCapShared: Send {
30+
fn value(&self) -> &str;
31+
}
32+
33+
struct MacroTestCap;
34+
impl super::private::Sealed for MacroTestCap {}
35+
impl ExtensionCapability for MacroTestCap {
36+
const NAME: &'static str = "macro_test_cap";
37+
type Local = dyn MacroTestCapLocal;
38+
type Shared = dyn MacroTestCapShared;
39+
fn wrap_shared_as_local(shared: Box<Self::Shared>) -> Rc<Self::Local> {
40+
struct Adapter(Box<dyn MacroTestCapShared>);
41+
impl MacroTestCapLocal for Adapter {
42+
fn value(&self) -> &str {
43+
self.0.value()
44+
}
45+
}
46+
Rc::new(Adapter(shared))
47+
}
48+
}
49+
50+
// Register the test capability so resolve_bindings sees it as known.
51+
#[allow(unsafe_code)]
52+
#[linkme::distributed_slice(KNOWN_CAPABILITIES)]
53+
#[linkme(crate = linkme)]
54+
static _MACRO_TEST_CAP: KnownCapability = KnownCapability {
55+
name: "macro_test_cap",
56+
description: "Test capability for extension_capabilities! macro tests",
57+
type_id: || TypeId::of::<MacroTestCap>(),
58+
};
59+
60+
// Casters mirror what `#[capability]` emits. The macro calls
61+
// `<MacroTestCap>::shared_entry::<E>` and `local_entry::<E>`.
62+
impl MacroTestCap {
63+
fn shared_entry<E>(
64+
ext_id: ExtensionId,
65+
factory: SharedInstanceFactory,
66+
) -> SharedCapabilityEntry
67+
where
68+
E: MacroTestCapShared + 'static,
69+
{
70+
let produce = move || -> Box<dyn Any + Send> {
71+
let erased = factory.produce();
72+
let concrete: Box<E> = erased.downcast().expect("instance factory");
73+
let shared: Box<dyn MacroTestCapShared> = concrete;
74+
Box::new(shared) as Box<dyn Any + Send>
75+
};
76+
let adapt_as_local: fn(Box<dyn Any + Send>) -> Rc<dyn Any> = |erased| {
77+
let shared: Box<Box<dyn MacroTestCapShared>> =
78+
erased.downcast().expect("envelope");
79+
let rc_local = <MacroTestCap as ExtensionCapability>::wrap_shared_as_local(*shared);
80+
Rc::new(rc_local) as Rc<dyn Any>
81+
};
82+
SharedCapabilityEntry::new(ext_id, produce, adapt_as_local)
83+
}
84+
85+
fn local_entry<E>(
86+
ext_id: ExtensionId,
87+
factory: LocalInstanceFactory,
88+
) -> LocalCapabilityEntry
89+
where
90+
E: MacroTestCapLocal + 'static,
91+
{
92+
let produce = move || -> Rc<dyn Any> {
93+
let erased = factory.produce();
94+
let concrete: Rc<E> = erased.downcast().expect("instance factory");
95+
let local: Rc<dyn MacroTestCapLocal> = concrete;
96+
Rc::new(local) as Rc<dyn Any>
97+
};
98+
LocalCapabilityEntry::new(ext_id, produce)
99+
}
100+
}
101+
102+
// ── Test extension impls ─────────────────────────────────────────────
103+
104+
#[derive(Clone)]
105+
struct Shared(&'static str);
106+
impl MacroTestCapShared for Shared {
107+
fn value(&self) -> &str {
108+
self.0
109+
}
110+
}
111+
112+
struct Local(&'static str);
113+
impl MacroTestCapLocal for Local {
114+
fn value(&self) -> &str {
115+
self.0
116+
}
117+
}
118+
119+
fn shared_factory(val: &'static str) -> SharedInstanceFactory {
120+
SharedInstanceFactory::new(move || Box::new(Shared(val)) as Box<dyn Any + Send>)
121+
}
122+
fn local_factory(val: &'static str) -> LocalInstanceFactory {
123+
let shared: Rc<Local> = Rc::new(Local(val));
124+
LocalInstanceFactory::new(move || Rc::clone(&shared) as Rc<dyn Any>)
125+
}
126+
127+
fn bindings() -> HashMap<CapabilityId, ExtensionId> {
128+
let mut m = HashMap::new();
129+
let _ = m.insert("macro_test_cap".into(), "ext".into());
130+
m
131+
}
132+
fn known_exts() -> HashSet<ExtensionId> {
133+
let mut s = HashSet::new();
134+
let _ = s.insert("ext".into());
135+
s
136+
}
137+
138+
fn resolve(registry: &CapabilityRegistry) -> Capabilities {
139+
let mut tracker = ConsumedTracker::new();
140+
resolve_bindings(&bindings(), registry, &known_exts(), &mut tracker)
141+
.expect("resolve_bindings")
142+
}
143+
144+
// ── Tests ────────────────────────────────────────────────────────────
145+
146+
#[test]
147+
fn macro_shared_only_form() {
148+
let ec = extension_capabilities!(shared: Shared => [MacroTestCap]);
149+
assert_eq!(ec.shared, &["macro_test_cap"]);
150+
assert!(ec.local.is_empty());
151+
152+
let mut registry = CapabilityRegistry::new();
153+
(ec.register_shared)("ext".into(), shared_factory("s-only"), &mut registry)
154+
.expect("register_shared");
155+
// register_local must be a no-op even when called with a local factory.
156+
(ec.register_local)("ext".into(), local_factory("unused"), &mut registry)
157+
.expect("register_local no-op");
158+
159+
let caps = resolve(&registry);
160+
assert_eq!(caps.require_shared::<MacroTestCap>().unwrap().value(), "s-only");
161+
// Local consumers are served by the SharedAsLocal fallback.
162+
assert_eq!(caps.require_local::<MacroTestCap>().unwrap().value(), "s-only");
163+
}
164+
165+
#[test]
166+
fn macro_local_only_form() {
167+
let ec = extension_capabilities!(local: Local => [MacroTestCap]);
168+
assert!(ec.shared.is_empty());
169+
assert_eq!(ec.local, &["macro_test_cap"]);
170+
171+
let mut registry = CapabilityRegistry::new();
172+
(ec.register_local)("ext".into(), local_factory("l-only"), &mut registry)
173+
.expect("register_local");
174+
175+
let caps = resolve(&registry);
176+
assert_eq!(caps.require_local::<MacroTestCap>().unwrap().value(), "l-only");
177+
assert!(caps.require_shared::<MacroTestCap>().is_err());
178+
}
179+
180+
#[test]
181+
fn macro_dual_form() {
182+
let ec = extension_capabilities!(
183+
(shared: Shared, local: Local) => [MacroTestCap]
184+
);
185+
assert_eq!(ec.shared, &["macro_test_cap"]);
186+
assert_eq!(ec.local, &["macro_test_cap"]);
187+
188+
let mut registry = CapabilityRegistry::new();
189+
(ec.register_shared)("ext".into(), shared_factory("s-dual"), &mut registry)
190+
.expect("register_shared");
191+
(ec.register_local)("ext".into(), local_factory("l-dual"), &mut registry)
192+
.expect("register_local");
193+
194+
let caps = resolve(&registry);
195+
// Each side returns its own type's value — not a SharedAsLocal
196+
// adapter over the shared instance.
197+
assert_eq!(caps.require_shared::<MacroTestCap>().unwrap().value(), "s-dual");
198+
assert_eq!(caps.require_local::<MacroTestCap>().unwrap().value(), "l-dual");
199+
}

0 commit comments

Comments
 (0)