-
Notifications
You must be signed in to change notification settings - Fork 22
moved from types to classes #114
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
moved from types to classes #114
Conversation
Signed-off-by: Antonio Mendoza Pérez <[email protected]>
this is not related with the rest of the changes, I can create another PR for this if you want |
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.
Impressive work @antmendoza, congratz! There are still few things to address, the "bigger" one being the "hydrating" of the non primitive types in the constructors.
src/lib/builders/action-builder.ts
Outdated
const result = {} as Specification.Action; | ||
|
||
Object.assign(result, data); | ||
validate('Action', result); | ||
return result; |
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 the building functions should rely on the constructor of their underlying type.
For instance:
const model = new Specification.Action(data); // I don't like `result`, it's not "self describing", but that's just a little detail, not very important
validate('Action', model );
return model;
This keeps the creation of the object and the defaulting of the properties DRY. If you take the example of callbackstate-builder, it does the same think than it's constructor counterpart. Furthermore, using a constructor instead of an object literal allows the use of instanceof
(aka runtime typechecking in addition to compile time typechecking):
const foo = new Specification.Action(); // This means "create a new instance of Action"
const bar = {} as Specification.Action; // This means to TypeScript only "consider this object literal as Action"
console.log(foo instanceof Specification.Action); // true, the object is "typed" at runtime
console.log(bar instanceof Specification.Action); // false, this is just an object literal
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.
thanks @JBBianchi
I can change the property name to model, no problem
The problem using constructor is that the code in the actual constructor is setting values we don't want in the final serialized json for example https://github.com/antmendoza/sdk-typescript/blob/from-types-to-classes/src/lib/definitions/function.ts#L18, we don't want type:rest if the client has not set it. Instead we want the value on deserialization.
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.
Indeed, and I don't really see how to prevent that... Maybe with a prepareSerialization()
method or something that would need to be called on the whole object tree to mutate the serialized copy but that doesn't sound right... Or maybe using class-transformer for that? But that feels wrong as well :/
const result = { | ||
type: 'event', | ||
} as Specification.Eventstate; | ||
|
||
if (!data.end && !data.transition) { | ||
result.end = true; | ||
} | ||
|
||
Object.assign(result, data); |
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.
In relation to my previous comment, this should be done in the constructor and be kept in one place only.
const result = { | ||
type: 'delay', | ||
} as Specification.Delaystate; | ||
|
||
Object.assign(result, data); | ||
validate('Delaystate', result); | ||
return result; |
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.
Handling of usedForCompensation
is missing?
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.
Since usedForCompensation is required now I don't think we have to set the value, I am thinking now that setting the value useForCompensation
in the constructor is not needed, I will have a look
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 might not be useful indeed, it should validate/be considerated false by default without having to specify it. @tsurdilo do you agree?
@@ -19,7 +19,7 @@ To build the project and run tests locally: | |||
```sh | |||
git clone https://github.com/serverlessworkflow/sdk-typescript.git | |||
cd sdk-typescript | |||
npm install && npm run update-code-base && npm run test |
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.
With or without running the code generation should produce the same output...
src/lib/definitions/action.ts
Outdated
|
||
export class Action { | ||
constructor(model: any) { | ||
const result = {}; |
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.
Use the name defaultModel
instead of result
? It's more descriptive?
src/lib/definitions/subflowstate.ts
Outdated
|
||
export class Subflowstate { | ||
constructor(model: any) { | ||
const result = { |
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.
Missing type: 'subflow'
(after Object.assign to enforce the const event if something else is specified in model ?)
/** | ||
* State type | ||
*/ | ||
type?: 'subflow'; |
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.
type is required
src/lib/definitions/workflow.ts
Outdated
const functions = this.functions; | ||
if (typeof functions === typeof []) { | ||
this.functions = (functions as Function[]).map((f) => new Function(JSON.stringify(f))) as Functions; | ||
} |
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 don't understand the copy of the reference of function neither the stringify in the Function
constructor.
I thing I would go for:
if (typeof model.functions !== typeof '') {
this.functions = (model.functions as Function[]).map((f) => new Function(f)) as Functions;
}
but maybe I'm missing something?
Furthermore, I think this should ideally be done for all properties that are not primitive types, in all the definitions... That's the only way to properly hydrate the models I think.
src/lib/definitions/workflow.ts
Outdated
[key: string]: any; | ||
}; | ||
|
||
const states = this.states; |
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.
Same, I don't think copying the reference really makes sense.
src/lib/definitions/workflow.ts
Outdated
metadata?: /* Metadata information */ Metadata; | ||
static fromSource(value: string): Specification.Workflow { | ||
try { | ||
return yaml.load(value) as Specification.Workflow; |
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.
Should be wrapped into the constructor:
return new Specification.Workflow(yaml.load(value));
After reading all your comments @JBBianchi most of then are related to setting default values and constants and I think that everything can be merged in the constructor, as you've mentioned 👍 |
By using the constructor in the builders makes the test fail, since serialized workflow (json file) do not has set default values like see antmendoza@86d4583#diff-c590f689b760b021149371854a8e8dcf45b515f719c7a8abfd5dafea608cf7f2R136 |
If you use constructors all the way down, even when deserializing, it should be ok, shouldn't it? |
deserializing is working ok, serializing is when we are populating default values we don't want to be in the final serialized string as you mention before, we have to modify the object before serialize it, the place I can see now to do it is in the xxxBuildingFn event if I don't like it.
having this logic here is weird to me, anyway. (I am thinking that the proper way might be having two serilize/deserialize methods or classes per type, as they have specific logic ) Let's make this works and we will see later, as the API should not change I am going to modify the code in this way and see if everything works as expected |
Ah yes, sorry, I mixed things up. Serializing is indeed a bit tricky (maybe even more than "a bit")... export class Workflow {
makeSerializable() {
const clone = deepCopy(this);
// managing self defaults
if (clone.expressionLang === 'jq') {
delete clone.expressionLang;
}
// deeply removing defaults
if (typeof clone.start !== typeof '') {
clone.start = clone.start.makeSerializable();
}
if (clone.execTimeout) {
clone.execTimeout = clone.execTimeout.makeSerializable();
}
//...
clone.states = clone.states.map(s => s.makeSerializable());
return clone;
}
static toJson(workflow: Workflow): string {
validate('Workflow', workflow);
return JSON.stringify(workflow.makeSerializable());
}
static toYaml(workflow: Workflow): string {
validate('Workflow', workflow);
return yaml.dump(workflow.makeSerializable());
}
} |
I don't think we should delete always default values, only if the user/client has not set them |
How to differentiate the two use cases? |
We can't, to make it work I am re-setting the value after creating the object, in the builder. Pretty ugly, I know.
I am testing also the implementation by "hidratating" non-primitive object and I think that it is not enough, since non-primitive properties are created as Object type
so for each non primitive property I am reseting it by instantiating the corresponding class |
I don't understand why you'd set type again here,
In the current state of workflow.ts, I don't see the hydration of if (model.events && typeof model.events !== typeof '') {
this.events = model.events.map(e => new Eventdef(e));
} Another concern I just thought about: what if the user sets a property outside the constructor... ? const wf = new Workflow();
wf.events = [
{
name: "CarBidEvent",
type: "carBidMadeType",
source: "carBidEventSource"
}
]; This would assign a plain object to the The workaround would be to define the properties with getters/setters to hydrate objects instead of doing it in the constructor ... export class Workflow {
private _events?: Events;
get events(): Events {
return this._events;
}
set events(value: Events) {
if (value && typeof value !== typeof '') {
this.events = value.map(e => new Eventdef(e));
}
else {
this.events = value;
}
}
constructor(model: any) {
const defaultModel = { expressionLang: 'jq' } as Specification.Workflow;
Object.assign(this, defaultModel, model);
}
} but that's a lot of hassle... |
Signed-off-by: Antonio Mendoza Pérez <[email protected]>
Signed-off-by: Antonio Mendoza Pérez <[email protected]>
I have added a test @JBBianchi hope this helps |
Sorry, it's Monday morning, maybe I'm not awake but I still don't understand. "In memory" objects should have the default value... This line of function-builder is redundant: model.type = data.type; It's already assigned when calling: const model = new Specification.Function(data); As the function constructor already assign the values: Object.assign(this, defaultModel, model); In other words: const fn1 = new Function({ name: 'function', operation: 'operation' }); // fn1.type === 'rest'
const fn2 = functionBuilder().name('function').operation('operation').build(); // fn2.type === 'rest'
const fn3 = new Function({ name: 'function', operation: 'operation', type: 'grpc' }); // fn3.type === 'grpc'
const fn4 = functionBuilder().name('function').operation('operation').type('grpc').build(); // fn4.type === 'grpc'
const fn5 = new Function({ name: 'function', operation: 'operation', type: 'rest' }); // fn5.type === 'rest'
const fn6 = functionBuilder().name('function').operation('operation').type('rest').build(); // fn6.type === 'rest'
fn1.makeSerializable(); // { name: "function", operation: "operation" }
fn2.makeSerializable(); // { name: "function", operation: "operation" }
fn3.makeSerializable(); // { name: "function", operation: "operation", type: 'gprc' }
fn4.makeSerializable(); // { name: "function", operation: "operation", type: 'gprc' }
fn5.makeSerializable(); // { name: "function", operation: "operation" } <-- default is gone even if the user specified it
fn6.makeSerializable(); // { name: "function", operation: "operation" } <-- default is gone even if the user specified it Whenever the user set the default value or not, the value exists in memory but shouldn't get outputted in the serialized object I think. It's "not possible" to flag whenever the user actually set the property and still output the value in the serialized object if he did. At least, I don't think it's necessary/makes sense. |
It is Monday for everyone @JBBianchi I am not sure but I think that if the user set the value, the value should be outputted ,at least that would be the behaviour I was expecting as a client using the library. As far I have seen, this only is happening with |
Signed-off-by: Antonio Mendoza Pérez <[email protected]>
I think |
👍 Thank you @JBBianchi , I am going to have a look |
Signed-off-by: Antonio Mendoza Pérez <[email protected]>
@JBBianchi I have added a couple of test for subflowstateBuilder and eventstateBuilder how do you think this should work ? |
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.
@JBBianchi I have added a couple of test for subflowstateBuilder and eventstateBuilder
how do you think this should work ?
I think the property assertion "toBeUndefined" are not valid, they should reflect the default value from what I understood from @tsurdilo guidelines.
Then, following my previous train of thoughts, the lines expect(JSON.stringify(object)).toBe(
should be expect(JSON.stringify(object.makeSerializable())).toBe(
import { eventstateBuilder } from '../../../src/lib/builders/eventstate-builder'; | ||
import { oneventsBuilder } from '../../../src/lib/builders/onevents-builder'; | ||
|
||
describe('subflowstateBuilder ', () => { |
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.
eventstate builder
.transition('Get Book Status') | ||
.build(); | ||
|
||
expect(object.exclusive).toBeUndefined(); |
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 guess this should be true (https://github.com/serverlessworkflow/specification/blob/0.6.x/schema/workflow.json#L535)
it('should build an object', () => { | ||
const object = subflowstateBuilder().name('StartApplication').workflowId('startApplicationWorkflowId').build(); | ||
|
||
expect(object.waitForCompletion).toBeUndefined(); |
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.
Ok, sorry I miss all the defaults when get rid of 'class-transformer' so putting this on code, the behaviour should be something like
|
I think that's the idea. I don't know if you need a |
I think we should move serialize/deserialize functions to another class, Making them optionals does not seem the solution. BTW I am not sure that deserialize is needed |
I'm not sure to understand what you mean by "the compiler is complaining all the time" but I'm not against having dedicated classes for those. Note that |
Signed-off-by: Antonio Mendoza Pérez <[email protected]>
Added default values to each constructor and normalize method when required, to delete recursively properties whose value = default value |
Signed-off-by: Antonio Mendoza Pérez <[email protected]>
@tsurdilo @JBBianchi this PR is ready to be reviewed. @JBBianchi since we are creating object for each non-primitive property in the constructor , do you thing that hydration is still needed? |
You did an amazing job @antmendoza, congratz! 👏
I'll try to digg into the details and review asap. I did a quick test and a very fast look around, it seems great. Maybe a detail or two that I'll point but it will probably be more about code style/preference rather than concepts.
I don't understand the question, creating a new instance for each non promivite prop is hydration. Instead of having "dumb" objects, you recursively hydrated each object to its instance. So if I'm not missing something, it's all good here ! Thanks again mate, you did great! |
did you have the chance to look into the PR...? this is not a final code, there will be more changes in the sdk for sure... If there is no any major issue I would like to have this merge at some point soon in order to cover the 0.6 spec version If you need any clarification please let me know 🙂 |
I'm very sorry I didn't have the opportunity to check in depth. At a first glance, it looks good so let's merge, we can still change things if needed. |
Many thanks for submitting your Pull Request ❤️!
What this PR does / why we need it:
closes #111
closes #104
Special notes for reviewers:
There are many files changes but we can resume them in:
src/lib/schema/types/workflow.ts
I am still working in the generate-classes file to have most of the code autogenerated, will create a PR soon.
Additional information (if needed):