-
Notifications
You must be signed in to change notification settings - Fork 74
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
Add resource trial routes #4274
Add resource trial routes #4274
Conversation
new ResourcesPracticeRoutes( | ||
identities, | ||
aclCheck, | ||
(project, source, caller) => schemas.createDryRun(project, source)(caller).toBIO[SchemaRejection], |
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.
Having this as a private method in ResourcesPracticeRoutes
would allow getting rid of the constructor all together? Not super important
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 idea is to make unit-testing easier because Schemas
comes with a lot of dependencies (doobie, SHACL engine, ...)
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.
Using functions like this makes general navigation of the code harder, as well as this constructor being unnecessary. It should already be easy to use mock implementations in a unit test
Interface segregation is a good way of handling this sort of situation and will mean you won't need the dependencies you mention
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 am not sure we want to do a remake between OOP and FP in the comments here.
As long as we have the apply
method here, I don't think it hurts navigability.
The FP way allows better expressivity and better flexibility than introducing a multitude of interfaces.
On the flexibility side, what happens if in another case, you want to generate a schema but only care about one of the fields or just if it succeeds ? Or something else ?
You would have to introduce new interfaces or have your mocks to know about the result before dropping the info.
(Those are just simple examples, there could be others)
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.
Every time I want to see what code actually run when we are passing one of these functions, I have to navigate multiple steps and potentially multiple branches. That is definitely harder than just clicking on show implementations
In this instance, using a function instead of an interface gives us nothing and costs a lot, which is why I'm saying we should change it 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.
Also I don't believe there is a conflict between OOP and FP here, I'm not sure how using an interface makes something not functional?
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.
Interesting discussion! In these kinds of situations we've found single-method traits to be quite a good compromise in the past. They essentially act as just named functions, sort of like a type alias but first-class. Navigation is still easy and Scala's de-sugaring makes them simple to stub.
For example if a dependent expects a trait MyTrait { def foo(i: Int): String }
, you can pass it as a stub like (i: Int) -> "hello"
in tests and the compiler will understand it needs to do the whole new MyTrait { ... }
bit for you, which is nice. In Scala 3 there's even less boilerplate needed (better type inference).
Not sure I understand this bit you want to generate a schema but only care about one of the fields or just if it succeeds ?
- what would that look like? (Just trying figure out what I might do in that situation)
As an aside, in Haskell there are a number of approaches to this type of dependency injection. You might do a type alias, newtype wrapper round the function, or a type class (if doing tagless final). Otherwise type signatures become very hard to read when containing lots of functions passed as arguments. The single-method trait isn't that different from the type class pattern as I see it. (Well really all of these approaches are pretty similar 😛 )
The "method dictionary" pattern can be quite neat too - you basically have a bunch of newtype wrapped functions stored in some Env
data type, and access them by the record accessor (just the thing inside the newtype). For convenience you might also have syntax to pull out the functions and run them in one fell swoop. In this case that may require quite a big refactor though so I'm not sure.
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 only downside with SAM (the example (i: Int) -> "hello"
from above) is that Intellij doesn't really understand that it's an implementation of the interface from a code-navigation perspective. I suppose mostly we would only use SAM in tests, where it isn't so necessary to have the navigation functionality
In production code it's just nice to easily see what the dependency actually does
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 would love to be able to use a SAM here and we should use them where we can.
The Schemas module is complex and contains a collection of operations though (with like 3 operations to create a schema) and the part which is mentioned is only used in this context (so far).
As it is a matter that goes further than the context of this PR, we should rather discuss that when we are all in the office
delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesPracticeRoutes.scala
Outdated
Show resolved
Hide resolved
delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesPracticeRoutes.scala
Outdated
Show resolved
Hide resolved
/** | ||
* The resource practice routes allowing to do read-only operations on resource | ||
*/ |
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 would question whether "practise" makes sense in this context
Also it's not really that you're doing read-only operations, it's that you are performing dry-runs of write operations?
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.
They are not, the two operations do not go through the state machine because they don't result in an event and a new state to be persisted in the primary store
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.
Maybe there is another way to phrase it, but I think this comment is misleading
private def generate(project: ProjectRef, input: GenerationInput)(implicit caller: Caller) = | ||
input.schema match { | ||
case ExistingSchema(schemaId) => | ||
emit(resourcesPractice.generate(project, schemaId, input.resource).flatMap(_.asJson)) | ||
case NewSchema(schemaSource) => | ||
emit( | ||
generateSchema(project, schemaSource, caller).flatMap { schema => | ||
resourcesPractice.generate(project, schema, input.resource).flatMap(_.asJson) | ||
} | ||
) | ||
} |
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 feels like this is too much logic for routes, it could go int the ResourcesPractise instead
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.
Those are 10 lines of code here without complexity where:
- Most of it is related to how we handle the input
- ResourcesPractis(c)e is about generating resources, not schemas
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 function is unrelated to parsing or response-writing so it should be in a service rather than routes, but that's just my opinion I guess
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.
Any time the user calls an API they are ostensibly performing a single action. The user is saying they want to generate using a generated schema. Perhaps that affects they way you design the ResourcesPractise, but it shouldn't really be a concern for the routes
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 function is unrelated to parsing or response-writing so it should be in a service rather than routes, but that's just my opinion I guess
It is related to parsing as GenerationInput
only lives in the scope of the routes and reflects the payload submitted to the user.
Any time the user calls an API they are ostensibly performing a single action. The user is saying they want to generate using a generated schema. Perhaps that affects they way you design the ResourcesPractise, but it shouldn't really be a concern for the routes
We are talking here about a pattern matching and a flatMap operation and they live in a private method that does not pollute the route declaration.
I prefer this solution than changing ResourcePractise
or including yet another layer
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.
All I can say is that I see unnecessary logic in the routes a lot and it would be better if it weren't there
"generate a resource without passing a schema" in { | ||
val payload = json"""{ "resource": $validSource }""" | ||
Get(s"/v1/practice/resources/$projectRef/", payload.toEntity) ~> asAlice ~> routes ~> check { | ||
response.status shouldEqual StatusCodes.OK | ||
response.asJson shouldEqual jsonContentOf("practice/resource-without-schema.json") | ||
} | ||
} |
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.
Why should passing this payload return this result? If this test fails we should know what the code is intended to do
val unconstrainedEncoded = UrlUtils.encode(schemas.resources.toString) | ||
Get(s"/v1/resources/$projectRef/$unconstrainedEncoded/myId/validate") ~> asAlice ~> routes ~> check { |
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.
You could use a name like unconstrained
here, or maybe unconstrainedSchema
if that makes more sense
"fail to create a resource against a schema that does not exist" in { | ||
Put( | ||
"/v1/resources/myorg/myproject/pretendschema/wrong", | ||
payload.removeKeys(keywords.id).replaceKeyWithValue("number", "wrong").toEntity |
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.
Why is this line needed?
practice.generate(projectRef, resourceSchema, source).map(_.value).assert(expectedData) | ||
for { | ||
generated <- practice.generate(projectRef, resourceSchema, source) | ||
_ = assertEquals(generated.schema, None) | ||
_ = assertEquals(generated.attempt.map(_.value), Right(expectedData)) | ||
} yield () |
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.
Having to re-write the existing test when you add a second assertion suggests that it's probably better to write this way initially
payload.removeKeys(keywords.id).replaceKeyWithValue("number", "wrong").toEntity | ||
payloadWithoutId.replaceKeyWithValue("number", "wrong").toEntity |
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.
After looking at this for a while I can see now that if you set the number field to be a string, presumably that breaks validation. It's not very obvious though, because there is nothing to suggest that is what the schema checks. With this and other tests, it's possible to make it clearer
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 tried to make it a bit clearer, note that this was some existing test code, that I only refactored a bit
…e-routes' into 4102-introduce-resources-practice-routes
does this mean that we can perform dry-runs now? |
Fixes #4102