-
-
Notifications
You must be signed in to change notification settings - Fork 12
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
[WIP] feat: JSON Deserializer/Denormalizer #506
Conversation
Signed-off-by: Luca Lindhorst <[email protected]>
src/serialize/json/denormalize.ts
Outdated
if (spec === undefined) { | ||
throw new UnsupportedFormatError(`Spec version ${data.specVersion} is not supported.`) | ||
} | ||
this.factory.spec = spec |
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 should be part of the tool that does initial analysis.
reading which spec version is applied should be done before the factory is initialized.
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 might wonder how the factory should be injectable, while it is not fully initialized?
possible solution: Inject a factory method, or the class type, instead of an object o the class.
Thanks for your work, |
Signed-off-by: Luca Lindhorst <[email protected]>
Worked on the denormalizer. Probably less flexible than wished, but silently ignoring known properties if their type isn't right, does not seem like good behavior to me either. No tests yet, but want feedback (@jkowalleck) on the general approach. |
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.
reviewed as requested
remarks:
- 👍 I like the implementation of type assertions.
PS: they need improvement - see [WIP] feat: JSON Deserializer/Denormalizer #506 (comment) - ❌ there is an option in the code, to change the
#spec
of a denormalizerFactory
at runtime while this factory is in use. This side effect must be removed to prevent inconsistency. - 👎 : I don't like the unforgiving behavior when data is not as expected. You could have an injectable optional
warningFunc: (msg: string) => void
that is called each time a value is unexpected or wrong, right before the value is ignored. It is important for the final result, to be able to generate a BOM model from partially invalid data. 💯 This feature could still be added later on.
note possible implementation - example code:class Factory { readonly #spec: Spec readonly #warnFn: (msg: string) => void constructor(spec: Spec, warnFn?: (msg: string) => void) { this.#spec = spec this.#warnFn = warnFn ?? function () {/* noop */} } get warn () { return this.#warnFn } } myFactory = new Factory(...) myFactory.warn('foooo baaarrr')
src/serialize/json/denormalize.ts
Outdated
if (spec === undefined) { | ||
throw new UnsupportedFormatError(`Spec version ${data.specVersion} is not supported.`) | ||
} | ||
this.factory.spec = spec |
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 might wonder how the factory should be injectable, while it is not fully initialized?
possible solution: Inject a factory method, or the class type, instead of an object o the class.
re: #506 (review) @lal12 i have it more thought and i thing the type checkers you were implementing are technically correct, but do not help TypeScript with the type assertions. read https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates example: function isString (v: any): v is string
{
return typeof v === 'string'
}
function assertIsString (v: any): assert v is string
{
if ( typeof v !== 'string' ) {
throw new AssertionError()
}
} example code of a dynamic/templated type predicate: cyclonedx-javascript-library/src/_helpers/notUndefined.ts Lines 20 to 24 in 9367f5d
|
@jkowalleck I still had it on my list to look into But I will refactor/change that anyway to get it working with |
Signed-off-by: Luca Lindhorst <[email protected]>
@jkowalleck did some refactoring. Removed spec from factory and created a context object instead. |
@lal12 |
Signed-off-by: Luca Lindhorst <[email protected]>
Signed-off-by: Luca Lindhorst <[email protected]>
Yeah doesn't really make sense anymore with the warning function. Corrected. Fixed remaining type issues |
@jkowalleck something I've noticed while writing tests is that normalizer will look for BomRef by reference to the object in
Made tests working, not ideal, but comparing boms fails due to URLs and web build complains that there is no URL polyfill, should I add one? Waiting for your review |
yes. is intended.
where? how? this is new.
will review soon |
I import |
just remove this import. |
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.
did not review all of it, stopped at a point.
Generally I see a big wrong concept here, which i will try to explain
- you are deserializing the JOSN to some data, and force the data to be denormalized. while the denormalization is already ongoing, it is decided to stop all of this, because something seams odd.
THIS MUST NOT BE. - correct would be:
- Deserialize the JSON to some data
- check if this data is an object, that has the property "bomFormat" set to "CycloneDX" - if not STOP/THROW
- check if this data is an object, that has the property "specVersion" set to "one value that is known"
- fetch the Spec from
SpecVersionDict
based on the "specVersion" properrty. If it is not existing: Exit/Throw. If the Spec does not support JSON: Exit/Throw - feed the data model into the denormalizer.
very basic (pseudo) example code to show the process:
Deserializer {
readonly #df
constructor(df: DenormalizerFactory) {
this.df=df
}
deserialize(bom: string) {
const data = JSON.parse(bom)
if (typeof data !== 'object') { throw RangeError('no object') }
if (data.bomFormat !== 'CycloneDX') { throw RangeError('wrong format') }
const spec = SpecVersionDict[data.specVersion]
if (! spec?.supportsFormat('JSON')) { throw SpecVersionDict('something') }
this.#df.makeForBom().denormalize(data)
}
}
// i dont see a need to inject the spec into the normalizer.
assertNonEmptyStr(url, path) | ||
try { | ||
return new URL(url) | ||
} catch (e) { |
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.
error not used? then just write try{ ...} catch { ... }
without keeping the error e
version: warnStringOrUndef(data.version, ctx, [...path, 'version']), | ||
tagVersion: warnNumberOrUndef(data.tagVersion, ctx, [...path, 'tagVersion']), | ||
text: denormalizeRecord(data.text, ctx, [...path, 'text'], this._factory.makeForAttachment(ctx)), | ||
url: url !== undefined |
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.
better turned, so that no bool switch is needed:
url === undefined
? url
: this._factory.makeForUrl(ctx).denormalize(url, ctx, [...path, 'url'])
} | ||
|
||
function callWarnFunc (ctx: JSONDenormalizerContext, warning: JSONDenormalizerWarning): void | never { | ||
if (typeof ctx.options.warningFunc !== 'function') { |
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.
turn arround. no bool inversin needed.
export interface Serializer { | ||
/** | ||
* @throws {@link Error} | ||
*/ | ||
serialize: (bom: Bom, options?: SerializerOptions & NormalizerOptions) => string | ||
} | ||
|
||
export type VarType = 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | '_array' | '_record' |
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.
these are no things we want to publish, they are internal.
need to find a place where to put this, so it is not exported to downstream
data: any, | ||
options: JSONDenormalizerOptions = {} | ||
): Bom { | ||
return this.#normalizerFactory.makeForBom({ options }) |
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.
options are injected into the class (constructor) and into the function? why???
} | ||
|
||
function warnStringOrUndef (value: unknown, ctx: JSONDenormalizerContext, path: PathType): string | undefined { | ||
if (value !== undefined && typeof value !== 'string') { |
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.
turn bool compare around. and twist the code blocks.
value === undefined || typeof value === 'string'
import { Format, SpecVersionDict, UnsupportedFormatError } from '../../spec' | ||
import type { JSONDenormalizerOptions, JSONDenormalizerWarning, PathType } from '../types' | ||
|
||
interface JSONDenormalizerContext { |
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 needed? why are these three values encapsulated into a record?
i found rare usage - and none was justified. all usage i found was artificially complex.
denormalize (data: Record<string, unknown>, { options }: { options: JSONDenormalizerOptions }, path: PathType): Models.Bom { | ||
assertEnum(data.bomFormat, ['CycloneDX'], [...path, 'bomFormat']) | ||
assertEnum(data.specVersion, Object.keys(SpecVersionDict), [...path, 'specVersion']) | ||
const spec = SpecVersionDict[data.specVersion as keyof typeof SpecVersionDict] as Protocol |
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 as keyof typeof SpecVersionDict
needed? the assertEnum
should have done this already. if not, then it is done wrong.
i dont see a real need for this complex usage. please simplify
const spec = SpecVersionDict[data.specVersion as Version]
if (!spec) { throw Error('something') }
assertEnum(data.bomFormat, ['CycloneDX'], [...path, 'bomFormat']) | ||
assertEnum(data.specVersion, Object.keys(SpecVersionDict), [...path, 'specVersion']) | ||
const spec = SpecVersionDict[data.specVersion as keyof typeof SpecVersionDict] as Protocol | ||
if (!spec.supportsFormat(Format.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.
❌ NO!
the (de)normalizer does not decide whether to run or not - it just does the job.
the (de)serializer might decide to not run because of constraints.
FYI: i will be working on #620 soon. |
Implements (half of) #86
Mostly done (I hope :D), but still have some issues with tests. Happy accepting advise.