Skip to content
18 changes: 9 additions & 9 deletions src/keria/app/aiding.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 20 additions & 4 deletions src/keria/app/credentialing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -131,7 +133,7 @@ class Schema:

@dataclass
class CredentialStateBase:
vn: Tuple[int, int]
vn: List[int]
i: str
s: str
d: str
Expand Down Expand Up @@ -201,15 +203,29 @@ 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
class Registry:
name: str
regk: str
pre: str
state: Union[CredentialStateIssOrRev, CredentialStateBisOrBrv]
state: RegistryState


class RegistryCollectionEnd:
Expand Down
34 changes: 27 additions & 7 deletions src/keria/app/specing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"] = {
Expand All @@ -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",
Expand Down Expand Up @@ -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(
Expand Down
92 changes: 30 additions & 62 deletions src/keria/utils/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -328,33 +336,26 @@ def dataclassFromFielddom(
if customTypes is None:
customTypes = {}

requiredFields = []
optionalFields = []
allFields = []
customFields = {}

# Store alt constraints for use by schema generation
altConstraints = getattr(fieldDom, "alts", {})

# 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
Expand Down Expand Up @@ -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
Expand Down
Loading