-
Notifications
You must be signed in to change notification settings - Fork 18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this useful?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FIXED!
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For better or worse I've pimped Option[List] to provide this niceness.
deploymentType <- availableTypes.find(_.name == deployment.`type`) | ||
.toRight(ConfigError(deployment.name, s"Unknown type ${deployment.`type`}")).right | ||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does leftMap(NonEmptyList.of)
not typecheck?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sadly not.
import magenta.deployment_type.DeploymentType | ||
import magenta.graph.{DeploymentTasks, Graph} | ||
import magenta.input._ | ||
import magenta.{DeployParameters, DeploymentResources} | ||
|
||
object Resolver { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ow my head
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to me to be more sensible than ConfigError.nel
(see above).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the status really "ok" here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
:-0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The frontend needs some attention. It has that everywhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you really need a spacer.gif to get into the flavour.
d62b6bb
to
d1365e1
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It tend to find mapN
easier to talk about than |@|
, but that's definitely personal preference.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there are some quite good reasons for not calling this flatMap
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.