diff --git a/src/keria/app/aiding.py b/src/keria/app/aiding.py index 5eaede1..e64f245 100644 --- a/src/keria/app/aiding.py +++ b/src/keria/app/aiding.py @@ -2109,15 +2109,15 @@ class WellKnown: @dataclass class MemberEnds: - agent: Optional[Dict[str, str]] = None - controller: Optional[Dict[str, str]] = None - witness: Optional[Dict[str, str]] = None - registrar: Optional[Dict[str, str]] = None - watcher: Optional[Dict[str, str]] = None - judge: Optional[Dict[str, str]] = None - juror: Optional[Dict[str, str]] = None - peer: Optional[Dict[str, str]] = None - mailbox: Optional[Dict[str, str]] = None + agent: Optional[Dict[str, Dict[str, str]]] = None + controller: Optional[Dict[str, Dict[str, str]]] = None + witness: Optional[Dict[str, Dict[str, str]]] = None + registrar: Optional[Dict[str, Dict[str, str]]] = None + watcher: Optional[Dict[str, Dict[str, str]]] = None + judge: Optional[Dict[str, Dict[str, str]]] = None + juror: Optional[Dict[str, Dict[str, str]]] = None + peer: Optional[Dict[str, Dict[str, str]]] = None + mailbox: Optional[Dict[str, Dict[str, str]]] = None @dataclass diff --git a/src/keria/app/credentialing.py b/src/keria/app/credentialing.py index e05f89b..92758c9 100644 --- a/src/keria/app/credentialing.py +++ b/src/keria/app/credentialing.py @@ -22,7 +22,7 @@ from keri.core.serdering import Protocols, Vrsn_1_0, Vrsn_2_0, SerderKERI from ..core import httping, longrunning from marshmallow import fields, Schema as MarshmallowSchema -from typing import List, Dict, Any, Optional, Tuple, Literal, Union +from typing import List, Dict, Any, Optional, Literal, Union from .aiding import ( Seal, ICP_V_1, @@ -93,6 +93,8 @@ class ACDCAttributes: acdcCustomTypes = { "a": ACDCAttributes, "A": Union[str, List[Any]], + "e": Dict[str, Any], + "r": Dict[str, Any], } acdcFieldDomV1 = SerderKERI.Fields[Protocols.acdc][Vrsn_1_0][None] ACDC_V_1, ACDCSchema_V_1 = dataclassFromFielddom( @@ -131,7 +133,7 @@ class Schema: @dataclass class CredentialStateBase: - vn: Tuple[int, int] + vn: List[int] i: str s: str d: str @@ -201,7 +203,21 @@ class ClonedCredential: status: Union[CredentialStateIssOrRev, CredentialStateBisOrBrv] anchor: Anchor anc: AnchoringEvent # type: ignore - ancatc: str + ancatc: List[str] + + +@dataclass +class RegistryState: + vn: List[int] + i: str + s: str + d: str + ii: str + dt: str + et: Literal["vcp", "vrt"] + bt: str + b: List[str] + c: List[str] @dataclass @@ -209,7 +225,7 @@ class Registry: name: str regk: str pre: str - state: Union[CredentialStateIssOrRev, CredentialStateBisOrBrv] + state: RegistryState class RegistryCollectionEnd: diff --git a/src/keria/app/specing.py b/src/keria/app/specing.py index ec61b19..fd3eeeb 100644 --- a/src/keria/app/specing.py +++ b/src/keria/app/specing.py @@ -175,14 +175,36 @@ def __init__(self, app, title, version="1.0.1", openapi_version="3.1.0"): {"$ref": "#/components/schemas/DRT_V_2"}, ] } - credentialSchema["properties"]["anc"] = ancEvent + self.spec.components.schemas["KeyEvent"] = ancEvent + credentialSchema["properties"]["anc"] = { + "$ref": "#/components/schemas/KeyEvent" + } # CredentialState self.spec.components.schemas["CredentialState"] = { "oneOf": [ {"$ref": "#/components/schemas/CredentialStateIssOrRev"}, {"$ref": "#/components/schemas/CredentialStateBisOrBrv"}, - ] + ], + "properties": { + "et": { + "type": "string", + "enum": ["iss", "rev", "bis", "brv"], + }, + "ra": { + "type": "object", + "description": "Empty for iss/rev, RaFields for bis/brv", + }, + }, + "discriminator": { + "propertyName": "et", + "mapping": { + "iss": "#/components/schemas/CredentialStateIssOrRev", + "rev": "#/components/schemas/CredentialStateIssOrRev", + "bis": "#/components/schemas/CredentialStateBisOrBrv", + "brv": "#/components/schemas/CredentialStateBisOrBrv", + }, + }, } credentialSchema["properties"]["status"] = { @@ -194,10 +216,6 @@ def __init__(self, app, title, version="1.0.1", openapi_version="3.1.0"): "Registry", schema=marshmallow_dataclass.class_schema(credentialing.Registry)(), ) - registrySchema = self.spec.components.schemas["Registry"] - registrySchema["properties"]["state"] = { - "$ref": "#/components/schemas/CredentialState" - } self.spec.components.schema( "AgentResourceResult", @@ -314,7 +332,9 @@ def __init__(self, app, title, version="1.0.1", openapi_version="3.1.0"): schema=marshmallow_dataclass.class_schema(agenting.KeyEventRecord)(), ) keyEventRecordSchema = self.spec.components.schemas["KeyEventRecord"] - keyEventRecordSchema["properties"]["ked"] = ancEvent + keyEventRecordSchema["properties"]["ked"] = { + "$ref": "#/components/schemas/KeyEvent" + } # Register the AgentConfig schema self.spec.components.schema( diff --git a/src/keria/utils/openapi.py b/src/keria/utils/openapi.py index d399d1a..19c0191 100644 --- a/src/keria/utils/openapi.py +++ b/src/keria/utils/openapi.py @@ -235,6 +235,15 @@ def createCustomNestedField( return createOptionalField( key, customType, mm_fields.List, (nestedField,), {}, isOptional ) + elif customType.__origin__ is dict: + return createOptionalField( + key, + customType, + mm_fields.Dict, + (), + {"keys": mm_fields.String(), "values": mm_fields.Raw()}, + isOptional, + ) # For other generic types, fall back to Raw field return createOptionalField(key, customType, mm_fields.Raw, (), {}, isOptional) @@ -291,12 +300,11 @@ def createRegularField( def processField( key: str, value: Any, - fieldDom: serdering.FieldDom, + isOptional: bool, customTypes: Dict[str, type], name: str = "", ) -> tuple[tuple, Optional[mm_fields.Field]]: """Process a single field from the FieldDom.""" - isOptional = key in fieldDom.opts # Check if there's a custom type specified if key in customTypes: @@ -328,8 +336,7 @@ def dataclassFromFielddom( if customTypes is None: customTypes = {} - requiredFields = [] - optionalFields = [] + allFields = [] customFields = {} # Store alt constraints for use by schema generation @@ -337,24 +344,18 @@ def dataclassFromFielddom( # Process all fields from alls (all possible fields) for key, value in fieldDom.alls.items(): + isOptional = key in fieldDom.opts fieldDef, marshmallowField = processField( - key, value, fieldDom, customTypes, name + key, value, isOptional, customTypes, name ) if marshmallowField: customFields[key] = marshmallowField - # Check if field is optional (in opts) - isOptional = key in fieldDom.opts - if isOptional: - optionalFields.append(fieldDef) - else: - requiredFields.append(fieldDef) + allFields.append(fieldDef) - allFields = requiredFields + optionalFields - generatedCls = make_dataclass(name, allFields) + generatedCls = make_dataclass(name, allFields, kw_only=True) schema = class_schema(generatedCls)() - # Override the automatically generated fields with our custom ones # marshmallow_dataclass automatically creates fields with allow_none=True for Optional[T] # We need to override them to have allow_none=False for openapi-typescript compatibility @@ -385,62 +386,29 @@ def applyAltConstraintsToOpenApiSchema( properties = openApiSchemaDict.get("properties", {}) required = openApiSchemaDict.get("required", []) + baseRequired = [f for f in required if f not in altConstraints] - # Find alternate field pairs that exist in properties - altGroups = {} - processedAlts = set() - - for field1, field2 in altConstraints.items(): - if field1 in processedAlts or field2 in processedAlts: - continue - if field1 in properties and field2 in properties: - groupKey = f"{field1}_{field2}" - altGroups[groupKey] = [field1, field2] - processedAlts.add(field1) - processedAlts.add(field2) - - if not altGroups: - return - - # Create oneOf schemas for alternate field combinations oneOfSchemas = [] - - for _, altFields in altGroups.items(): - field1, field2 = altFields - - # Base properties (all except alternates) - baseProps = {k: v for k, v in properties.items() if k not in altFields} - baseRequired = [f for f in required if f not in altFields] - - # Schema with field1 only - schemaWithField1 = { - "type": "object", - "properties": {**baseProps, field1: properties[field1]}, - "additionalProperties": openApiSchemaDict.get( - "additionalProperties", False - ), - } - if field1 in required or baseRequired: - schemaWithField1["required"] = baseRequired + ( - [field1] if field1 in required else [] - ) - oneOfSchemas.append(schemaWithField1) - - # Schema with field2 only - schemaWithField2 = { + # Note: For now, this only works with 1 pair of alts - if more ever come, we need to start computing permutations. + for keepField in altConstraints: + if keepField not in properties: + continue + variant = { "type": "object", - "properties": {**baseProps, field2: properties[field2]}, + "properties": { + k: v + for k, v in properties.items() + if k == keepField or k not in altConstraints + }, "additionalProperties": openApiSchemaDict.get( "additionalProperties", False ), } - if field2 in required or baseRequired: - schemaWithField2["required"] = baseRequired + ( - [field2] if field2 in required else [] - ) - oneOfSchemas.append(schemaWithField2) + variantRequired = baseRequired + ([keepField] if keepField in required else []) + if variantRequired: + variant["required"] = variantRequired + oneOfSchemas.append(variant) - # Replace the schema with oneOf constraint if oneOfSchemas: openApiSchemaDict.clear() openApiSchemaDict["oneOf"] = oneOfSchemas