-
Notifications
You must be signed in to change notification settings - Fork 31
#2 Implement options parsing. #5
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
|
This pull request has been linked to Shortcut Story #154352: Implement options class and parsing.. |
| "declaration": true, | ||
| "declarationMap": true, // enables importers to jump to source | ||
| "resolveJsonModule": true, | ||
| "stripInternal": true |
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 is to support @internal. So we can implement things and keep their details out of our exported types.
| // types. Calls to the SDK could contain anything without any regard to typing. | ||
| // So, data we take from external sources must be normalized into something | ||
| // that can be trusted. | ||
|
|
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.
Typescript basically can verify your code, that you write, at compile time. Once it is compiled, then anyone can do anything with it. Many people may use the SDK from JS and they will just pass us any random object.
So this converts that object into something that does meet the typings and then we can use further without additional runtime checks (mostly).
| @@ -0,0 +1,121 @@ | |||
| /* eslint-disable class-methods-use-this */ | |||
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 is vaguely inspired by: https://github.com/gcanti/io-ts/blob/master/index.md
I would rather not include any dependencies beyond what are strictly required. Especially for common, which we want to run on anything without worrying about polyfills.
| this.typeOf = typeof example; | ||
| } | ||
|
|
||
| is(u: unknown): u is T { |
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 guards are special methods. We aren't really using the benefit they provide here, but I figured that we may as well provide it in case we need it later.
If you use a type guard for an if-statement, then the contents of that statement are treated as the guarding type.
class Cat {
meow() {}
}
function isCat(value: any): value is Cat {
return ("meow" in value) && (typeof value.meow === 'function');
}
function example() {
const fish = {
meow: () => {}
};
if(isCat(fish)) {
// We have informed the type system that this fish is indeed a cat, so it will not complain.
fish.meow()
}
}
| @@ -1,5 +1,11 @@ | |||
| module.exports = { | |||
| transform: {'^.+\\.ts?$': 'ts-jest'}, | |||
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.
Some updates to make coverage more useful.
| @@ -0,0 +1,62 @@ | |||
| import { LDLogger } from '../src'; | |||
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 is a logger implementation for tests. Helps to validate some common scenarios.
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 is great! I read through and it overall makes sense to me (a non-TypeScript-expert).
| value < 60 | ||
| ? OptionMessages.optionBelowMinimum('diagnosticRecordingInterval', value as number, 60) | ||
| : OptionMessages.wrongOptionType('diagnosticRecordingInterval', 'number with minimum value of 60', typeof value), | ||
| ); |
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.
Because all your examples expect either 0 or 1 warning, you could potentially add more detail to the "test data" (i.e. your list of tuples) indicating what warning you expect to see, if any, and then simplify the logic of the assertions later on. One way I've seen this done is to use regexes to confirm that certain phrases appear in the warning:
it.each([
[0, 60, /using minimum of 60/],
[500, 500, null],
['potato', 900, /should be of type number with minimum value of 60/],
])('allow setting and validates diagnosticRecordingInterval', (value, expected, warningPattern) => {
const config = new Configuration(withLogger({ diagnosticRecordingInterval: value }));
expect(config.diagnosticRecordingInterval).toEqual(expected);
// This is probably DRY-able
if (warningPattern) {
expect(logger(config).warningMessages.length).toEqual(1);
expect(logger(config).warningMessages[0]).toMatch(warningPattern);
} else {
expect(logger(config).warningMessages.length).toEqual(0);
}
}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 have a thought here. I can probably just add a containsMessages or something like that to the TestLogger.
I only validated the specific message on a couple, because most are the same, but I could at least make it cleaner.
| validatedOptions.streamUri, | ||
| validatedOptions.baseUri, | ||
| validatedOptions.eventsUri, | ||
| ); |
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.
FYI, ever since we added the new ServiceEndpoints configuration builder to Java, Go, and .NET, those SDKs now have some additional validation to detect when, for example,
- the caller has overridden the
streamingURI to a custom value, but notpollingorevents - the SDK is configured to send events (i.e.
sendEvents = true)
and then we log a warning about it, because that almost definitely means they missed some configuration.
Even though we don't have a ServiceEndpoints "builder" here, and we don't really need one, it might still be worth bringing the level of validation up to parity so it can also catch those cases. I think that can be added fairly easily now that we have a dedicated "config validation" phase.
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.
Yeah, I was thinking about that and just kind of forgot about it. Can add a message for it though. Thanks for bringing it up.
…thodology for logs.
| return { errors, validatedOptions }; | ||
| } | ||
|
|
||
| function validateEndpoints(options: LDOptions, validatedOptions: ValidatedOptions) { |
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 doing this up-front for now. But once the other components are implemented I may move it. If it looks like it will be cleaner.
| Object.keys(options).forEach((optionName) => { | ||
| // We need to tell typescript it doesn't actually know what options are. | ||
| // If we don't then it complains we are doing crazy things with it. | ||
| const optionValue = (options as unknown as any)[optionName]; |
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.
sdk-common/src/Context.ts
Outdated
| @@ -0,0 +1,19 @@ | |||
| type MultiKind = { | |||
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 guy isn't intentional, but doesn't hurt anything. In progress stuff upcoming.
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.
Removing
aengelberg
left a comment
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 like the improvements. I had some minor comments about how the endpoint validation logic could be more consistent, and an observation about the new testing code.
| validatedOptions.logger?.warn(OptionMessages.partialEndpoint('streamUri')); | ||
| } | ||
|
|
||
| if (!pollingEndpointSpecified) { |
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.
Likewise, for polling
| if (!pollingEndpointSpecified) { | |
| if (!pollingEndpointSpecified && !validatedOptions.stream) { |
| const { baseUri, streamUri, eventsUri } = options; | ||
| const streamingEndpointSpecified = streamUri !== undefined && streamUri !== null; | ||
| const pollingEndpointSpecified = baseUri !== undefined && baseUri !== null; | ||
| const eventEndpointSpecified = eventsUri !== undefined && eventsUri !== null; |
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.
If you want to be consistent with how the other SDKs do it, I believe these checks would be "is the URI set to some non-null value that is not equal to the default URI". i.e. if it's not null, but happens to be equal to the default, that doesn't count as "custom" for the purposes of this check.
| expect(logger(config).getCount()).toEqual(logs.length); | ||
| // There should not be any messages, so checking them for undefined is a workaround | ||
| // for a lack of pure assert. | ||
| logger(config).verifyMessages(logs).forEach((message) => expect(message).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.
The fact that verifyMessages returns the list of missing messages threw me at first when reading this, because that was the opposite of what I assumed was happening. Also, this .forEach makes me wonder if there's a way to avoid putting in this addendum every time.
In Jest, can you call expect during a helper function, outside of the scope of a unit test? It might be cleaner if this method were called expectMessages(logs) and took care of all of the expecting.
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 will think about the missing versus not. I generally try to avoid any assertions outside the test file itself. You can do it, but then the stacks and things are not very apparent what is happening.
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 to clean up the "undefined" situation, the verify messages function could return null if everything's ok. Then this could be
expect(logger(config).verifyMessages(logs)).toBeNull();
Co-authored-by: Alex Engelberg <alex.benjamin.engelberg@gmail.com>

This PR adds configuration parsing and validation.
While the SDK is typescript consumers may be using JS, or they may just force TS to their will. So we need to carefully validate anything that comes from an external source.
This adds most of the bast configuration and tests. Additional configuration will be added later as additional components are implemented.
#4 is the base for this PR.
The PR is pretty long, but that is largely from tests. There isn't a large amount of logic, but I wanted to test configuration items relatively independently. So it will be easy to adjust individual option configuration later.