Skip to content
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

Merged
merged 12 commits into from
Sep 22, 2023

Conversation

imsdu
Copy link
Contributor

@imsdu imsdu commented Sep 14, 2023

Fixes #4102
image

docs/src/main/paradox/docs/delta/api/practice.md Outdated Show resolved Hide resolved
docs/src/main/paradox/docs/delta/api/practice.md Outdated Show resolved Hide resolved
docs/src/main/paradox/docs/delta/api/practice.md Outdated Show resolved Hide resolved
new ResourcesPracticeRoutes(
identities,
aclCheck,
(project, source, caller) => schemas.createDryRun(project, source)(caller).toBIO[SchemaRejection],
Copy link
Contributor

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

Copy link
Contributor Author

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, ...)

Copy link
Contributor

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

Copy link
Contributor Author

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)

Copy link
Contributor

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

Copy link
Contributor

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?

Copy link
Contributor

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.

Copy link
Contributor

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

Copy link
Contributor Author

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

Comment on lines 37 to 39
/**
* The resource practice routes allowing to do read-only operations on resource
*/
Copy link
Contributor

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?

Copy link
Contributor Author

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

Copy link
Contributor

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

Comment on lines 92 to 102
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)
}
)
}
Copy link
Contributor

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

Copy link
Contributor Author

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

Copy link
Contributor

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

Copy link
Contributor

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

Copy link
Contributor Author

@imsdu imsdu Sep 20, 2023

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

Copy link
Contributor

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

Comment on lines 138 to 144
"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")
}
}
Copy link
Contributor

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

Comment on lines 207 to 208
val unconstrainedEncoded = UrlUtils.encode(schemas.resources.toString)
Get(s"/v1/resources/$projectRef/$unconstrainedEncoded/myId/validate") ~> asAlice ~> routes ~> check {
Copy link
Contributor

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
Copy link
Contributor

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?

Comment on lines 72 to 76
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 ()
Copy link
Contributor

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

@imsdu imsdu changed the title Add resource practice routes Add resource trial routes Sep 20, 2023
Comment on lines 186 to 181
payload.removeKeys(keywords.id).replaceKeyWithValue("number", "wrong").toEntity
payloadWithoutId.replaceKeyWithValue("number", "wrong").toEntity
Copy link
Contributor

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

Copy link
Contributor Author

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

@imsdu imsdu merged commit 16483bc into BlueBrain:master Sep 22, 2023
6 checks passed
@imsdu imsdu deleted the 4102-introduce-resources-practice-routes branch September 22, 2023 08:16
@crisely09
Copy link
Contributor

does this mean that we can perform dry-runs now?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Allow users to perform dry-runs on resources (with optional validation with a schema)
5 participants