Switch to ValidatedNel / Validated instead of Either#369
Conversation
29bc4e5 to
03dc907
Compare
This makes much of the code a bit cleaner and easier to reason about and in come cases means that we can return multiple errors. As a bonus some of the Json reads code becomes a little easier to reason about as well. This adds an API endpoint to which YAML can be sent for validation and some very rudimentary web pages that let you do the same (your eyes will hurt!)
03dc907 to
4b83d6e
Compare
| def :::(other: ConfigErrors) = ConfigErrors(other.errors ::: errors) | ||
| def ::(other: ConfigError) = ConfigErrors(other :: errors) | ||
| object ConfigError { | ||
| def nel(context: String, message: String) = NonEmptyList.of(ConfigError(context, message)) |
There was a problem hiding this comment.
No - have removed - aliasing NonEmptyList helps.
| _ <- validateDependencies(label, deployment, config.deployments).right | ||
| } yield deployment | ||
| } | ||
| applyTemplates(label, rawDeployment, config.templates).andThen { templated => |
There was a problem hiding this comment.
Not enamoured with the layout of this, makes it needlessly hard to see the steps having the andThen calls all over the place. Nice thing about for comps is they make that so clear.
:minor_point:
| private[input] def resolveDeployment(label: String, templated: DeploymentOrTemplate, globalStacks: Option[List[String]], globalRegions: Option[List[String]]): ValidatedNel[ConfigError, Deployment] = { | ||
| (Validated.fromOption(templated.`type`, ConfigError.nel(label, "No type field provided")) |@| | ||
| Validated.fromOption(templated.stacks.orElse(globalStacks), ConfigError.nel(label, "No stacks provided")) |@| | ||
| Validated.fromOption(templated.regions.orElse(globalRegions), ConfigError.nel(label, "No regions provided"))) map { (deploymentType, stacks, regions) => |
There was a problem hiding this comment.
I think this bit should also validate the lengths of stacks and regions are > 0 so you can match on stackHead :: stacks to avoid the unsafe NEL creation. Or some such*.
- disclaimer: other approaches exist
There was a problem hiding this comment.
For better or worse I've pimped Option[List] to provide this niceness.
| deploymentWithActions <- resolveDeploymentActions(deployment, deploymentType).right | ||
| deployment <- verifyDeploymentParameters(deploymentWithActions, deploymentType).right | ||
| } yield deployment | ||
| Validated.fromEither(result).leftMap(NonEmptyList.of(_)) |
There was a problem hiding this comment.
Does leftMap(NonEmptyList.of) not typecheck?
| import magenta.input._ | ||
| import magenta.{DeployParameters, DeploymentResources} | ||
|
|
||
| object Resolver { |
| val deploymentTypeEither = deploymentTypes.find(_.name == deployment.`type`). | ||
| toRight(ConfigError(deployment.name, s"Deployment type ${deployment.`type`} not found")) | ||
| val validatedDeploymentType = Validated.fromOption(deploymentTypes.find(_.name == deployment.`type`), | ||
| NEL.of(ConfigError(deployment.name, s"Deployment type ${deployment.`type`} not found"))) |
There was a problem hiding this comment.
This seems to me to be more sensible than ConfigError.nel (see above).
There was a problem hiding this comment.
Yep - that was before I started aliasing NonEmptyList => NEL in all the imports. Am thinking about a global type alias.
| * Two functions are pimped onto Validated for the purposes of testing and if the value is not of the expected type | ||
| * then the test will fail detailing what the unexpected value was. | ||
| */ | ||
| trait ValidatedValues { |
There was a problem hiding this comment.
Nice.
By analogy with EitherValues this would be .valid.value. I realise that's mainly an artifact of Either's projections but the .value is the common bit (see also OptionValues). The trait scalatest has for Future is called Futures, presumably because it doesn't have the value accessor syntax.
:minor_point:
There was a problem hiding this comment.
I've left it as I can't think of an alternative.
| case Invalid(errors) => | ||
| Json.obj( | ||
| "response" -> Json.obj( | ||
| "status" -> "ok", |
There was a problem hiding this comment.
Is the status really "ok" here?
There was a problem hiding this comment.
Yes. I think so. It has successfully given you a validation result. That result might be a failure, but it is still a 200 response which in the API world is OK. See the case above where you don't even give it valid data to validate in which case you do get an error.
| @(implicit request: Security.AuthenticatedRequest[AnyContent, com.gu.googleauth.UserIdentity], errors: NonEmptyList[ConfigError]) | ||
|
|
||
| @main("Validation errors", request) { | ||
| <div class="clearfix"><p> </p></div> |
There was a problem hiding this comment.
The frontend needs some attention. It has that everywhere.
There was a problem hiding this comment.
I think you really need a spacer.gif to get into the flavour.
d62b6bb to
d1365e1
Compare
philwills
left a comment
There was a problem hiding this comment.
This generally looks good, but probably should talk through some of it in person.
| def resolve(config: RiffRaffDeployConfig): List[Either[ConfigError, Deployment]] = { | ||
| config.deployments.map { case (label, rawDeployment) => | ||
| def resolve(config: RiffRaffDeployConfig): ValidatedNel[ConfigError, List[Deployment]] = { | ||
| config.deployments.traverseU[ValidatedNel[ConfigError, Deployment]] { case (label, rawDeployment) => |
There was a problem hiding this comment.
scala/scala#5102 means that the weird U bit will go away in 2.12.
| regions <- templated.regions.orElse(globalRegions).toRight(ConfigError(label, "No regions provided")).right | ||
| } yield { | ||
| private[input] def resolveDeployment(label: String, templated: DeploymentOrTemplate, globalStacks: Option[List[String]], globalRegions: Option[List[String]]): ValidatedNel[ConfigError, Deployment] = { | ||
| (Validated.fromOption(templated.`type`, NEL.of(ConfigError(label, "No type field provided"))) |@| |
There was a problem hiding this comment.
I think a custom error type with it's own Semigroup might avoid the littering of NEL.of.
There was a problem hiding this comment.
Let me give that a bash.
| } yield { | ||
| private[input] def resolveDeployment(label: String, templated: DeploymentOrTemplate, globalStacks: Option[List[String]], globalRegions: Option[List[String]]): ValidatedNel[ConfigError, Deployment] = { | ||
| (Validated.fromOption(templated.`type`, NEL.of(ConfigError(label, "No type field provided"))) |@| | ||
| Validated.fromOption(templated.stacks.orElse(globalStacks), NEL.of(ConfigError(label, "No stacks provided"))) |@| |
There was a problem hiding this comment.
It tend to find mapN easier to talk about than |@|, but that's definitely personal preference.
There was a problem hiding this comment.
I had a play with that and couldn't see how to get it to work and can't find any examples. Seems to work for higher kinded types with one type parameter rather than two or more or have I missed something?
| parameters = templated.parameters.getOrElse(Map.empty) | ||
| name = label, | ||
| `type` = deploymentType, | ||
| stacks = NEL.fromListUnsafe(stacks), |
There was a problem hiding this comment.
Couldn't this Unsafe be avoided by lifting into the NEL within the Validated clause?
e.g.
Validated.fromOption(templated.stacks.orElse(globalStacks).flatMap(NEL.fromList), NEL.of(ConfigError(label, "No stacks provided")))|
|
||
| package object resolver { | ||
| implicit class RichValidated[E, A](validated: Validated[E, A]) { | ||
| def flatMap[EE >: E, B](f: A => Validated[EE, B]): Validated[EE, B] = validated.andThen(f) |
There was a problem hiding this comment.
I think there are some quite good reasons for not calling this flatMap
There was a problem hiding this comment.
The reason it is here is to make for comprehensions work and eliminate the uses of andThen across the codebase.
d1365e1 to
a5b8e5c
Compare
- pimping Validated with `flatMap` means we can write much clearer code by switching to for comprehensions and avoid the use of `andThen` - using traverseU means that we don't have to produce singleton lists and labouriously combine them Leaving the use of andThen in the tests.
a5b8e5c to
ff600b3
Compare
|
OK - I've made some more changes to move from |

This makes much of the code a bit cleaner and easier to reason about and in come cases means that we can return multiple errors. As a bonus some of the Json reads code becomes a little easier to reason about as well.
This adds an API endpoint to which YAML can be sent for validation and some very rudimentary web pages that let you do the same (your eyes will hurt!)
Would be interested in doing a code review of all of this in person with anyone who has used cats before.