Skip to content

Commit

Permalink
Disallow non-sequential version (close #135)
Browse files Browse the repository at this point in the history
  • Loading branch information
oguzhanunlu committed Aug 24, 2023
1 parent ad1d603 commit 85267fe
Show file tree
Hide file tree
Showing 4 changed files with 40 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@ object IgluResponse {
val SchemaInvalidationMessage = "The schema does not conform to a JSON Schema v4 specification"
val DataInvalidationMessage = "The data for a field instance is invalid against its schema"
val NotFoundEndpoint = "The endpoint does not exist"
val NonSequentialSchema = "The schema is not sequential"

case object SchemaNotFound extends IgluResponse
case object SchemaNonSequential extends IgluResponse
case object EndpointNotFound extends IgluResponse
case object InvalidSchema extends IgluResponse
case class SchemaMismatch(uriSchemaKey: SchemaKey, payloadSchemaKey: SchemaKey) extends IgluResponse
Expand Down Expand Up @@ -96,6 +98,8 @@ object IgluResponse {
)
case InvalidSchema =>
Json.fromFields(List("message" -> Json.fromString(DecodeError)))
case SchemaNonSequential =>
Json.fromFields(List("message" -> Json.fromString(NonSequentialSchema)))
case SchemaValidationReport(report) =>
Json.fromFields(
List(
Expand Down Expand Up @@ -129,8 +133,9 @@ object IgluResponse {

implicit val responsesDecoder: Decoder[IgluResponse] = Decoder.instance { cur =>
cur.downField("message").as[String] match {
case Right(NotFoundSchema) => SchemaNotFound.asRight
case Right(NotAuthorized) => Forbidden.asRight
case Right(NotFoundSchema) => SchemaNotFound.asRight
case Right(NonSequentialSchema) => SchemaNonSequential.asRight
case Right(NotAuthorized) => Forbidden.asRight
case Right(Mismatch) =>
for {
uriSchemaKey <- cur.downField("uriSchemaKey").as[SchemaKey]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,16 @@ object VersionCursor {
sealed trait Inconsistency extends Product with Serializable
object Inconsistency {
case object PreviousMissing extends Inconsistency
case object NextRevisionExists extends Inconsistency
case object AlreadyExists extends Inconsistency
case class Availability(isPublic: Boolean, previousPublic: Boolean) extends Inconsistency

implicit val inconsistencyShowInstance: Show[Inconsistency] =
Show.show {
case PreviousMissing =>
"Preceding SchemaVer in the group is missing, check that schemas published in proper order"
case NextRevisionExists =>
"Next revision in the group exists, check that schemas are published sequentially"
case AlreadyExists =>
"Schema already exists"
case Availability(isPublic, previousPublic) =>
Expand All @@ -59,8 +62,7 @@ object VersionCursor {
patchesAllowed: Boolean
): Either[Inconsistency, Unit] =
if (existing.contains(version) && !patchesAllowed) Inconsistency.AlreadyExists.asLeft
else if (previousExists(existing, get(version))) ().asRight
else Inconsistency.PreviousMissing.asLeft
else isVersionAllowed(existing, get(version))

def get(version: SchemaVer.Full): VersionCursor = version match {
case SchemaVer.Full(1, 0, 0) => Initial
Expand All @@ -73,18 +75,25 @@ object VersionCursor {
* Check if existing state allows new schema
* It makes an assumption that `existing` is entirely consistent list without `current` schema
*/
private[model] def previousExists(existing: List[SchemaVer.Full], current: VersionCursor) =
private[model] def isVersionAllowed(
existing: List[SchemaVer.Full],
current: VersionCursor
): Either[Inconsistency, Unit] =
current match {
case Initial => // We can always create a new schema group (vendor/name/1-0-0)
true
().asRight
case StartModel(m) =>
existing.map(_.model).contains(m - 1)
Either.cond(existing.map(_.model).contains(m - 1), (), Inconsistency.PreviousMissing)
case StartRevision(m, r) =>
val thisModel = existing.filter(_.model == m)
thisModel.map(_.revision).contains(r - 1)
Either.cond(thisModel.map(_.revision).contains(r - 1), (), Inconsistency.PreviousMissing)
case NonInitial(version) =>
val thisModel = existing.filter(_.model == version.model)
val thisRevision = thisModel.filter(_.revision == version.revision)
thisRevision.map(_.addition).contains(version.addition - 1)
val thisModel = existing.filter(_.model == version.model)
val thisRevision = thisModel.filter(_.revision == version.revision)
val sequentialAddition = thisRevision.map(_.addition).contains(version.addition - 1)
val nextRevision = thisModel.map(_.revision).contains(version.revision + 1)
if (nextRevision) Inconsistency.NextRevisionExists.asLeft
else if (!sequentialAddition) Inconsistency.PreviousMissing.asLeft
else ().asRight
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ class SchemaService[F[+_]: Sync](
_ <- updateSupersedingVersion(schema.self, Some(s))
response <- Ok(IgluResponse.SupersedingVersionUpdated(schema.self.schemaKey): IgluResponse)
} yield response
case (Left(Inconsistency.NextRevisionExists), _) =>
Conflict(IgluResponse.SchemaNonSequential: IgluResponse)
case (_, Left(error)) =>
Conflict(IgluResponse.Message(error): IgluResponse)
case (Left(error), _) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package com.snowplowanalytics.iglu.server.model

import com.snowplowanalytics.iglu.core.SchemaVer
import com.snowplowanalytics.iglu.server.model.VersionCursor.Inconsistency

class VersionCursorSpec extends org.specs2.Specification {
def is = s2"""
Expand All @@ -25,36 +26,37 @@ class VersionCursorSpec extends org.specs2.Specification {
previousExists rejects new addition if previous does not exist $e5
isAllowed allows overriding schema if patchesAllowed set to true $e6
isAllowed forbids overriding schema if patchesAllowed set to false $e7
previousExists rejects new addition if next revision exists $e8
"""

def e1 = {
val existing = List(SchemaVer.Full(1, 0, 0), SchemaVer.Full(1, 0, 1))
val current = VersionCursor.get(SchemaVer.Full(1, 1, 0))
VersionCursor.previousExists(existing, current) must beTrue
VersionCursor.isVersionAllowed(existing, current) must beRight
}

def e2 = {
val existing = List(SchemaVer.Full(1, 0, 0), SchemaVer.Full(1, 1, 0), SchemaVer.Full(1, 0, 1))
val current = VersionCursor.get(SchemaVer.Full(2, 0, 0))
VersionCursor.previousExists(existing, current) must beTrue
VersionCursor.isVersionAllowed(existing, current) must beRight
}

def e3 = {
val existing = List(SchemaVer.Full(1, 0, 0), SchemaVer.Full(1, 1, 0))
val current = VersionCursor.get(SchemaVer.Full(3, 0, 0))
VersionCursor.previousExists(existing, current) must beFalse
VersionCursor.isVersionAllowed(existing, current) must beLeft
}

def e4 = {
val existing = List(SchemaVer.Full(1, 0, 0), SchemaVer.Full(1, 1, 0), SchemaVer.Full(1, 1, 1))
val current = VersionCursor.get(SchemaVer.Full(1, 1, 2))
VersionCursor.previousExists(existing, current) must beTrue
VersionCursor.isVersionAllowed(existing, current) must beRight
}

def e5 = {
val existing = List(SchemaVer.Full(1, 0, 0), SchemaVer.Full(1, 1, 0), SchemaVer.Full(1, 1, 1))
val current = VersionCursor.get(SchemaVer.Full(1, 1, 3))
VersionCursor.previousExists(existing, current) must beFalse
VersionCursor.isVersionAllowed(existing, current) must beLeft
}

def e6 =
Expand All @@ -64,4 +66,10 @@ class VersionCursorSpec extends org.specs2.Specification {
VersionCursor.isAllowed(SchemaVer.Full(1, 0, 0), List(SchemaVer.Full(1, 0, 0)), false) must beLeft(
VersionCursor.Inconsistency.AlreadyExists
)

def e8 = {
val existing = List(SchemaVer.Full(1, 0, 0), SchemaVer.Full(1, 1, 0))
val current = VersionCursor.get(SchemaVer.Full(1, 0, 1))
VersionCursor.isVersionAllowed(existing, current) must beLeft(Inconsistency.NextRevisionExists)
}
}

0 comments on commit 85267fe

Please sign in to comment.