30 KiB
Rust API Model
Status
WIP HostをRustで実装する際、Interfaceの宣言とObjectの実装をどう組み合わせて公開するか。このdraftは、そのための概念的なAPI modelを提案する。
Trait名、attribute、derive、generated type名、method signatureは説明用であり、具体的なRust API syntaxは確定していない。Ownership、lifetime、async runtime、error type、macro crate構成も未決定である。
Protocol上の確定事項はCore Model、Object tree / Schema、Operation and Interface、Protocolに従う。動的なObject projectionの運用要件はDynamic Worldspace Projection、対応するTypeScript modelはTypeScript API Modelを参照する。
目的
Rust APIでは、Interfaceをtraitで宣言し、structで実装するmodelを自然に表現できるようにする。ただし、structが実装しているInterfaceと、ある時点で公開するInterfaceは分けて選べるようにする。
- Rust traitでWIP Interfaceを宣言する。
- Rust structが、Protocol Objectとして公開するmetadataを提供する。
- 同じstructが、必要に応じて公開する0個以上のInterface traitを実装する。
- Objectを返す時点で、公開するInterfaceのordered setを明示的に選択する。
- Trait declarationからInterface Descriptor、Value codec、validation、Operation dispatchに必要なcodeを生成する。
- Struct instanceをWorldspace全体について常駐させず、
observeとcall_operationの解決時にdata-drivenに生成できる。
Protocol上でObjectとInterfaceを関連付けるのはObject.interfacesだけである。Operation implementationを呼び出すためのRust内部codeはSDK / Host Runtimeの実装詳細として扱い、新しいProtocol conceptは導入しない。
Conceptual model
Interface trait
WIP Interfaceは、Rust traitとして宣言する。
#[wip::interface]
trait FileInterface {
/// Reads bytes from the file.
async fn read(
&self,
/// Byte offset from the beginning of the file.
offset: Option<i64>,
/// Maximum number of bytes to return.
length: Option<i64>,
) -> Bytes;
/// Replaces the file contents.
async fn write(
&self,
content: Bytes,
) -> WriteResult;
}
このcodeはconceptual syntaxであり、具体的なAPIとして確定したものではない。Option<i64>の対応範囲、Bytes wrapper、documentationの取得方法、async methodとerrorの具体形は別途決定する。
Trait declarationとProtocol上の宣言は、次のように対応付ける。
- 一つのtrait declarationを、一つのInterface Descriptorを生成するRust側definitionとして扱う。
- Trait名はgenerated Rust itemやlocal registrationを識別するために使用できるが、Protocol
InterfaceDescriptorのfieldや構造化されたInterface参照としてそのまま公開しない。 - Trait methodをOperation Declarationとして扱う。
- ReceiverはProtocol parameterに含めない。
- Method parameterを名前付きOperation parameterとして扱う。
- Return typeをOperation return typeとして扱う。
- Rust typeをDescriptorの
TypeExprとProtocolValueへ対応付ける。 - Documentationをtrait、method、parameter、return、関連typeから取得できるようにする。
したがって、Descriptorのshapeを決めるのはtrait declarationである。各structがそのtraitをどう実装するかによって、Descriptorは変化しない。
Object implementation
その時点でWorldspaceへ投影するObjectは、Host内部ではRust structとして表現できる。
struct File {
name: String,
path: PathBuf,
reference: Option<String>,
validator: Option<Vec<u8>>,
}
公開metadataを提供するために、structが概念上のWipObject traitを実装する形を想定する。
impl WipObject for File {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> Option<&str> {
None
}
fn reference(&self) -> Option<&str> {
self.reference.as_deref()
}
fn validator(&self) -> Option<&[u8]> {
self.validator.as_deref()
}
}
WipObjectは、Protocol Objectの公開metadataのうち、publication時に選ぶInterface集合以外を提供する。返す値のownershipやborrow形式は未決定である。
Structが保持する情報のすべてを公開metadataにするわけではない。次の情報はWipObjectがProtocol metadataとして返す値に含めないが、implementing structのprivate fieldとして保持してよい。
- Absolute path。PathはObjectのfieldではなく、Worldspace resolutionのcontextである。
- Children。ChildrenはTree上の配置contextである。
- 固定されたInterface集合。InterfaceはObjectを公開する時点で追加する。
- Backend connection、file descriptor、credential等のHost-private state。
たとえば、structのpathやbackend handleはHost内部のOperation implementationで使用できる。ただし、WipObjectのmetadataやProtocol Objectへ自動的にserializeする対象にはしない。
Interface implementation
Object implementationに使うstructは、公開したいInterface traitを実装する。
impl FileInterface for File {
async fn read(
&self,
offset: Option<i64>,
length: Option<i64>,
) -> Bytes {
// self.pathが示す現在のfileを読む
}
async fn write(
&self,
content: Bytes,
) -> WriteResult {
// self.pathが示す現在のfileへ書く
}
}
Trait implementationが提供するのはOperationのbehaviorである。Interface Descriptorの生成元は、このimplementationではなくInterface trait declarationとする。
また、traitを実装しただけでInterfaceを公開することにはしない。Rustには、あるtypeが実装しているtraitを列挙する一般的なreflectionがなく、一つのstructが複数のInterface traitを実装することもあるためである。
Object publication
ObjectをWorldspaceへ返す時点で、struct instanceと、公開する0個以上のInterface Declarationを明示的に組み合わせる。Interfaceは追加順を保持するordered setとし、同じDefinitionの重複追加は認めない。
次のconceptual syntaxでは、解決したFileに公開するInterfaceを追加している。
let file = File::resolve(path).await?;
let object = PublishedObject::new(file)
.expose(FileInterfaceDefinition);
各InterfaceDefinitionは、#[wip::interface]が生成するconceptualなcompanion definitionを表す。実際の名前も、associated item、module、valueのどの形で生成するかも未決定である。
Publication APIが各Definitionの追加に要求するboundは、概念的には次の形になる。
impl<T: WipObject> PublishedObject<T> {
fn expose<I>(self, interface: I) -> Self
where
I: InterfaceDefinition<T>;
}
impl<T: FileInterface> InterfaceDefinition<T>
for FileInterfaceDefinition
{
// generated registration and dispatch metadata
}
このboundで、公開する値がObject metadataを提供し、追加する各InterfaceのOperation implementationも提供することをcompile timeに確認できる。Interfaceを一つも追加しないObjectも表現できる。
PublishedObjectはHost SDK内のconceptual typeであり、Protocol typeには追加しない。Host Runtimeはこの値からProtocol Objectを構築する。
Object {
name: object.name(),
description: object.description(),
interfaces: selected_interface_references,
r#ref: object.reference(),
validator: object.validator(),
}
selected_interface_referencesには、Hostが各generated Interface Definitionの登録または公開時に割り当てる構造化参照{ scope, name }を、publication APIへの追加順で並べる。同じ(scope, name)の重複は拒否し、Objectの配置pathに対する祖先制約も検証する。Macroが参照をcompile timeに固定することは要求しない。
observeでObjectに含めるのは、Descriptor自体ではなく、このInterface参照のordered setだけである。Clientが各参照をfetch_interfaceすると、Host Runtimeはtrait declarationから生成したDescriptorと、存在する場合はInterface validatorを返す。
同じ参照とvalidatorの組に対応するDescriptorは、Host上で共有およびcacheできる。このため、Object instanceごとにDescriptorを再生成したり再送したりする必要はない。
Macroが生成する責務
#[wip::interface]には、宣言からDescriptorを生成するだけでなく、Valueの変換・検証とdispatchに必要なcodeを提供する役割を想定する。
- Interface Descriptorを構築するdefinition。
- Trait methodとOperation Declarationの対応情報。
- ArgumentsのProtocol
Valuedecoder。 - ResultのProtocol
Valueencoder。 - Argumentsとresultのruntime validation。
- Operation名から対応するtrait methodを呼び出すdispatch code。
- 任意の
T: WipObject + InterfaceTraitについて、そのInterfaceをPublishedObject<T>へ追加するための型付きentry point。 - HostがInterface参照、optional validator、Descriptorを登録、取得、cacheするためのidentity。
対象structがInterface traitを実装しているかは、publication時のgeneric boundで確認する。MacroがRust codebase内のtrait implementationを探索したり列挙したりすることは要求しない。
Generated dispatch codeはRust SDK内部の実装詳細である。Host内部のdispatch mechanismやRust type名等はProtocolへ公開しない。
Rust typeとProtocol type
Interface traitで使うRust typeには、DescriptorのTypeExprとProtocol Valueへの対応付けが必要となる。概念的には、deriveまたは明示的なcodec implementationを使うことを想定する。
#[derive(WipType)]
struct WriteResult {
bytes_written: i64,
}
#[derive(WipType)]
enum ReadResult {
Data { content: Bytes },
EndOfFile,
}
このsyntaxも未決定である。Typeの対応付けでは、少なくとも次を表現できる必要がある。
- Unit、Boolean、Integer、Number、String、Bytes、Json、Entry。
- Record、List、Enum、Union。
- Descriptor内のNamed type。
- Requiredとoptionalなfieldおよびparameter。
- Documentation。
RustのOption<T>をoptional field / parameter以外でも許可するかは、まだ決めていない。EnumをProtocol EnumまたはUnionへどう対応付けるか、newtypeをどう扱うかも未決定である。
型の対応付けとは別に、Protocol boundaryでの検証も必要となる。Rust SDKは、integerのProtocol相互運用範囲、finite number、unknown field、Union shape、Entry path等をそこで検証しなければならない。
Dynamic Worldspaceでのlifecycle
observe
Projection implementationはrequest時にbackend dataを解決し、その時点のstruct instanceを生成する。
async fn resolve_file(path: &WipPath) -> Result<PublishedObject> {
let stat = filesystem.stat(path).await?;
let file = File {
name: stat.name,
path: stat.path,
reference: make_reference(&stat),
validator: make_validator(&stat),
};
Ok(PublishedObject::new(file)
.expose(FileInterfaceDefinition))
}
Host RuntimeはPublishedObjectからProtocol Objectを返す。Responseを生成した後は、このFile instanceを破棄してよい。Worldspace内の全fileに対応するinstanceを保持する必要はない。
Requested depthが正の場合は、Directory listingで発見した各entryについて、その時点のstruct instanceを生成して公開できる。Providerがdirectory全体を先に収集し、Vecとして返すことは要求しない。ただし、一つのparentのobservationを成功させるには、公開されたindexable childrenをclean exhaustionまで列挙する必要がある。
let mut entries = filesystem.discover_children(path, budget).await?;
while let Some(entry) = entries.next().await? {
let Some(child) = project_current_object(
entry.path(),
context,
).await? else {
continue;
};
observation.push_child(child)?;
}
discover_children、budget、observationは具体APIではなく、conceptualな表記である。Childの発見と公開には、次のsemanticsを要求する。
- 各childを現在の主体についてprojectし、非公開Objectとそのsubtreeを返さない。
- 公開されたindexable childrenをすべて列挙し、clean exhaustionで終了する。
- Backend failure、cancellation、resource budget超過をclean exhaustionとして扱わず、部分的なsuccess responseを返さない。
- Requested depthに達したnodeではchild discoveryを行わず、
childrenを未観測として省略する。 - 異なるconcrete published typeを、Providerから共通のreturn typeとして扱える。
具体的なreturn type、internal streaming、backpressure、type erasure、error propagationは未決定である。
call_operation
Call時には現在のObjectを解決し直す。HostはrequestのTargetとInterfaceTargetをHost Runtimeへ渡し、Runtimeは一つのatomic execution boundaryに入ってから、target.pathを現在のObject implementationとstableなbackend対象へ解決する。
runtime.call_current(
target,
interface,
operation,
arguments,
context,
|path, context| resolve_current_projection(path, context),
).await
resolve_current_projectionとruntime.call_currentを実際のAPIとして採用するかは決めていない。ResolverはRuntimeが開始したexecution boundary内で呼ばれ、そのboundary中にObject identityとbackend対象を安定して保持するhandleまたはtransaction contextを含む値を返す必要がある。上のexampleは、次の要件を表す。
- 過去の
observeで作成したstruct instanceを保持していなくてもOperationを実行できる。 - Call時の現在の主体、Object visibility、Object metadata、Interface集合を同じProjection pathで再解決し、Objectが非公開なら
NotFoundを返す。 - Requestの
interface.referenceが現在のObject.interfacesに含まれ、self-scopeを含む祖先制約を満たすことを確認する。指定されたinterface.scope_refと現在のscope Objectのrefも照合し、満たせない場合はInterfaceMismatchを返す。scope_refの省略だけでは拒否しない。 - 現在のInterfaceにOperationが宣言されていなければ
OperationNotFound、Operationを公開しつつ実行だけを拒否する場合はPermissionDeniedを返す。 interface.validatorを現在のInterface validatorへ照合する。Mutable InterfaceにvalidatorがなければInterfaceValidatorRequired、不一致ならInterfaceValidatorMismatchを返す。- 現在のProjected Object、Object validator、選択されたInterfaceとそのvalidator、Descriptor、Operation、Rust側のObject implementation、実際のbackend対象を一つのexecution boundary内で一致させる。
- Object validatorが指定されている場合は、現在のObject identityと保護対象状態の両方へ照合し、同じexecution boundary内でOperationを実行する。
- Pathが別Objectを指す場合は、provider-local revisionや内容が同じでもObject validatorを一致させない。
- Providerのvalidatorがobject-localなrevisionだけの場合、HostはObject identityを含む形へnamespace化またはwrapできなければならない。
- Object validatorが省略され、Operationがvalidatorを要求する場合は、実行せず
ValidatorRequiredを返す。 - Object validatorなしの実行をOperationが許可する場合は、実行時にpathが指す現在Objectへ無条件に実行する。
- Macro-generated dispatchが、選択されたInterface traitのmethodを現在のstruct instanceに対して呼び出す。異なるInterfaceの同名Operationをmergeまたは曖昧に解決しない。
- Operation実行後の現在状態に対応するvalidatorを、必要に応じて
CallOperationResponseへ返せる。
Object validatorとObject identityの関連付けに、Client-facingなrefは必須ではない。ProviderまたはHostはHost-privateなidentityを使用できる。ただし、identity confirmation、Object validator比較、Interface membershipとvalidator比較、DescriptorとOperation implementationの固定、authorization、Operation実行は、一つのatomic boundaryで行う。
実行後のvalidatorをどこから取得するかも未決定である。同じstruct instanceから再計算する方式、Operation resultとともに返す方式、backendを再観測する方式が考えられるが、いずれもresultを生成した実行後状態とvalidatorを対応させる必要がある。
StructがPathBufだけを保持している場合、validator比較後にpathを無条件で開き直すと、対象差し替えに対するatomicityを保証できないことがある。Rust APIには、file descriptor、transaction、conditional update、lock等でbackend固有のatomicityを実現できることを求める。そのための抽象化は未決定である。
複数Interfaceの実装
一つのstructは、複数のInterface traitを実装できる。
impl ReadOnlyFileInterface for File {
// ...
}
impl WritableFileInterface for File {
// ...
}
そのうち、現在の主体へ公開する0個以上のInterfaceをpublication時に選び、表示したい順に追加する。
let mut object = PublishedObject::new(file)
.expose(ReadOnlyFileInterfaceDefinition);
if authorization.can_write() {
object = object.expose(WritableFileInterfaceDefinition);
}
object
Protocol Objectのinterfacesは追加順を保持するordered setであり、同じInterface参照の重複は拒否する。各Interface Descriptorは独立したOperation集合を宣言するため、複数のtrait implementationを一つのDescriptorへ暗黙にmergeしない。
異なるInterfaceが同じOperation名を宣言しても、dispatchはrequestのInterfaceTarget.referenceとOperation名の組で一意に決まる。表示上のqualified表記はText Viewの参照表記に従い、signature全体をProtocol request fieldへparseし直さない。
Rust traitのsupertrait、trait alias、複数traitの組み合わせをDescriptor生成にどう対応付けるかは未決定である。これらによって、Protocol上のInterface inheritanceを暗黙に導入することはしない。
固定Interface向けのsugar
一つのRust typeを常に同じInterface集合で公開する場合は、次のようなsugarを追加してよい。
#[wip::object(interfaces = [ReadOnlyFileInterface, WritableFileInterface])]
struct File {
// ...
}
別のconceptualな形として、WipObjectのassociated itemにdefault Interface集合を指定することも考えられる。
ただし、core modelの基礎はexplicit publicationとする。主体、policy、resource kind等によって、同じstructへ異なるInterface subsetを追加できる必要があるため、固定Interface集合をcore modelにはしない。
Sugarを使う場合も、対象typeが指定したすべてのInterface traitを実装していることと、同じInterfaceを重複していないことを確認する必要がある。
HostへのInterface登録
Generated Interface Definitionには、Hostへの登録を通じて構造化されたInterface参照を割り当てる必要がある。参照の絶対scopeとlocal name、および配置pathへの祖先制約は2.2. Operation and Interfaceに従う。
Self-scopeは許可され、scopeはそのpathのObjectに紐づく。Scope ObjectのoptionalなrefはProtocolのscope_refとして観測・検証する。refがなければidentityを追跡できないことも許容し、scopeの移動追跡や独立したscope validatorは導入しない。
登録方法としては、conceptualにはeager registrationとlazy registrationの両方が考えられる。次の例はscope / nameを渡す具体構文を省略しており、暗黙のroot scopeを定めてはいない。登録時のcontextと引数の具体APIも引き続き未決定である。
let file_read_interface = host.register_interface(ReadOnlyFileInterfaceDefinition)?;
let file_write_interface = host.register_interface(WritableFileInterfaceDefinition)?;
let object = PublishedObject::new(file)
.expose(file_read_interface)
.expose(file_write_interface);
また、Hostが最初のpublication時に各definitionをinternする方式でもよい。どちらの登録方法でも、次の条件を満たす必要がある。
- MutableなInterface参照では
fetch_interfaceがvalidatorを返し、同じ参照とvalidatorの組から異なるDescriptorを返さない。 - Validatorを返さないInterface参照は、その参照の有効期間中Descriptorをimmutableにする。
- Descriptorを更新する場合は同じ参照へ新しいvalidatorを発行するか、新しいInterface参照を使用する。
- Objectの
interfacesへ設定した各Interface参照のDescriptorを、その主体からfetch_interfaceできる。 - 一つのObjectへ同じInterface参照を重複して追加しない。
- 構造的に同じDescriptorを同じ参照へdeduplicateすることは要求しない。
- Rust type名やprocess-local addressを、そのままportableなProtocol semanticsとして扱わない。
Request contextとauthorization
Operation implementationが必要とする情報は、Object instanceだけとは限らない。Request contextとして、次の情報も必要になり得る。
- 現在の主体。
- Cancellationとdeadline。
- Tracing / audit context。
- Host serviceやbackend connectionへのaccess。
- Resource limit。
このcontextをmethod parameter、receiver wrapper、generated context、task-local stateのどれで渡すかは未決定である。どの形でも、Protocol parameterとして宣言された値とHostが注入するcontextは明確に区別する必要がある。
Interface集合はObject publication時に選択するだけでなく、Operation call時にもmembership、Interface validator、authorizationを再評価できなければならない。以前のobserveで公開したInterface集合だけを根拠に、現在のOperation実行を許可することはできない。
Resultのvisibility checkも、通常のstructural Value validationとは別に必要となる。Hostは、Operation resultのRecord、List、Union等にnestedしたすべてのEntryについて、現在の主体にそのpathを認識させてよいかを再帰的に確認できなければならない。この確認をMacro-generated code、Projection、Host Runtimeのどこで行うかは未決定である。
Argumentとして受け取ったEntry pathについても、pathの所持をauthorizationの証明にはしない。参照先へ作用する時点で、現在の主体について認可する。
Error model
Rust methodのerror表現には、次の四種類を区別できることを求める。
- Protocol validationまたはdispatch failure。
- Effectがcommitされていないと保証できるHost / backend implementation failure。
- Operationの未実行またはrollbackを保証できない
OperationOutcomeUnknown。 - Interfaceがreturn typeとして宣言したdomain failure。
現在のProtocolはdomain failureをOperation resultのUnion caseとして表す。RustのResult<T, E>をdomain Union、Host error、またはその両方のどれに対応付けるかは未決定である。
Rust panic、result encode failure、宣言型に違反するimplementation resultを、Macroがdomain failureへ自動変換することは認めない。Host Runtimeは、effectがcommitされていないと保証できる場合だけInternalへmappingし、保証できなければOperationOutcomeUnknownへmappingする。Clientへ送信済みかどうか不明なtransport切断についても、自動retryはしない。
要件
このdraftのRust API modelに求める要件は、少なくとも次のとおりである。
- Interface Descriptorをtrait declarationから生成し、trait implementationごとに変化させない。
- Structが
WipObjectと、追加する各Interface traitを実装していることをcompile timeに確認する。 - 0個以上のInterfaceをpublication時に明示し、実装済みtraitの自動列挙に依存しない。
- Objectのpath、children、Interface集合をstructへ恒久的に固定しない。
- Struct instanceをrequest scopeで生成および破棄できる。
- 全Object instanceの事前登録を要求しない。
- Direct childrenを現在の主体についてprojectし、非公開Objectとそのsubtreeを除外しつつlazyに発見し、success responseではclean exhaustionまで列挙する。
- Backend failure、cancellation、resource budget超過をclean exhaustionとして扱わず、部分的なobservationをsuccess responseとして返さない。
- 同じstructへ複数のInterfaceを同時に追加でき、追加順を保持し、重複を拒否できる。
- 異なるInterfaceの同名Operationを、Interface参照とOperation名の組でdispatchできる。
- Protocol boundaryでarguments、result、Object、Descriptorを検証する。
- Operation result内のnestedな
Entryへ、現在の主体についてvisibility checkを適用できる。 - Host-private fieldをProtocol ObjectまたはValueへ暗黙にserializeしない。
- Call時の現在の主体、Object visibility、Object validator、Interface membershipとvalidator、Descriptor、Operation、Rust側implementation、backend対象を一貫したexecution boundaryで扱う。
- 非公開Object、Interface非membership、非公開Operation、公開済みOperationの実行拒否をそれぞれ
NotFound、InterfaceMismatch、OperationNotFound、PermissionDeniedへ対応付ける。 - Interface validatorの必須・不一致を
InterfaceValidatorRequired、InterfaceValidatorMismatchへ対応付ける。 - Object validatorを現在のObject identityと保護対象状態へ結び付け、別Objectに同じprovider-local revisionがあっても一致させない。
- Object validator比較、identity confirmation、Interface membershipとvalidator比較、DescriptorとOperation実装の固定、authorization、Operation実行を同じatomic boundaryで行う。
- Object validatorを要求するOperationへの省略を
ValidatorRequiredとして拒否できる。 - Provider、Host Runtime、macro-generated codeのerrorを、effect未commitを保証できる
Internalと、保証できないOperationOutcomeUnknownへ安全に対応付けられる。 - InvalidなObject、Descriptor、Operation resultをsuccess responseとして送信せず、Host-side validation failureをdomain failureへ変換しない。
- Rust SDK固有の型やlifetimeをProtocol semanticsとして要求しない。
非目標
次の事項は、このdraftでは決定しない。
- 最終的なcrate、module、trait、attribute、deriveの名前。
- Proc macroの内部実装。
- 特定のasync runtime。
- Object instanceの常駐registryまたはactor model。
- Rust trait objectをwireへ公開すること。
- Rust trait inheritanceをProtocol Interface inheritanceとして扱うこと。
- Backend entityとRust structまたはProtocol Objectの一対一対応。
- Client SDKのtyped proxy API。
未決定事項
WipObjectの具体的なmethodとmetadata ownership。- Object metadata取得をsyncまたはasyncのどちらにするか。
- Publication APIがObject instanceをown、borrow、
Arc化するか。 - Generated Interface Definitionの具体的なRust表現。
- Interface登録とscope / local name割当のlifecycle。
- Trait、Operation、parameter名のrename規則。
- Rust doc commentとProtocol Documentationの対応。
- Supported Rust typeと
TypeExprの対応。 Option<T>を許可する位置。- Generic method、associated type、lifetime parameterをInterface traitで許可するか。
&self、&mut self、owned receiverのどれをOperationで許可するか。- Async methodをnative syntax、generated boxed future、別macroのどれで扱うか。
- Request contextとProtocol parameterのAPI上の分離。
- Nestedな
Entryresultのvisibility checkをMacro-generated code、Projection、Host Runtimeのどこで行うか。 - Domain failure、Host error、panicのRust上の表現。
- Macro-generated dispatchとHost Runtimeの責務境界。
- Published ObjectをProviderから返す際のtype erasure。
- Validator比較とOperation実行を囲むbackend-specificなatomicity abstraction。
- Provider-local validatorをObject identityへnamespace化またはwrapするHost API。
- Operation実行後validatorをObject instance、Operation implementation、Providerのどこから取得するか。
- 主体ごとに異なるInterface combinationを追加したPublished Objectのconcrete return typeをどう統一するか。
- Rust client APIを同じtrait declarationから生成するか。
これらの選択は、静的service objectに使えるだけでは足りない。Filesystem、database row、virtual aggregate、主体別Interface compositionも、同じAPIで実装できるかを評価して決定する。