@@ -165,7 +165,7 @@ pub struct ExtensionManifest {
165165 #[ serde( skip_serializing_if = "Option::is_none" ) ]
166166 pub requires : Option < RequirementsConfig > ,
167167
168- // Multi-extension composition: role ownership used to disambiguate a
168+ // Multi-extension composition: `includes` primacy is used to disambiguate a
169169 // capability provided by more than one linked extension.
170170 #[ serde( default , skip_serializing_if = "Option::is_none" ) ]
171171 pub composition : Option < CompositionConfig > ,
@@ -179,33 +179,20 @@ pub struct ExtensionManifest {
179179 pub extension_path : Option < String > ,
180180}
181181
182- /// Multi-extension composition metadata. Declares how a component's linked
183- /// extensions relate so Homeboy can resolve a capability that more than one of
184- /// them provides without requiring manual `capability_extensions` selection.
182+ /// Multi-extension composition metadata.
183+ ///
184+ /// `includes` is the sole ownership signal: when several linked extensions
185+ /// provide the same capability, an extension that composes all the others is
186+ /// resolved as the primary owner. See
187+ /// `homeboy_core::extension_execution::disambiguate_capability_owner`.
188+ ///
189+ /// Unknown keys are ignored rather than rejected, so manifests still carrying
190+ /// the retired `roles`/`optional`/`conflicts` metadata continue to load.
185191#[ derive( Debug , Clone , Default , Serialize , Deserialize , PartialEq , Eq ) ]
186192pub struct CompositionConfig {
187- /// Extensions this one composes with (informational) .
193+ /// Extensions this one composes with. Used to resolve capability ownership .
188194 #[ serde( default , skip_serializing_if = "Vec::is_empty" ) ]
189195 pub includes : Vec < String > ,
190- /// Optional companion assets (informational).
191- #[ serde( default , skip_serializing_if = "Vec::is_empty" ) ]
192- pub optional : Vec < String > ,
193- /// Role name -> owning extension(s). A role with a single owner designates
194- /// the extension that owns that role's capabilities across the composition.
195- #[ serde( default , skip_serializing_if = "BTreeMap::is_empty" ) ]
196- pub roles : BTreeMap < String , RoleOwners > ,
197- /// Extensions that must not be linked together (informational).
198- #[ serde( default , skip_serializing_if = "Vec::is_empty" ) ]
199- pub conflicts : Vec < String > ,
200- }
201-
202- /// Owner(s) of a composition role. Manifests use both a single owner
203- /// (`"javascript": "nodejs"`) and a list (`"project": ["a", "b"]`).
204- #[ derive( Debug , Clone , Serialize , Deserialize , PartialEq , Eq ) ]
205- #[ serde( untagged) ]
206- pub enum RoleOwners {
207- One ( String ) ,
208- Many ( Vec < String > ) ,
209196}
210197
211198impl ExtensionManifest {
@@ -574,7 +561,22 @@ mod composition_tests {
574561 use super :: * ;
575562
576563 #[ test]
577- fn deserializes_mixed_single_and_list_role_owners ( ) {
564+ fn deserializes_includes ( ) {
565+ let manifest: ExtensionManifest = serde_json:: from_value ( serde_json:: json!( {
566+ "name" : "wordpress" ,
567+ "version" : "1.0.0" ,
568+ "composition" : { "includes" : [ "nodejs" ] }
569+ } ) )
570+ . expect ( "manifest with composition deserializes" ) ;
571+
572+ let composition = manifest. composition . expect ( "composition present" ) ;
573+ assert_eq ! ( composition. includes, vec![ "nodejs" . to_string( ) ] ) ;
574+ }
575+
576+ /// Manifests published before `roles`/`optional`/`conflicts` were retired
577+ /// must still load — the keys are simply ignored rather than rejected.
578+ #[ test]
579+ fn retired_composition_keys_are_tolerated ( ) {
578580 let manifest: ExtensionManifest = serde_json:: from_value ( serde_json:: json!( {
579581 "name" : "wordpress" ,
580582 "version" : "1.0.0" ,
@@ -588,25 +590,10 @@ mod composition_tests {
588590 "conflicts" : [ ]
589591 }
590592 } ) )
591- . expect ( "manifest with composition deserializes" ) ;
593+ . expect ( "manifest with retired composition keys still deserializes" ) ;
592594
593595 let composition = manifest. composition . expect ( "composition present" ) ;
594596 assert_eq ! ( composition. includes, vec![ "nodejs" . to_string( ) ] ) ;
595- // A bare string owner deserializes into the single-owner shape.
596- assert_eq ! (
597- composition. roles. get( "javascript" ) ,
598- Some ( & RoleOwners :: One ( "nodejs" . to_string( ) ) )
599- ) ;
600- // A list owner deserializes into the multi-owner shape.
601- assert_eq ! (
602- composition. roles. get( "project" ) ,
603- Some ( & RoleOwners :: Many ( vec![
604- "wordpress-plugin" . to_string( ) ,
605- "wordpress-theme" . to_string( ) ,
606- ] ) )
607- ) ;
608- // Absent role.
609- assert_eq ! ( composition. roles. get( "missing" ) , None ) ;
610597 }
611598
612599 #[ test]
0 commit comments