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 a robustier instanceof alternative to allow cross-package compatibility. #122

Merged
merged 26 commits into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
390eae4
Add robust duck-typing instanceof check to composite types
Shaptic May 9, 2024
58d3338
Drop ? usage since we know value is defined
Shaptic May 9, 2024
4119cfb
Add changelog entry
Shaptic May 9, 2024
cad3a7c
Run formatter
Shaptic May 9, 2024
e11574a
Clarify comments
Shaptic May 9, 2024
ddf85de
Disable minification for browser builds and reserve structures
Shaptic May 9, 2024
d57f913
Reformat :unamused:
Shaptic May 9, 2024
9a38f6e
Add test cases to make sure inner behavior works
Shaptic May 9, 2024
cafa550
Separate out function
Shaptic May 14, 2024
42aec43
Add stringify to error message for verbosity
Shaptic May 14, 2024
1d4caf5
Run formatter
Shaptic May 14, 2024
4531ad8
Undo webpack changes
Shaptic May 14, 2024
df2a659
Actually throw the errors lmao
Shaptic May 14, 2024
64024ab
Check for injected ctor name too
Shaptic Jun 24, 2024
020e66c
Robust validity checks for all subtypes
Shaptic Jun 24, 2024
3982f06
Improve error messages
Shaptic Jun 24, 2024
a399c38
Oopsie, use 'this'
Shaptic Jun 24, 2024
c950cab
Do proper injection of the field names
Shaptic Jun 27, 2024
588078c
Proper subtypes, fixup tests accordingly
Shaptic Jun 27, 2024
bfd851e
Add debugging code
Shaptic Jun 27, 2024
1569c7d
Better value check: primitive over composite
Shaptic Jun 27, 2024
5a2f66f
Make inherited classes accurate: Composite vs. Primitive has semantic…
Shaptic Jun 27, 2024
2ca01d0
Wtf it actually worked omg :o
Shaptic Jun 27, 2024
1b9701d
Improve error messages by using actual expected field name
Shaptic Jun 27, 2024
a44f20c
Simpler: just check property names directly
Shaptic Jul 17, 2024
1cf3733
Run formatter
Shaptic Jul 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ project adheres to [Semantic Versioning](http://semver.org/).

## Unreleased

### Fixed
* Increase robustness of compatibility across multiple `js-xdr` instances in an environment ([#122](https://github.com/stellar/js-xdr/pull/122)).


## [v3.1.1](https://github.com/stellar/js-xdr/compare/v3.1.0...v3.1.1)

Expand Down
2 changes: 1 addition & 1 deletion src/array.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class Array extends XdrCompositeType {
* @inheritDoc
*/
write(value, writer) {
if (!(value instanceof global.Array))
if (!global.Array.isArray(value))
throw new XdrWriterError(`value is not array`);

if (value.length !== this._length)
Expand Down
4 changes: 2 additions & 2 deletions src/enum.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Int } from './int';
import { XdrPrimitiveType } from './xdr-type';
import { XdrPrimitiveType, isSerializableIsh } from './xdr-type';
import { XdrReaderError, XdrWriterError } from './errors';

export class Enum extends XdrPrimitiveType {
Expand All @@ -26,7 +26,7 @@ export class Enum extends XdrPrimitiveType {
* @inheritDoc
*/
static write(value, writer) {
if (!(value instanceof this))
if (!isSerializableIsh(value, Enum))
throw new XdrWriterError(`unknown ${value} is not a ${this.enumName}`);

Int.write(value.value, writer);
Expand Down
4 changes: 2 additions & 2 deletions src/struct.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Reference } from './reference';
import { XdrPrimitiveType } from './xdr-type';
import { XdrPrimitiveType, isSerializableIsh } from './xdr-type';
import { XdrWriterError } from './errors';

export class Struct extends XdrPrimitiveType {
Expand All @@ -23,7 +23,7 @@ export class Struct extends XdrPrimitiveType {
* @inheritDoc
*/
static write(value, writer) {
if (!(value instanceof this))
if (!isSerializableIsh(value, Struct))
throw new XdrWriterError(`${value} is not a ${this.structName}`);

for (const [fieldName, type] of this._fields) {
Expand Down
8 changes: 5 additions & 3 deletions src/union.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Void } from './void';
import { Reference } from './reference';
import { XdrCompositeType } from './xdr-type';
import { XdrCompositeType, isSerializableIsh } from './xdr-type';
import { XdrWriterError } from './errors';

export class Union extends XdrCompositeType {
Expand Down Expand Up @@ -81,8 +81,10 @@ export class Union extends XdrCompositeType {
* @inheritDoc
*/
static write(value, writer) {
if (!(value instanceof this))
throw new XdrWriterError(`${value} is not a ${this.unionName}`);
if (!isSerializableIsh(value, Union))
throw new XdrWriterError(
`${value} is ${value?.constructor?.name}, not ${this.unionName}`
);

this._switchOn.write(value.switch(), writer);
value.armType().write(value.value(), writer);
Expand Down
42 changes: 42 additions & 0 deletions src/xdr-type.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,48 @@ function decodeInput(input, format) {
}
}

/**
* Provides a "duck typed" version of the native `instanceof` for read/write.
*
* "Duck typing" means if the parameter _looks like_ and _acts like_ a duck
* (i.e. the type we're checking), it will be treated as that type.
*
* In this case, the "type" we're looking for is "like XdrCompositeType",
* meaning serialization, but also conditioned on a particular subclass of
* "XdrCompositionType" (e.g. {@link Union} which extends XdrCompositeType ->
* XdrType).
*
* This makes the package resilient to downstream systems that may be combining
* many versions of a package across its stack that are technically compatible
* but fail `instanceof` checks due to cross-pollination.
*/
export function isSerializableIsh(value, subtype) {
const hasConstructor = (instance, name) => {
Shaptic marked this conversation as resolved.
Show resolved Hide resolved
do {
if (instance.constructor.name === name) {
return true;
}
} while ((instance = Object.getPrototypeOf(instance)));
return false;
};

return (
value !== undefined &&
value !== null && // prereqs, otherwise `getPrototypeOf` pops
(value instanceof subtype || // quickest check
// Do an initial constructor check (anywhere is fine so that children of
// `subtype` still work), then
(hasConstructor(value, subtype) &&
// ensure it has read/write methods, then
typeof value.read === 'function' &&
typeof value.write === 'function' &&
// ensure XdrCompositeType is in the prototype chain (XdrCompositeType
// reliably gives us XdrType unless you're *intentionally* being weird,
// so skip checking for that).
hasConstructor(value, 'XdrCompositeType')))
);
}

/**
* @typedef {'raw'|'hex'|'base64'} XdrEncodingFormat
*/
29 changes: 29 additions & 0 deletions test/unit/xdr-type_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { isSerializableIsh } from '../../src/xdr-type';
import { Union } from '../../src/union';

describe('duck-typing serialization', function () {
class XdrCompositeType {}
class Duck extends XdrCompositeType {
write() {}
read() {}
}
class Duckling extends Duck {}

it('respects ducks', function () {
expect(isSerializableIsh(new Duck(), XdrCompositeType)).to.be.true;
expect(isSerializableIsh(new Duck(), Duck)).to.be.true;
expect(isSerializableIsh(new Duckling(), Duck)).to.be.true;
expect(isSerializableIsh(new Duckling(), Duckling)).to.be.true;
});

class LiterateDuck extends XdrCompositeType {
read() {}
}
class Platypus extends LiterateDuck {}
it('does not respect non-ducks', function () {
expect(isSerializableIsh(new Platypus(), Duck)).to.be.false;
expect(isSerializableIsh(new Platypus(), XdrCompositeType)).to.be.true;
expect(isSerializableIsh(new LiterateDuck(), LiterateDuck)).to.be.true;
expect(isSerializableIsh(new Platypus(), Union)).to.be.false;
});
});
13 changes: 11 additions & 2 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,19 @@ module.exports = function () {
};
if (browserBuild) {
config.optimization = {
minimize: true,
minimize: false,
minimizer: [
new TerserPlugin({
parallel: true
parallel: true,
terserOptions: {
reserved: [
'XdrType',
'XdrCompositeType',
'Union',
'Array',
'Struct'
]
}
})
]
};
Expand Down
Loading