Skip to content

Commit

Permalink
Fix validation of gRPC stub with nested types
Browse files Browse the repository at this point in the history
Fixed problem: The validation fails if the request prerequisite refers
to a field with a nested type or request/response classes has a nested type.

Example:

proto-file:

    syntax = "proto3";

    message V1 {
      message Request {
        enum Order {
          ORDER_UNKNOWN = 0;
          ORDER_ASC = 1;
          ORDER_DESC = 2;
        }
        Order order = 1;
      }

      message Response {
        enum Code {
          OK = 0;
          FAIL = 1;
        }
      }
    }

    service Service {
      rpc Call (V1.Request) returns (V1.Response);
    }

prerequisite:

    "requestPredicates": {
      "order": { "==": "ORDER_ASC" }
    }
  • Loading branch information
ashashev authored and danslapman committed May 23, 2024
1 parent c86339a commit e895004
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -433,9 +433,11 @@ final class AdminApiHandler(
)
)
requestSchema <- protobufSchemaResolver.parseDefinitionFrom(requestSchemaBytes)
rootFields <- GrpcStub.getRootFields(body.requestClass, requestSchema)
requestPkg = GrpcStub.PackagePrefix(requestSchema)
requestTypes = GrpcStub.makeDictTypes(requestPkg, requestSchema.schemas).toMap
rootFields <- GrpcStub.getRootFields(requestPkg.resolve(body.requestClass), requestTypes)
_ <- ZIO.foreachParDiscard(body.requestPredicates.definition.keys)(
GrpcStub.validateOptics(_, requestSchema, rootFields)
GrpcStub.validateOptics(_, requestTypes, rootFields)
)
candidates0 <- grpcStubDAO.findChunk(
prop[GrpcStub](_.methodName) === body.methodName,
Expand All @@ -456,8 +458,10 @@ final class AdminApiHandler(
)
)
responseSchema <- protobufSchemaResolver.parseDefinitionFrom(responseSchemaBytes)
_ <- GrpcStub.getRootFields(body.responseClass, responseSchema)
now <- ZIO.clockWith(_.instant)
responsePkg = GrpcStub.PackagePrefix(responseSchema)
responseTypes = GrpcStub.makeDictTypes(responsePkg, responseSchema.schemas).toMap
_ <- GrpcStub.getRootFields(responsePkg.resolve(body.responseClass), responseTypes)
now <- ZIO.clockWith(_.instant)
stub = body
.into[GrpcStub]
.withFieldComputed(_.id, _ => SID.random[GrpcStub])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import eu.timepit.refined.collection.*
import eu.timepit.refined.numeric.*
import io.circe.Json
import io.circe.refined.*
import io.estatico.newtype.macros.newtype
import io.estatico.newtype.ops.*
import mouse.boolean.*
import sttp.tapir.codec.refined.*
import sttp.tapir.derevo.schema
Expand Down Expand Up @@ -54,24 +56,25 @@ final case class GrpcStub(
object GrpcStub {
private val indexRegex = "\\[([\\d]+)\\]".r

def getRootFields(className: String, definition: GrpcProtoDefinition): IO[ValidationError, List[GrpcField]] = {
val name = definition.`package`.map(p => className.stripPrefix(p ++ ".")).getOrElse(className)
def getRootFields(
name: NormalizedTypeName,
types: Map[NormalizedTypeName, GrpcRootMessage] = Map.empty
): IO[ValidationError, List[GrpcField]] =
for {
rootMessage <- ZIO.getOrFailWith(ValidationError(Vector(s"Root message '$className' not found")))(
definition.schemas.find(_.name == name)
rootMessage <- ZIO.getOrFailWith(ValidationError(Vector(s"Root message '${name}' not found")))(
types.get(name)
)
rootFields <- rootMessage match {
case GrpcMessageSchema(_, fields, oneofs, _, _) =>
ZIO.succeed(fields ++ oneofs.map(_.flatMap(_.options)).getOrElse(List.empty))
case GrpcEnumSchema(_, _) =>
ZIO.fail(ValidationError(Vector(s"Enum cannot be a root message, but '$className' is")))
ZIO.fail(ValidationError(Vector(s"Enum cannot be a root message, but '${name}' is")))
}
} yield rootFields
}

def validateOptics(
optic: JsonOptic,
definition: GrpcProtoDefinition,
types: Map[NormalizedTypeName, GrpcRootMessage],
rootFields: List[GrpcField]
): IO[ValidationError, Unit] = for {
fields <- Ref.make(rootFields)
Expand All @@ -84,12 +87,11 @@ object GrpcStub {
case Right(fieldName) =>
for {
fs <- fields.get
pkgPrefix = definition.`package`.map(p => s".$p.").getOrElse(".")
field <- ZIO.getOrFailWith(ValidationError(Vector(s"Field $fieldName not found")))(fs.find(_.name == fieldName))
_ <-
if (primitiveTypes.values.exists(_ == field.typeName)) fields.set(List.empty)
else
definition.schemas.find(_.name == field.typeName.stripPrefix(pkgPrefix)) match {
types.get(NormalizedTypeName(field.typeName)) match {
case Some(message) =>
message match {
case GrpcMessageSchema(_, fs, oneofs, _, _) =>
Expand All @@ -107,6 +109,35 @@ object GrpcStub {
}
} yield ()

@newtype class PackagePrefix private (val asString: String) {
def :+(nested: String): PackagePrefix =
(asString ++ nested.dropWhile(_ == '.') ++ ".").coerce
def resolve(n: String): NormalizedTypeName =
if (n.startsWith(".")) NormalizedTypeName(n)
else if (n.startsWith(asString.drop(1))) NormalizedTypeName(s".$n")
else NormalizedTypeName(s"$asString$n")
}
object PackagePrefix {
def apply(definition: GrpcProtoDefinition): PackagePrefix =
definition.`package`.map(p => s".$p.").getOrElse(".").coerce
}

@newtype class NormalizedTypeName private (val asString: String)
object NormalizedTypeName {
def apply(name: String): NormalizedTypeName =
s".${name.dropWhile(_ == '.')}".coerce
}

def makeDictTypes(p: PackagePrefix, ms: Seq[GrpcRootMessage]): Vector[(NormalizedTypeName, GrpcRootMessage)] =
ms.foldLeft(Vector.empty[(NormalizedTypeName, GrpcRootMessage)]) {
case (b, m @ GrpcMessageSchema(name, _, _, nested, nestedEnums)) =>
(b :+ (p.resolve(name) -> m)) ++
makeDictTypes(p :+ name, nested.getOrElse(Nil)) ++
makeDictTypes(p :+ name, nestedEnums.getOrElse(Nil))
case (b, m @ GrpcEnumSchema(name, _)) =>
b :+ (p.resolve(name) -> m)
}

private val stateNonEmpty: Rule[GrpcStub] =
_.state.exists(_.isEmpty).valueOrZero(Vector("The state predicate cannot be empty"))

Expand Down

0 comments on commit e895004

Please sign in to comment.