-
Notifications
You must be signed in to change notification settings - Fork 22
better manage default values #111
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
Conversation
@antmendoza very nice!! |
@tsurdilo this is not automatically generated, moving from interfaces to classes and all the annotations... we have to do it by hand. Classes we could generate automatically probably, but all the code around I don't think so. Default values are set in the method from Constant are set in the constructor https://github.com/antmendoza/sdk-typescript/blob/poc_/src/poc/poc-databasedswitch.ts#L47 For default values and constant I can investigate and see if we could have something like
but still we need to have some code to handle "oneOf" and conditional required fields https://github.com/antmendoza/sdk-typescript/blob/poc_/src/poc/poc-injectstate.ts#L77 |
@antmendoza I'm very grateful for the huge amount of work you put into this but I must admit I'm quite confused. I didn't look deep enough into it but at first glance I have two concerns:
Other questions/remarks that are more tangible:
I'm really sorry if I missed something or if the critic is a bit harsh, once again, your work is much appreciated. |
Hi @JBBianchi no problem, thanks for your comments,
It is not, I am not sure if it is gonna be easy but from my point of view having the types generated and builders with validation is enough, I don't think that we are having the specification changing with a new version every month. I don't see a problem having things done by hand as long as it is working.
Neither I am a big fan of having this library here but I have not found any better way, I will spend some more time this weekend on it. I am still exploring this library but When we deserialize a string we can populate default values, I don't know how to achieve this with
That implementation was passing the test, but I think that a better approach is this one . I have changed it now. As I have mention before, by calling plainToClass invoque de
I think that default should not be populated in serialization, only deserialization (#108 (comment) ). Please @tsurdilo correct me if I am wrong.
Builder provide a fluent api and validation as you created it. I think that both are compatibles, in the same way we have now types exposing properties and builders. |
I think I kinda see the interest of /** Requirements
const isObject = (value: any): boolean => {
if (!value) return false;
const type = typeof value;
return type === 'object' && !Array.isArray(value);
}
// this is a quick & dirty trick, doesn't handle dates, regexp, symbols...
const deepCopy = (object: any): any => {
return JSON.parse(JSON.stringify(object));
}
class Hydrator {
constructor(model?: any) {
if (model && isObject(model)) {
Object.assign(
this,
deepCopy(model)
);
}
}
}
**/
class PocWorkflow extends Hydrator {
// ...props declarations...
constructor(model?: any) {
super(model);
// set defaults
this.expressionLang = this.expressionLang || 'jq';
if (model) {
// hydrate non primitive properties
if (model.states?.lenght) {
this.states = this.model.states.map(s => {
switch (s.type) {
case 'inject':
return new PocInjectstate(s);
case 'subflow':
return new PocSubflowstate(s);
case 'switch':
return new PocDatabasedswitch(s);
}
});
}
}
}
static fromString(value: string): PocWorkflow {
return new PocWorkflow(yaml.load(value));
}
static toYaml(workflow: PocWorkflow): string {
validate('Workflow', workflow);
const clone = deepCopy(workflow);
// we don't want to output the defaults
if (clone.expressionLang === 'jq') {
delete clone.expressionLang;
}
return yaml.dump(clone);
}
} Anyway, my biggest concern still is the incompatibility with the current approach. From my understanding, the PoC classes have to define the properties (so they can be decorated for instance) and they can declare their own validation function. This means that the generated types & builders are of no use anymore. It would instead require to manually create "PoCBuilders" which kinda fallback to the first approach and its disadvantages. In conclusion, we obviously don't have the constraints introduced by automatic generation when building everything manually. The question is(are) do we want the freedom at the cost of more work or ease of maintenance (and also, can we manage defaults with the current approach). |
@tsurdilo your input is also welcome knowing it's more of a conceptual/philosophical aspect rather than a technical one. |
I usually prefer the way you are proposing here, but this time I think that it is more practical to use an external library that is doing the work for us, this or any other one, due to the amount of builders and validation, I really like the way you implemented it and I don't think they are not used, instead I have copied the same code into the POC implementation Having a separate class for builders? Idk to be honest, but still your builder proxy can be reused in this case. I am also copying the code from interfaces into classes, so having the interfaces and types generated automatically is also used here (and very useful and time saving btw) Moving from interfaces to classes is 1 minute each I would say, and adding the decorators less than 5 minutes once you have done a couple of them. We still have to add code into the fn function because the oneOf conditions. (not sure if this is the right place) I don't think that after version 1 of the specification we will be having a lot of changes in the specification. The issue with the actual implementation is the amount of I might be wrong, for sure 🙄 anyway, if this behaviour is not required for the sdks this conversation does not have any sense. |
I see your point and I think your solution is smart but still, my spider sense is tingling, something feels off.
Please don't say that, you're working hard and providing a solution and that's great, thanks a lot! It is not a trivial problem and none of us have a solution. We are all at the same point, nobody is right or wrong, just trying to figure how out to make things work the best. I'm challenging the choice for |
I am a fan of generating concrete classes rather than interfaces because you can extend them. From the perspective of the majority of people writing their dsl in json/yaml it would be nice if it's possible to just do:
and then allow for getters and setters for everything. If builders are a must thats fine too but if possible to reduce it to the least amount of code user has to write. I don't really think that validation needs to be necessarily built into the core classes. It could be externalized into its own class if that makes things easier. it could be its own module that can be added separately like its with the java sdk. just ideas. For defining workflow using the object model, I like the way you have it here. Only thing that if possible to keep the amount of "build()" calls to a minimum as thats just noice for users imo. Overall I am super impressed by typescript sdk and don't worry too much I think it will evolve over time. |
@tsurdilo From my current knowledge state, builders are required with the @antmendoza I had a deeper look and I think this additionnal layer brings a some confusion. The PocWorkflow (or PocSomething) One thing that was pointed to me on the JSON Schema Slack is the option to add the default values when validating: https://ajv.js.org/guide/modifying-data.html#assigning-defaults This means we could output the default values when needed by passing a flag to |
Is this due to limitations of schema generators out there? Otherwise I don't really see this statement as a valid point honestly, sorry :) We were able to solve this with Java SDK by for example:
We also created custom interfaces for generic types, rather than trying to generate them: https://github.com/serverlessworkflow/sdk-java/tree/main/api/src/main/java/io/serverlessworkflow/api/interfaces Same thing for Function defs events, etc. Having this "custom" solution is a little bit of a setup but in end allows us to work around limitations of schema validators which is also the case in the Java world as well.
So there are ways to get around limitations IMO, but I understand the TypeScript world is quite different so maybe there are more obstacles there. Idk. |
@JBBianchi i mean worst case scenario there are even things like this - https://www.habarta.cz/typescript-generator/maven/typescript-generator-maven-plugin/generate-mojo.html we could spit out typescript from the currently generated Java classes in the java-sdk :) idk if that would even be an option but just saying that if we are running into the same issues in typescript sdk but are too hard to solve there are always options :) |
@tsurdilo there is also a workflowValidator class, where we have to add the workflow integrity validation. @JBBianchi I don't have any problem having the builders in a separate class, on the contrary.
I was just focusing in the output. We can change it (here and here) and remove the constructor. Now both (builder and fromString) rely on decorators. I might agree that it is not the best solution we could reach but it's a solution that give us the behaviour we want. As @tsurdilo said once we have the API we can change the implementation. My plan is to continue with this implementation (by hand) and see: 1)if it works; 2) if we agree an API. is it ok? |
@tsurdilo indeed, I never said it was impossible, I just said I reached the limitation of what I know. @antmendoza sure, I don't see why not. I would advice to double check your methodology with v0.7 of the spec. Nevertheless, bare in mind that going for classes vs types fixes some problems but introduce others, or at least drawbacks. Take Action for instance, with the type definition it clearly defines you can have either export class Action {
name?: string;
functionRef?: Functionref;
eventRef?: Eventref;
timeout?: string;
actionDataFilter?: Actiondatafilter;
} Which means you'd only know at runtime, when validating the object, that either |
Hi we have most of the code for this PR auto generated, > 95% I would say. There is a lot of snippets with if.. else, I have to clean it a little bit and make more maintainable. A couple of things, builder is a method inside each class, I would prefer to have in on a different class? wdyt? For union types, as @JBBianchi has already mention we don't have validation at compile time, we will have to relies in the And for union types like End, Startdef, Funciontref, Crondef..., I have moved the union to the property declaration |
or we can use the funcions we already have. |
I think the functions are enough, I don't really see the use for additionnal class. |
👍 |
Quick update. the So I have implemented the solution more or less in the way that @JBBianchi proposed here, thanks btw. I will create a PR today. |
Many thanks for submitting your Pull Request ❤️!
What this PR does / why we need it:
@tsurdilo @JBBianchi
This is a POC to get feedback about this implementation, that allows us to have more control on what is been serialized/deserialized
We are using here https://github.com/typestack/class-transformer/ to convert from json to object and we have to enable experimental mode to use decorators/annotations (I still have to read more about decorators and possible inconveniences)
The actual implementation is under the folder poc, https://github.com/antmendoza/sdk-typescript/tree/poc_/src/poc, I have named every new class as PocXXX to don't confuse myself, the Poc prefix have to be removed and the idea is to convert each of the current interfaces to a class.
As you will see every class has a
builder()
method that allows us to have the same fluent api as before as well as validation against json schema as before (it is pending to add logical validation)We have to annotate attributes ( example here and here) and in some cases (like the funcions attribute) add logic, to have nested object converted.
The actual change involve:
build()
method to the new class + validation, that means the implementation of the current builders (and remove the current builders)I have written some tests that demonstrate the actual implementation.
Please let me know your thoughts.