11package com.avsystem.justworks.core.parser
22
3+ import arrow.core.compareTo
34import arrow.core.fold
45import arrow.core.merge
56import arrow.core.raise.context.Raise
@@ -124,11 +125,29 @@ object SpecParser {
124125 }
125126 }
126127
128+ // Pick up synthetic schemas added by detectAndUnwrapOneOfWrappers.
129+ // Iterate until stable, since processing a synthetic schema could register more.
130+ tailrec fun collectModels (processed : Set <String >, acc : List <SchemaModel >): List <SchemaModel > {
131+ val currentKeys = componentSchemas.keys - allSchemas.keys - processed
132+ return if (currentKeys.isEmpty()) {
133+ acc
134+ } else {
135+ val newModels = currentKeys
136+ .asSequence()
137+ .mapNotNull { name -> componentSchemas[name]?.let { name to it } }
138+ .filterNot { (_, schema) -> schema.isEnumSchema }
139+ .map { (name, schema) -> extractSchemaModel(name, schema) }
140+
141+ collectModels(processed + currentKeys, acc + newModels)
142+ }
143+ }
144+
145+ val syntheticModels = collectModels(emptySet(), emptyList())
127146 return ApiSpec (
128147 title = info?.title ? : " Untitled" ,
129148 version = info?.version ? : " 0.0.0" ,
130149 endpoints = endpoints,
131- schemas = schemaModels,
150+ schemas = schemaModels + syntheticModels ,
132151 enums = enumModels,
133152 securitySchemes = securitySchemes,
134153 )
@@ -261,6 +280,14 @@ object SpecParser {
261280 Discriminator (propertyName = propertyName, mapping = disc.mapping.orEmpty())
262281 }
263282
283+ // Resolve underlying type for primitive-only / $ref-wrapper schemas.
284+ // Uses $ref for wrapper schemas, otherwise resolves structurally
285+ // from type/format to bypass componentSchemaIdentity (which would self-reference).
286+ val underlyingType = schema
287+ .takeIf { properties.isEmpty() && allOf.isNullOrEmpty() && oneOf.isNullOrEmpty() && anyOf.isNullOrEmpty() }
288+ ?.let { s -> s.`$ref`?.removePrefix(SCHEMA_PREFIX )?.let (TypeRef ::Reference ) ? : s.resolveByType() }
289+ ?.takeUnless { it is TypeRef .Unknown }
290+
264291 return SchemaModel (
265292 name = name,
266293 description = schema.description,
@@ -270,6 +297,7 @@ object SpecParser {
270297 oneOf = oneOf?.let { it.map(TypeRef ::Reference ).ifEmpty { null } },
271298 anyOf = anyOf?.let { it.map(TypeRef ::Reference ).ifEmpty { null } },
272299 discriminator = discriminator,
300+ underlyingType = underlyingType,
273301 )
274302 }
275303
@@ -332,12 +360,14 @@ object SpecParser {
332360 )
333361
334362 val schemaName = ensureNotNull(
335- propertySchema.resolveName() ? : propertyName
336- .takeIf { propertySchema.isInlineObject }
337- ?.also { name ->
338- componentSchemas[name] = propertySchema
339- componentSchemaIdentity[propertySchema] = name
340- },
363+ propertySchema.resolveName()
364+ ? : propertyName
365+ .takeIf { propertySchema.isInlineObject }
366+ ?.let { rawName ->
367+ componentSchemas[rawName] = propertySchema
368+ componentSchemaIdentity[propertySchema] = rawName
369+ rawName
370+ },
341371 )
342372
343373 propertyName to schemaName
@@ -353,26 +383,30 @@ object SpecParser {
353383 private fun Schema <* >.toTypeRef (contextName : String? = null): TypeRef = contextName?.let { toInlineTypeRef(it) }
354384 ? : (resolveName() ? : allOf?.singleOrNull()?.resolveName())?.let (TypeRef ::Reference )
355385 ? : TypeRef .Unknown .takeIf { (allOf?.size ? : 0 ) > 1 }
356- ? : when (type) {
357- " string" -> STRING_FORMAT_MAP [format] ? : TypeRef .Primitive (PrimitiveType .STRING )
386+ ? : resolveByType(contextName)
358387
359- " integer" -> INTEGER_FORMAT_MAP [format] ? : TypeRef .Primitive (PrimitiveType .INT )
388+ /* * Resolves a [TypeRef] based on the schema's structural type/format, ignoring component identity. */
389+ context(_: ComponentSchemaIdentity , _: ComponentSchemas )
390+ private fun Schema <* >.resolveByType (contextName : String? = null): TypeRef = when (type) {
391+ " string" -> STRING_FORMAT_MAP [format] ? : TypeRef .Primitive (PrimitiveType .STRING )
360392
361- " number " -> NUMBER_FORMAT_MAP [format] ? : TypeRef .Primitive (PrimitiveType .DOUBLE )
393+ " integer " -> INTEGER_FORMAT_MAP [format] ? : TypeRef .Primitive (PrimitiveType .INT )
362394
363- " boolean " -> TypeRef .Primitive (PrimitiveType .BOOLEAN )
395+ " number " -> NUMBER_FORMAT_MAP [format] ? : TypeRef .Primitive (PrimitiveType .DOUBLE )
364396
365- " array " -> TypeRef .Array (items?.toTypeRef(contextName?. let { " ${it} Item " }) ? : TypeRef . Unknown )
397+ " boolean " -> TypeRef .Primitive ( PrimitiveType . BOOLEAN )
366398
367- " object" -> when (val ap = additionalProperties) {
368- is Schema <* > -> TypeRef .Map (ap.toTypeRef())
369- is Boolean -> if (ap) TypeRef .Map (TypeRef .Unknown ) else TypeRef .Unknown
370- else -> title?.let (TypeRef ::Reference ) ? : TypeRef .Unknown
371- }
399+ " array" -> TypeRef .Array (items?.toTypeRef(contextName?.let { " ${it} Item" }) ? : TypeRef .Unknown )
372400
373- else -> TypeRef .Unknown
401+ " object" -> when (val ap = additionalProperties) {
402+ is Schema <* > -> TypeRef .Map (ap.toTypeRef())
403+ is Boolean -> if (ap) TypeRef .Map (TypeRef .Unknown ) else TypeRef .Unknown
404+ else -> title?.let (TypeRef ::Reference ) ? : TypeRef .Unknown
374405 }
375406
407+ else -> TypeRef .Unknown
408+ }
409+
376410 context(_: ComponentSchemaIdentity , _: ComponentSchemas )
377411 private fun Schema <* >.toInlineTypeRef (contextName : String ): TypeRef ? = takeIf { isInlineObject }?.let {
378412 val required = required.orEmpty().toSet()
@@ -429,15 +463,26 @@ object SpecParser {
429463
430464 private val STRING_FORMAT_MAP = mapOf (
431465 " byte" to TypeRef .Primitive (PrimitiveType .BYTE_ARRAY ),
466+ " binary" to TypeRef .Primitive (PrimitiveType .BYTE_ARRAY ),
432467 " date-time" to TypeRef .Primitive (PrimitiveType .DATE_TIME ),
433468 " date" to TypeRef .Primitive (PrimitiveType .DATE ),
469+ " uuid" to TypeRef .Primitive (PrimitiveType .UUID ),
470+ " uri" to TypeRef .Primitive (PrimitiveType .STRING ),
471+ " url" to TypeRef .Primitive (PrimitiveType .STRING ),
472+ " email" to TypeRef .Primitive (PrimitiveType .STRING ),
473+ " hostname" to TypeRef .Primitive (PrimitiveType .STRING ),
474+ " ipv4" to TypeRef .Primitive (PrimitiveType .STRING ),
475+ " ipv6" to TypeRef .Primitive (PrimitiveType .STRING ),
476+ " password" to TypeRef .Primitive (PrimitiveType .STRING ),
434477 )
435478
436479 private val INTEGER_FORMAT_MAP = mapOf (
480+ " int32" to TypeRef .Primitive (PrimitiveType .INT ),
437481 " int64" to TypeRef .Primitive (PrimitiveType .LONG ),
438482 )
439483
440484 private val NUMBER_FORMAT_MAP = mapOf (
441485 " float" to TypeRef .Primitive (PrimitiveType .FLOAT ),
486+ " double" to TypeRef .Primitive (PrimitiveType .DOUBLE ),
442487 )
443488}
0 commit comments