From e63f514471bd99211354db5acc595ff9f65d714b Mon Sep 17 00:00:00 2001 From: Greg Brown Date: Sat, 1 Jul 2017 16:21:03 -0700 Subject: [PATCH] initial pbj lib for es6 (#2) * Properly handle tag presence in MessageRef::fromString and add resolveQName to MessageResolver Plus some unit tests * Added support for dynamodb item * Fixed test unit and related broken items * Fixed unmarshal test unit * Auto registers the schema with the MessageResolver for unit testing * Added dynamodb integration test * Added elastica document marshaler * Fixed dynamodb integration test unit - ignore exceptions * Implemented elastica mapping factory * Cleanup integration/dynamo-db.test.js * Added elastica integration unit test * Removed extra space * refactor - phase 1 - skeleton * type system - booleanType skeleton * stub tests for type guards (wip) * rollup testing * babel with es6 imports (wip) * build testing (wip) * build testing (wip) * build testing (wip) * build testing (wip) * finish tests on booleanType * adding tinyIntType and unit tests for that type + added AssertionFailed exception + added AbstractIntType which will be used for the other int types * start on other int types (wip) * adding smallIntType and unit tests for that type + moved type sampling to the test itself, it's simpler + added test helper for types (testing guards, encode and decode with samples) * adding smallIntType and unit tests for that type + moved type sampling to the test itself, it's simpler + added test helper for types (testing guards, encode and decode with samples) * finished the scalar int types and unit tests * finished the numeric types except BigInt, IntEnum which are not scalar * add trinaryType and unit tests * add timestampType and unit tests * starting string types + added AbstractStringType with basic guards and encode/decode + added remaining enums for DynamicFieldKind, FieldRule, Format + stubbed some more of the Field object + stubbed Message object + implemented TextType and created tests for it * finished text and medium text + use Buffer.from(...).bytesLength to get byte size (for guard) + add text with emojis to guard tests * stub stringType (wip) * fix custom string pattern validation in stringType + only create the regex one time (in the field) + when creating a RegExp instance, don't include leading/trailing "/" * fix custom string pattern validation in stringType + only create the regex one time (in the field) + when creating a RegExp instance, don't include leading/trailing "/" * finish SchemaVersion and start SchemaQName + added unit tests SchemaQName and SchemaVersion + stubbed SchemaId and SchemaCurie + stubbed failing tests for "fromId" and "fromCurie" to SchemaQName + using flyweight for SchemaQName * match up to php lib closer for sake of simplicity + export Type classes. + use "create" factory function in Type(s) to create instance + add unit tests to assert "create" factory on type always returns same instance * match up to php lib closer for sake of simplicity2 + export Type classes. + use "create" factory function in Type(s) to create instance + add unit tests to assert "create" factory on type always returns same instance * match up to php lib closer for sake of simplicity3 + export Type classes. + use "create" factory function in Type(s) to create instance + add unit tests to assert "create" factory on type always returns same instance * use @gdbots/common v0.1.0 + string type format validators (wip) + rename assert in tests to just "t". less "t"yping * finish StringType format rules with unit tests * starting binary type (wip) * finish binary types and IntEnum type + Binary, Blob and Medium Blob use utf8 and base-64 libs + Added unit tests for binary and int enum types + Created fixtures for IntEnum and StringEnum + Stubbed more methods to Field object for classProto and curies + Added more assertions to type testing (min, max and max bytes) * finished IntEnumType and StringEnumType with tests * finished BigIntType and SignedBigIntType with unit tests * finished WellKnown/UuidIdentifier and UuidType with unit tests * added WellKnown/TimeUuidIdentifier with unit tests * added TimeUuidType with unit tests * added WellKnown/Microtime with unit tests * added MicrotimeType with unit tests * added WellKnown GeoPoint with unit tests * added GeoPointType with unit tests * added IdentifierType with unit tests * added DatedSlugIdentifier, SlugIdentifier with unit tests * added DateType with unit tests * added DateTimeType with unit tests * added WellKnown/DynamicField and Type/DynamicFieldType with unit tests * added SchemaCurie with unit tests * some linter cleanup * added SchemaCurie, MessageRef and MessageRefType with unit tests * added SchemaId with unit tests + created InvalidArgumentException to remove some code duplication + added more tests for SchemaCurie + exporting all WellKnown from main index now * remove old src/old tests that have been refactored * added FieldBuilder with unit tests * added FieldBuilder to main module exports * remove eslint disable from MessageRef tests * use just "anyOfCuries" as js lib won't be using the same style of assertion that php does * progress on Field object, guarding default and value (wip) * completed Field object with unit tests * completed Mixin with unit tests * Schema with unit tests + createMessage method still incomplete, waiting for Message * Schema with unit tests (wip) + add schema and mixin to index exports + remove old src and tests that have been refactored * starting Message class (wip) * Message class (wip) * Message class (wip) * get rid of all wildcard imports * jsdoc fix * Message class - list field operations complete with unit tests (wip) * Message class - map field operations with unit tests (wip) * Message class - map field operations with unit tests2 (wip) * adding traits (wip) * mixin wip * starting serializers (wip) * message type and object serializer mostly done (wip) * message resolver (wip) * add more tests for object serializer * remove old source and tests * refactoring directory names to match js community conventions * finish MessageResolver with unit tests * prepare for npm publish --- .babelrc | 38 +- .codeclimate.yml | 10 + .eslintignore | 2 + .eslintrc | 21 + .gitignore | 59 +- .npmignore | 11 + .nvmrc | 1 + .travis.yml | 3 + Gruntfile.js | 78 - LICENSE | 201 -- LICENSE.md | 141 + README.md | 4 +- bower.json | 10 - build/use-lodash-es.js | 11 + dist/pbj.min.js | 10 - package-lock.json | 2594 +++++++++++++++++ package.json | 56 +- src/Field.js | 510 ++++ src/FieldBuilder.js | 227 ++ src/Message.js | 852 ++++++ src/MessageRef.js | 175 ++ src/MessageResolver.js | 219 ++ src/Mixin.js | 82 + src/Schema.js | 310 ++ src/SchemaCurie.js | 157 + src/SchemaId.js | 212 ++ src/SchemaQName.js | 126 + src/SchemaVersion.js | 115 + src/codec.js | 89 - src/enum/dynamic-field-kind.js | 23 - src/enum/field-rule.js | 19 - src/enum/format.js | 37 - src/enum/type-name.js | 77 - src/enums/DynamicFieldKind.js | 13 + src/enums/FieldRule.js | 11 + src/enums/Format.js | 22 + src/enums/TypeName.js | 40 + src/exception/decode-value-failed.js | 57 - src/exception/deserialize-message-failed.js | 6 - src/exception/encode-value-failed.js | 57 - src/exception/field-already-defined.js | 46 - src/exception/field-not-defined.js | 37 - .../field-override-not-compatible.js | 57 - src/exception/frozen-message-is-immutable.js | 33 - src/exception/gdbots-pbj-exception.js | 5 - src/exception/invalid-argument-exception.js | 6 - src/exception/invalid-resolved-schema.js | 48 - src/exception/invalid-schema-curie.js | 6 - src/exception/invalid-schema-id.js | 6 - src/exception/invalid-schema-q-name.js | 6 - src/exception/invalid-schema-version.js | 6 - src/exception/logic-exception.js | 6 - src/exception/mixin-already-added.js | 48 - src/exception/mixin-not-defined.js | 37 - .../more-than-one-message-for-mixin.js | 49 - src/exception/no-message-for-curie.js | 33 - src/exception/no-message-for-mixin.js | 33 - src/exception/no-message-for-schema-id.js | 33 - src/exception/required-field-not-set.js | 54 - src/exception/schema-not-defined.js | 6 - src/exceptions/AssertionFailed.js | 4 + src/exceptions/DecodeValueFailed.js | 36 + src/exceptions/FieldAlreadyDefined.js | 27 + src/exceptions/FieldNotDefined.js | 20 + src/exceptions/FieldOverrideNotCompatible.js | 36 + src/exceptions/FrozenMessageIsImmutable.js | 18 + src/exceptions/GdbotsPbjException.js | 4 + src/exceptions/InvalidArgumentException.js | 12 + src/exceptions/InvalidResolvedSchema.js | 20 + src/exceptions/InvalidSchemaCurie.js | 4 + src/exceptions/InvalidSchemaId.js | 4 + src/exceptions/InvalidSchemaQName.js | 4 + src/exceptions/InvalidSchemaVersion.js | 4 + src/exceptions/LogicException.js | 12 + src/exceptions/MixinAlreadyAdded.js | 29 + src/exceptions/MixinNotDefined.js | 20 + src/exceptions/MoreThanOneMessageForMixin.js | 28 + src/exceptions/NoMessageForCurie.js | 18 + src/exceptions/NoMessageForMixin.js | 18 + src/exceptions/NoMessageForQName.js | 18 + src/exceptions/NoMessageForSchemaId.js | 18 + src/exceptions/RequiredFieldNotSet.js | 35 + src/exceptions/SchemaException.js | 10 + src/exceptions/SchemaNotDefined.js | 4 + src/field-builder.js | 306 -- src/field.js | 624 ---- src/index.js | 27 + src/message-ref.js | 170 -- src/message-resolver.js | 256 -- src/message.js | 911 ------ src/mixin.js | 60 - src/schema-curie.js | 158 - src/schema-id.js | 203 -- src/schema-q-name.js | 119 - src/schema-version.js | 115 - src/schema.js | 294 -- src/serializer/array-serializer.js | 273 -- src/serializer/json-serializer.js | 58 - src/serializer/serializer.js | 27 - src/serializers/JsonSerializer.js | 36 + src/serializers/ObjectSerializer.js | 213 ++ src/type/abstract-binary-type.js | 114 - src/type/abstract-int-type.js | 53 - src/type/abstract-string-type.js | 60 - src/type/big-int-type.js | 68 - src/type/binary-type.js | 14 - src/type/blob-type.js | 14 - src/type/boolean-type.js | 51 - src/type/date-time-type.js | 64 - src/type/date-type.js | 63 - src/type/decimal-type.js | 74 - src/type/dynamic-field-type.js | 51 - src/type/float-type.js | 58 - src/type/geo-point-type.js | 53 - src/type/identifier-type.js | 85 - src/type/int-enum-type.js | 99 - src/type/int-type.js | 21 - src/type/medium-blob-type.js | 21 - src/type/medium-int-type.js | 21 - src/type/medium-text-type.js | 21 - src/type/message-ref-type.js | 46 - src/type/message-type.js | 94 - src/type/microtime-type.js | 64 - src/type/signed-big-int-type.js | 68 - src/type/signed-int-type.js | 21 - src/type/signed-medium-int-type.js | 21 - src/type/signed-small-int-type.js | 21 - src/type/signed-tiny-int-type.js | 21 - src/type/small-int-type.js | 21 - src/type/string-enum-type.js | 97 - src/type/string-type.js | 102 - src/type/text-type.js | 14 - src/type/time-uuid-type.js | 80 - src/type/timestamp-type.js | 49 - src/type/tiny-int-type.js | 21 - src/type/trinary-type.js | 73 - src/type/type.js | 185 -- src/type/uuid-type.js | 80 - src/types/AbstractBinaryType.js | 111 + src/types/AbstractIntType.js | 64 + src/types/AbstractStringType.js | 66 + src/types/BigIntType.js | 83 + src/types/BinaryType.js | 17 + src/types/BlobType.js | 17 + src/types/BooleanType.js | 75 + src/types/DateTimeType.js | 88 + src/types/DateType.js | 92 + src/types/DecimalType.js | 76 + src/types/DynamicFieldType.js | 80 + src/types/FloatType.js | 76 + src/types/GeoPointType.js | 80 + src/types/IdentifierType.js | 90 + src/types/IntEnumType.js | 104 + src/types/IntType.js | 24 + src/types/MediumBlobType.js | 24 + src/types/MediumIntType.js | 24 + src/types/MediumTextType.js | 24 + src/types/MessageRefType.js | 73 + src/types/MessageType.js | 99 + src/types/MicrotimeType.js | 80 + src/types/SignedBigIntType.js | 83 + src/types/SignedIntType.js | 24 + src/types/SignedMediumIntType.js | 24 + src/types/SignedSmallIntType.js | 24 + src/types/SignedTinyIntType.js | 24 + src/types/SmallIntType.js | 24 + src/types/StringEnumType.js | 96 + src/types/StringType.js | 153 + src/types/TextType.js | 17 + src/types/TimeUuidType.js | 85 + src/types/TimestampType.js | 62 + src/types/TinyIntType.js | 24 + src/types/TrinaryType.js | 92 + src/types/Type.js | 129 + src/types/UuidType.js | 85 + src/types/index.js | 71 + src/well-known/BigNumber.js | 3 + src/well-known/DatedSlugIdentifier.js | 37 + src/well-known/DynamicField.js | 296 ++ src/well-known/GeoPoint.js | 123 + src/well-known/Identifier.js | 62 + src/well-known/Microtime.js | 126 + src/well-known/SlugIdentifier.js | 28 + src/well-known/TimeUuidIdentifier.js | 25 + src/well-known/UuidIdentifier.js | 25 + src/well-known/big-number.js | 8 - src/well-known/dated-slug-identifier.js | 78 - src/well-known/dynamic-field.js | 280 -- src/well-known/generates-identifier.js | 11 - src/well-known/geo-point.js | 99 - src/well-known/identifier.js | 37 - src/well-known/index.js | 21 + src/well-known/microtime.js | 155 - src/well-known/slug-identifier.js | 69 - src/well-known/string-identifier.js | 59 - src/well-known/time-uuid-identifier.js | 30 - src/well-known/uuid-identifier.js | 63 - tests/Field.test.js | 319 ++ tests/FieldBuilder.test.js | 79 + tests/Message.list.test.js | 137 + tests/Message.map.test.js | 160 + tests/Message.set.test.js | 61 + tests/Message.single.test.js | 23 + tests/Message.test.js | 154 + tests/MessageRef.test.js | 319 ++ tests/MessageResolver.test.js | 137 + tests/Mixin.test.js | 23 + tests/Schema.test.js | 173 ++ tests/SchemaCurie.test.js | 91 + tests/SchemaId.test.js | 98 + tests/SchemaQName.test.js | 101 + tests/SchemaVersion.test.js | 48 + tests/add-types-test.js | 239 -- tests/babel-register.js | 3 + tests/bootstrap.js | 18 - tests/fixtures/SampleMessageV1.js | 53 + tests/fixtures/SampleMessageV2.js | 41 + tests/fixtures/SampleMixinV1.js | 28 + tests/fixtures/SampleMixinV2.js | 30 + tests/fixtures/SampleOtherMessageV1.js | 32 + tests/fixtures/SampleTraitV1.js | 24 + tests/fixtures/SampleTraitV2.js | 24 + tests/fixtures/SampleUnusedMixinV1.js | 12 + tests/fixtures/email-message.js | 106 - tests/fixtures/enum/int-enum.js | 15 - tests/fixtures/enum/priority.js | 17 - tests/fixtures/enum/provider.js | 17 - tests/fixtures/enum/string-enum.js | 15 - tests/fixtures/enums/SampleIntEnum.js | 10 + tests/fixtures/enums/SampleStringEnum.js | 10 + tests/fixtures/maps-message.js | 136 - tests/fixtures/nested-message.js | 47 - .../well-known/SampleDatedSlugIdentifier.js | 4 + .../well-known/SampleSlugIdentifier.js | 4 + .../well-known/SampleTimeUuidIdentifier.js | 4 + .../well-known/SampleUuidIdentifier.js | 4 + tests/index.test.js | 5 + tests/maps-test.js | 34 - tests/message-test.js | 339 --- tests/serializers/JsonSerializer.test.js | 16 + tests/serializers/ObjectSerializer.test.js | 121 + tests/type/trinary-type-test.js | 73 - tests/type/type-test.js | 60 - tests/types/BigIntType.test.js | 99 + tests/types/BinaryType.test.js | 101 + tests/types/BlobType.test.js | 101 + tests/types/BooleanType.test.js | 104 + tests/types/DateTimeType.test.js | 126 + tests/types/DateType.test.js | 111 + tests/types/DecimalType.test.js | 118 + tests/types/DynamicFieldType.test.js | 124 + tests/types/FloatType.test.js | 90 + tests/types/GeoPointType.test.js | 115 + tests/types/IdentifierType.test.js | 109 + tests/types/IntEnumType.test.js | 92 + tests/types/IntType.test.js | 98 + tests/types/MediumBlobType.test.js | 101 + tests/types/MediumIntType.test.js | 88 + tests/types/MediumTextType.test.js | 94 + tests/types/MessageRefType.test.js | 115 + tests/types/MessageType.test.js | 162 + tests/types/MicrotimeType.test.js | 107 + tests/types/SignedBigIntType.test.js | 106 + tests/types/SignedIntType.test.js | 94 + tests/types/SignedMediumIntType.test.js | 94 + tests/types/SignedSmallIntType.test.js | 94 + tests/types/SignedTinyIntType.test.js | 94 + tests/types/SmallIntType.test.js | 88 + tests/types/StringEnumType.test.js | 90 + tests/types/StringType.test.js | 333 +++ tests/types/TextType.test.js | 94 + tests/types/TimeUuidType.test.js | 119 + tests/types/TimestampType.test.js | 84 + tests/types/TinyIntType.test.js | 88 + tests/types/TrinaryType.test.js | 90 + tests/types/UuidType.test.js | 109 + tests/types/helpers.js | 114 + tests/well-known/DatedSlugIdentifier.test.js | 83 + tests/well-known/DynamicField.test.js | 118 + tests/well-known/GeoPoint.test.js | 91 + tests/well-known/Microtime.test.js | 64 + tests/well-known/SlugIdentifier.test.js | 82 + tests/well-known/TimeUuidIdentifier.test.js | 71 + tests/well-known/UuidIdentifier.test.js | 68 + tests/well-known/dynamic-field-test.js | 20 - tests/well-known/microtime-test.js | 30 - tests/well-known/uuid-identifier-test.js | 39 - 287 files changed, 16582 insertions(+), 8990 deletions(-) create mode 100644 .codeclimate.yml create mode 100644 .eslintignore create mode 100644 .eslintrc create mode 100644 .npmignore create mode 100644 .nvmrc create mode 100644 .travis.yml delete mode 100644 Gruntfile.js delete mode 100644 LICENSE create mode 100644 LICENSE.md delete mode 100755 bower.json create mode 100644 build/use-lodash-es.js delete mode 100644 dist/pbj.min.js create mode 100644 package-lock.json create mode 100644 src/Field.js create mode 100644 src/FieldBuilder.js create mode 100644 src/Message.js create mode 100644 src/MessageRef.js create mode 100644 src/MessageResolver.js create mode 100644 src/Mixin.js create mode 100644 src/Schema.js create mode 100644 src/SchemaCurie.js create mode 100644 src/SchemaId.js create mode 100644 src/SchemaQName.js create mode 100644 src/SchemaVersion.js delete mode 100644 src/codec.js delete mode 100644 src/enum/dynamic-field-kind.js delete mode 100644 src/enum/field-rule.js delete mode 100644 src/enum/format.js delete mode 100644 src/enum/type-name.js create mode 100644 src/enums/DynamicFieldKind.js create mode 100644 src/enums/FieldRule.js create mode 100644 src/enums/Format.js create mode 100644 src/enums/TypeName.js delete mode 100644 src/exception/decode-value-failed.js delete mode 100644 src/exception/deserialize-message-failed.js delete mode 100644 src/exception/encode-value-failed.js delete mode 100644 src/exception/field-already-defined.js delete mode 100644 src/exception/field-not-defined.js delete mode 100644 src/exception/field-override-not-compatible.js delete mode 100644 src/exception/frozen-message-is-immutable.js delete mode 100644 src/exception/gdbots-pbj-exception.js delete mode 100644 src/exception/invalid-argument-exception.js delete mode 100644 src/exception/invalid-resolved-schema.js delete mode 100644 src/exception/invalid-schema-curie.js delete mode 100644 src/exception/invalid-schema-id.js delete mode 100644 src/exception/invalid-schema-q-name.js delete mode 100644 src/exception/invalid-schema-version.js delete mode 100644 src/exception/logic-exception.js delete mode 100644 src/exception/mixin-already-added.js delete mode 100644 src/exception/mixin-not-defined.js delete mode 100644 src/exception/more-than-one-message-for-mixin.js delete mode 100644 src/exception/no-message-for-curie.js delete mode 100644 src/exception/no-message-for-mixin.js delete mode 100644 src/exception/no-message-for-schema-id.js delete mode 100644 src/exception/required-field-not-set.js delete mode 100644 src/exception/schema-not-defined.js create mode 100644 src/exceptions/AssertionFailed.js create mode 100644 src/exceptions/DecodeValueFailed.js create mode 100644 src/exceptions/FieldAlreadyDefined.js create mode 100644 src/exceptions/FieldNotDefined.js create mode 100644 src/exceptions/FieldOverrideNotCompatible.js create mode 100644 src/exceptions/FrozenMessageIsImmutable.js create mode 100644 src/exceptions/GdbotsPbjException.js create mode 100644 src/exceptions/InvalidArgumentException.js create mode 100644 src/exceptions/InvalidResolvedSchema.js create mode 100644 src/exceptions/InvalidSchemaCurie.js create mode 100644 src/exceptions/InvalidSchemaId.js create mode 100644 src/exceptions/InvalidSchemaQName.js create mode 100644 src/exceptions/InvalidSchemaVersion.js create mode 100644 src/exceptions/LogicException.js create mode 100644 src/exceptions/MixinAlreadyAdded.js create mode 100644 src/exceptions/MixinNotDefined.js create mode 100644 src/exceptions/MoreThanOneMessageForMixin.js create mode 100644 src/exceptions/NoMessageForCurie.js create mode 100644 src/exceptions/NoMessageForMixin.js create mode 100644 src/exceptions/NoMessageForQName.js create mode 100644 src/exceptions/NoMessageForSchemaId.js create mode 100644 src/exceptions/RequiredFieldNotSet.js create mode 100644 src/exceptions/SchemaException.js create mode 100644 src/exceptions/SchemaNotDefined.js delete mode 100644 src/field-builder.js delete mode 100644 src/field.js create mode 100644 src/index.js delete mode 100644 src/message-ref.js delete mode 100644 src/message-resolver.js delete mode 100644 src/message.js delete mode 100644 src/mixin.js delete mode 100644 src/schema-curie.js delete mode 100644 src/schema-id.js delete mode 100644 src/schema-q-name.js delete mode 100644 src/schema-version.js delete mode 100644 src/schema.js delete mode 100644 src/serializer/array-serializer.js delete mode 100644 src/serializer/json-serializer.js delete mode 100644 src/serializer/serializer.js create mode 100644 src/serializers/JsonSerializer.js create mode 100644 src/serializers/ObjectSerializer.js delete mode 100644 src/type/abstract-binary-type.js delete mode 100644 src/type/abstract-int-type.js delete mode 100644 src/type/abstract-string-type.js delete mode 100644 src/type/big-int-type.js delete mode 100644 src/type/binary-type.js delete mode 100644 src/type/blob-type.js delete mode 100644 src/type/boolean-type.js delete mode 100644 src/type/date-time-type.js delete mode 100644 src/type/date-type.js delete mode 100644 src/type/decimal-type.js delete mode 100644 src/type/dynamic-field-type.js delete mode 100644 src/type/float-type.js delete mode 100644 src/type/geo-point-type.js delete mode 100644 src/type/identifier-type.js delete mode 100644 src/type/int-enum-type.js delete mode 100644 src/type/int-type.js delete mode 100644 src/type/medium-blob-type.js delete mode 100644 src/type/medium-int-type.js delete mode 100644 src/type/medium-text-type.js delete mode 100644 src/type/message-ref-type.js delete mode 100644 src/type/message-type.js delete mode 100644 src/type/microtime-type.js delete mode 100644 src/type/signed-big-int-type.js delete mode 100644 src/type/signed-int-type.js delete mode 100644 src/type/signed-medium-int-type.js delete mode 100644 src/type/signed-small-int-type.js delete mode 100644 src/type/signed-tiny-int-type.js delete mode 100644 src/type/small-int-type.js delete mode 100644 src/type/string-enum-type.js delete mode 100644 src/type/string-type.js delete mode 100644 src/type/text-type.js delete mode 100644 src/type/time-uuid-type.js delete mode 100644 src/type/timestamp-type.js delete mode 100644 src/type/tiny-int-type.js delete mode 100644 src/type/trinary-type.js delete mode 100644 src/type/type.js delete mode 100644 src/type/uuid-type.js create mode 100644 src/types/AbstractBinaryType.js create mode 100644 src/types/AbstractIntType.js create mode 100644 src/types/AbstractStringType.js create mode 100644 src/types/BigIntType.js create mode 100644 src/types/BinaryType.js create mode 100644 src/types/BlobType.js create mode 100644 src/types/BooleanType.js create mode 100644 src/types/DateTimeType.js create mode 100644 src/types/DateType.js create mode 100644 src/types/DecimalType.js create mode 100644 src/types/DynamicFieldType.js create mode 100644 src/types/FloatType.js create mode 100644 src/types/GeoPointType.js create mode 100644 src/types/IdentifierType.js create mode 100644 src/types/IntEnumType.js create mode 100644 src/types/IntType.js create mode 100644 src/types/MediumBlobType.js create mode 100644 src/types/MediumIntType.js create mode 100644 src/types/MediumTextType.js create mode 100644 src/types/MessageRefType.js create mode 100644 src/types/MessageType.js create mode 100644 src/types/MicrotimeType.js create mode 100644 src/types/SignedBigIntType.js create mode 100644 src/types/SignedIntType.js create mode 100644 src/types/SignedMediumIntType.js create mode 100644 src/types/SignedSmallIntType.js create mode 100644 src/types/SignedTinyIntType.js create mode 100644 src/types/SmallIntType.js create mode 100644 src/types/StringEnumType.js create mode 100644 src/types/StringType.js create mode 100644 src/types/TextType.js create mode 100644 src/types/TimeUuidType.js create mode 100644 src/types/TimestampType.js create mode 100644 src/types/TinyIntType.js create mode 100644 src/types/TrinaryType.js create mode 100644 src/types/Type.js create mode 100644 src/types/UuidType.js create mode 100644 src/types/index.js create mode 100644 src/well-known/BigNumber.js create mode 100644 src/well-known/DatedSlugIdentifier.js create mode 100644 src/well-known/DynamicField.js create mode 100644 src/well-known/GeoPoint.js create mode 100644 src/well-known/Identifier.js create mode 100644 src/well-known/Microtime.js create mode 100644 src/well-known/SlugIdentifier.js create mode 100644 src/well-known/TimeUuidIdentifier.js create mode 100644 src/well-known/UuidIdentifier.js delete mode 100644 src/well-known/big-number.js delete mode 100644 src/well-known/dated-slug-identifier.js delete mode 100644 src/well-known/dynamic-field.js delete mode 100644 src/well-known/generates-identifier.js delete mode 100644 src/well-known/geo-point.js delete mode 100644 src/well-known/identifier.js create mode 100644 src/well-known/index.js delete mode 100644 src/well-known/microtime.js delete mode 100644 src/well-known/slug-identifier.js delete mode 100644 src/well-known/string-identifier.js delete mode 100644 src/well-known/time-uuid-identifier.js delete mode 100644 src/well-known/uuid-identifier.js create mode 100644 tests/Field.test.js create mode 100644 tests/FieldBuilder.test.js create mode 100644 tests/Message.list.test.js create mode 100644 tests/Message.map.test.js create mode 100644 tests/Message.set.test.js create mode 100644 tests/Message.single.test.js create mode 100644 tests/Message.test.js create mode 100644 tests/MessageRef.test.js create mode 100644 tests/MessageResolver.test.js create mode 100644 tests/Mixin.test.js create mode 100644 tests/Schema.test.js create mode 100644 tests/SchemaCurie.test.js create mode 100644 tests/SchemaId.test.js create mode 100644 tests/SchemaQName.test.js create mode 100644 tests/SchemaVersion.test.js delete mode 100644 tests/add-types-test.js create mode 100644 tests/babel-register.js delete mode 100644 tests/bootstrap.js create mode 100644 tests/fixtures/SampleMessageV1.js create mode 100644 tests/fixtures/SampleMessageV2.js create mode 100644 tests/fixtures/SampleMixinV1.js create mode 100644 tests/fixtures/SampleMixinV2.js create mode 100644 tests/fixtures/SampleOtherMessageV1.js create mode 100644 tests/fixtures/SampleTraitV1.js create mode 100644 tests/fixtures/SampleTraitV2.js create mode 100644 tests/fixtures/SampleUnusedMixinV1.js delete mode 100644 tests/fixtures/email-message.js delete mode 100644 tests/fixtures/enum/int-enum.js delete mode 100644 tests/fixtures/enum/priority.js delete mode 100644 tests/fixtures/enum/provider.js delete mode 100644 tests/fixtures/enum/string-enum.js create mode 100644 tests/fixtures/enums/SampleIntEnum.js create mode 100644 tests/fixtures/enums/SampleStringEnum.js delete mode 100644 tests/fixtures/maps-message.js delete mode 100644 tests/fixtures/nested-message.js create mode 100644 tests/fixtures/well-known/SampleDatedSlugIdentifier.js create mode 100644 tests/fixtures/well-known/SampleSlugIdentifier.js create mode 100644 tests/fixtures/well-known/SampleTimeUuidIdentifier.js create mode 100644 tests/fixtures/well-known/SampleUuidIdentifier.js create mode 100644 tests/index.test.js delete mode 100644 tests/maps-test.js delete mode 100644 tests/message-test.js create mode 100644 tests/serializers/JsonSerializer.test.js create mode 100644 tests/serializers/ObjectSerializer.test.js delete mode 100644 tests/type/trinary-type-test.js delete mode 100644 tests/type/type-test.js create mode 100644 tests/types/BigIntType.test.js create mode 100644 tests/types/BinaryType.test.js create mode 100644 tests/types/BlobType.test.js create mode 100644 tests/types/BooleanType.test.js create mode 100644 tests/types/DateTimeType.test.js create mode 100644 tests/types/DateType.test.js create mode 100644 tests/types/DecimalType.test.js create mode 100644 tests/types/DynamicFieldType.test.js create mode 100644 tests/types/FloatType.test.js create mode 100644 tests/types/GeoPointType.test.js create mode 100644 tests/types/IdentifierType.test.js create mode 100644 tests/types/IntEnumType.test.js create mode 100644 tests/types/IntType.test.js create mode 100644 tests/types/MediumBlobType.test.js create mode 100644 tests/types/MediumIntType.test.js create mode 100644 tests/types/MediumTextType.test.js create mode 100644 tests/types/MessageRefType.test.js create mode 100644 tests/types/MessageType.test.js create mode 100644 tests/types/MicrotimeType.test.js create mode 100644 tests/types/SignedBigIntType.test.js create mode 100644 tests/types/SignedIntType.test.js create mode 100644 tests/types/SignedMediumIntType.test.js create mode 100644 tests/types/SignedSmallIntType.test.js create mode 100644 tests/types/SignedTinyIntType.test.js create mode 100644 tests/types/SmallIntType.test.js create mode 100644 tests/types/StringEnumType.test.js create mode 100644 tests/types/StringType.test.js create mode 100644 tests/types/TextType.test.js create mode 100644 tests/types/TimeUuidType.test.js create mode 100644 tests/types/TimestampType.test.js create mode 100644 tests/types/TinyIntType.test.js create mode 100644 tests/types/TrinaryType.test.js create mode 100644 tests/types/UuidType.test.js create mode 100644 tests/types/helpers.js create mode 100644 tests/well-known/DatedSlugIdentifier.test.js create mode 100644 tests/well-known/DynamicField.test.js create mode 100644 tests/well-known/GeoPoint.test.js create mode 100644 tests/well-known/Microtime.test.js create mode 100644 tests/well-known/SlugIdentifier.test.js create mode 100644 tests/well-known/TimeUuidIdentifier.test.js create mode 100644 tests/well-known/UuidIdentifier.test.js delete mode 100644 tests/well-known/dynamic-field-test.js delete mode 100644 tests/well-known/microtime-test.js delete mode 100644 tests/well-known/uuid-identifier-test.js diff --git a/.babelrc b/.babelrc index c13c5f6..0711f59 100644 --- a/.babelrc +++ b/.babelrc @@ -1,3 +1,39 @@ { - "presets": ["es2015"] + "env": { + "cjs": { + "presets": [ + [ + "env", + { + "targets": { + "node": "current" + }, + "modules": "commonjs", + "useBuiltIns": true + } + ] + ], + "plugins": [ + "transform-es2015-modules-commonjs" + ] + }, + "es6": { + "presets": [ + [ + "env", + { + "targets": { + "node": "current" + }, + "modules": false, + "useBuiltIns": true + } + ] + ], + "plugins": [ + "lodash", + "./build/use-lodash-es" + ] + } + } } diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 0000000..9eafe3a --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,10 @@ +engines: + eslint: + enabled: true + +ratings: + paths: + - '**.js' + +exclude_paths: + - '/node_modules/*' diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..1a900ed --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +/dist/ +/*.js diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..e1c41c9 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,21 @@ +{ + "extends": "airbnb", + "globals": { + "document": true, + "window": true + }, + "env": { + "browser": true + }, + "parserOptions": { + "ecmaVersion": 8, + "sourceType": "module", + "ecmaFeatures": { + "impliedStrict": true + } + }, + "rules": { + "jsx-a11y/img-has-alt": "off", + "react/jsx-space-before-closing": "off" + } +} diff --git a/.gitignore b/.gitignore index 649d37e..0662814 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,57 @@ -dist/es6/ -node_modules/ +# Node App Files # +############ +/node_modules/ +/dist/ +/*.js +/enums +/exceptions +/serializers +/types +/well-known + +# Deployment/IDE Tools # +############ +.buildpath +*.iml +.idea/ +.project +.settings + +# Compiled source # +################### +*.com +*.class +*.dll +*.exe +*.o +*.so + +# Packages # +############ +# it's better to unpack these files and commit the raw source +# git has its own built in compression methods +*.7z +*.dmg +*.gz +*.iso +*.jar +*.rar +*.tar +*.zip + +# Logs and databases # +###################### +*.log +*.sql +*.sqlite + +# OS generated files # +###################### +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +Icon? +ehthumbs.db +Thumbs.db diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..d922a2d --- /dev/null +++ b/.npmignore @@ -0,0 +1,11 @@ +/.idea/ +/build/ +/dist/ +/src/ +/tests/ +.babelrc +.codeclimate.yml +.editorconfig +.eslint* +.nvmrc +.travis.yml diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..a00f43e --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v8 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..245900f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,3 @@ +language: node_js +node_js: + - 8 diff --git a/Gruntfile.js b/Gruntfile.js deleted file mode 100644 index f399543..0000000 --- a/Gruntfile.js +++ /dev/null @@ -1,78 +0,0 @@ -/*! - * usage: - * - * developing: $ grunt - * testing: $ grunt test - * deploy: $ grunt deploy - */ -module.exports = function(grunt) { - - grunt.initConfig({ - pkg: grunt.file.readJSON('package.json'), - uglify: { - options: { - banner: '/*! <%= pkg.name %> v<%= pkg.version %>, <%= grunt.template.today("yyyy-mm-dd HH:MM:ss Z") %> */\n', - mangle: false - }, - build: { - src: 'dist/pbj.min.js', - dest: 'dist/pbj.min.js' - } - }, - babel: { - dist: { - options: { - moduleIds: true, - getModuleId: function(moduleName) { - return 'gdbots/pbj/' + moduleName.substr(4); - }, - plugins: ['transform-es2015-modules-amd'] - }, - files: [{ - expand: true, - cwd: 'src', - src: ['**/*.js'], - dest: 'dist/es6' - }] - } - }, - concat: { - dist: { - src: [ - 'dist/es6/**/*.js' - ], - dest: 'dist/pbj.min.js' - } - }, - shell: { - 'cleanup': { - command: 'rm -rf ./dist/es6/' - } - }, - watch: { - scripts: { - files: ['Gruntfile.js', 'src/**/*.js', 'tests/**/*.js'], - tasks: ['babel', 'concat', 'shell:cleanup', 'mochaTest'] - } - }, - mochaTest: { - test: { - options: { - require: 'tests/bootstrap.js' - }, - src: ['tests/**/*.js'] - } - } - }); - - grunt.loadNpmTasks('grunt-babel'); - grunt.loadNpmTasks('grunt-contrib-watch'); - grunt.loadNpmTasks('grunt-contrib-concat'); - grunt.loadNpmTasks('grunt-contrib-uglify'); - grunt.loadNpmTasks('grunt-shell'); - grunt.loadNpmTasks('grunt-mocha-test'); - - grunt.registerTask('deploy', ['babel', 'concat', 'uglify', 'shell:cleanup']); - grunt.registerTask('default', ['watch']); - grunt.registerTask('test', ['mochaTest']); -}; diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 8dada3e..0000000 --- a/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..8d53e9f --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,141 @@ +# Apache License +Version 2.0, January 2004 + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +## 1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 +through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the +License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled +by, or are under common control with that entity. For the purposes of this definition, "control" means +(i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract +or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial +ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software +source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, +including but not limited to compiled object code, generated documentation, and conversions to other media +types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, +as indicated by a copyright notice that is included in or attached to the work (an example is provided in the +Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) +the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, +as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not +include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work +and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any +modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to +Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to +submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of +electronic, verbal, or written communication sent to the Licensor or its representatives, including but not +limited to communication on electronic mailing lists, source code control systems, and issue tracking systems +that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but +excluding communication that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been +received by Licensor and subsequently incorporated within the Work. + +## 2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare +Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +## 3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent +license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such +license applies only to those patent claims licensable by such Contributor that are necessarily infringed by +their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such +Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim +or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work +constitutes direct or contributory patent infringement, then any patent licenses granted to You under this +License for that Work shall terminate as of the date such litigation is filed. + +## 4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without +modifications, and in Source or Object form, provided that You meet the following conditions: + + 1. You must give any other recipients of the Work or Derivative Works a copy of this License; and + + 2. You must cause any modified files to carry prominent notices stating that You changed the files; and + + 3. You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, + trademark, and attribution notices from the Source form of the Work, excluding those notices that do + not pertain to any part of the Derivative Works; and + + 4. If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that + You distribute must include a readable copy of the attribution notices contained within such NOTICE + file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed as part of the Derivative Works; within + the Source form or documentation, if provided along with the Derivative Works; or, within a display + generated by the Derivative Works, if and wherever such third-party notices normally appear. The + contents of the NOTICE file are for informational purposes only and do not modify the License. You may + add Your own attribution notices within Derivative Works that You distribute, alongside or as an + addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be + construed as modifying the License. + +You may add Your own copyright statement to Your modifications and may provide additional or different license +terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative +Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the +conditions stated in this License. + +## 5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by +You to the Licensor shall be under the terms and conditions of this License, without any additional terms or +conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate +license agreement you may have executed with Licensor regarding such Contributions. + +## 6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, service marks, or product names of +the Licensor, except as required for reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +## 7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor +provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, +MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the +appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +## 8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless +required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any +Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential +damages of any character arising as a result of this License or out of the use or inability to use the Work +(including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has been advised of the possibility +of such damages. + +## 9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, +acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this +License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole +responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold +each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason +of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/README.md b/README.md index c028cc9..5d9cff9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -# pbj-js +pbj-js +============= +[![Build Status](https://api.travis-ci.org/gdbots/pbj-js.svg)](https://travis-ci.org/gdbots/pbj-js) Pbj library for es6. diff --git a/bower.json b/bower.json deleted file mode 100755 index 03f6fd2..0000000 --- a/bower.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "gdbots-pbj", - "version": "0.1.0", - "description": "Pbj library for es6", - "main": "dist/pbj.min.js", - "license": "Apache-2.0", - "dependencies": { - "gdbots-common": "*" - } -} diff --git a/build/use-lodash-es.js b/build/use-lodash-es.js new file mode 100644 index 0000000..384f46c --- /dev/null +++ b/build/use-lodash-es.js @@ -0,0 +1,11 @@ +/* eslint-disable */ +module.exports = function () { + return { + visitor: { + ImportDeclaration(path) { + const source = path.node.source; + source.value = source.value.replace(/^lodash($|\/)/, 'lodash-es$1'); + } + } + } +}; diff --git a/dist/pbj.min.js b/dist/pbj.min.js deleted file mode 100644 index 14f0c08..0000000 --- a/dist/pbj.min.js +++ /dev/null @@ -1,10 +0,0 @@ -/*! @gdbots/pbj v0.1.0, 2016-08-25 15:11:01 PDT */ -define("gdbots/pbj/codec",["exports","gdbots/pbj/well-known/dynamic-field","gdbots/pbj/well-known/geo-point","gdbots/pbj/message","gdbots/pbj/message-ref"],function(exports,_dynamicField,_geoPoint,_message,_messageRef){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{"default":obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(exports,"__esModule",{value:!0});var _dynamicField2=_interopRequireDefault(_dynamicField),_geoPoint2=_interopRequireDefault(_geoPoint),_message2=_interopRequireDefault(_message),_messageRef2=_interopRequireDefault(_messageRef),_createClass=function(){function defineProperties(target,props){for(var i=0;i "+schema.getClassName()}),_this=_possibleConstructorReturn(this,(MoreThanOneMessageForMixin.__proto__||Object.getPrototypeOf(MoreThanOneMessageForMixin)).call(this,"MessageResolver returned multiple messages using ["+mixin.getId().getCurieMajor()+"] when one was expected. Messages found: \n"+ids.join("\n")));return privateProps.set(_this,{mixin:mixin,messages:messages}),_this}return _inherits(MoreThanOneMessageForMixin,_SystemUtils$mixinCla),_createClass(MoreThanOneMessageForMixin,[{key:"getMixin",value:function(){return privateProps.get(this).mixin}},{key:"getMessage",value:function(){return privateProps.get(this).messages}}]),MoreThanOneMessageForMixin}(_systemUtils2["default"].mixinClass(_gdbotsPbjException2["default"]));exports["default"]=MoreThanOneMessageForMixin}),define("gdbots/pbj/exception/no-message-for-curie",["exports","gdbots/common/util/system-utils","gdbots/pbj/exception/gdbots-pbj-exception"],function(exports,_systemUtils,_gdbotsPbjException){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{"default":obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}Object.defineProperty(exports,"__esModule",{value:!0});var _systemUtils2=_interopRequireDefault(_systemUtils),_gdbotsPbjException2=_interopRequireDefault(_gdbotsPbjException),_createClass=function(){function defineProperties(target,props){for(var i=0;i0?(privateProps.get(this).maxLength=maxLength,privateProps.get(this).minLength=_numberUtils2["default"].bound(minLength,0,privateProps.get(this).maxLength)):privateProps.get(this).minLength=_numberUtils2["default"].bound(minLength,0,privateProps.get(this).type.getMaxBytes()),null!==pattern&&(privateProps.get(this).pattern=pattern.trim().replace("/","")),null!==format&&_format2["default"].enumValueOf(format)?privateProps.get(this).format=_format2["default"].enumValueOf(format):privateProps.get(this).format=_format2["default"].UNKNOWN}function applyNumericOptions(){var min=arguments.length<=0||void 0===arguments[0]?null:arguments[0],max=arguments.length<=1||void 0===arguments[1]?null:arguments[1],precision=arguments.length<=2||void 0===arguments[2]?10:arguments[2],scale=arguments.length<=3||void 0===arguments[3]?2:arguments[3];null!==max&&(privateProps.get(this).max=parseInt(max)),null!==min&&(privateProps.get(this).min=parseInt(min),null!==privateProps.get(this).max&&privateProps.get(this).min>privateProps.get(this).max&&(privateProps.get(this).min=privateProps.get(this).max)),privateProps.get(this).precision=_numberUtils2["default"].bound(parseInt(precision),1,65),privateProps.get(this).scale=_numberUtils2["default"].bound(parseInt(scale),0,privateProps.get(this).precision)}function applyDefault(){var defaultValue=arguments.length<=0||void 0===arguments[0]?null:arguments[0];if(privateProps.get(this).defaultValue=defaultValue,privateProps.get(this).type.isScalar())privateProps.get(this).type.getTypeName()!==_typeName2["default"].TIMESTAMP&&(privateProps.get(this).useTypeDefault=!0);else{var decodeDefault=null!==privateProps.get(this).defaultValue&&"function"!=typeof privateProps.get(this).defaultValue;switch(privateProps.get(this).type.getTypeName()){case _typeName2["default"].IDENTIFIER:if(null===privateProps.get(this).instance)throw new Error("Field ["+privateProps.get(this).name+"] requires an instance.");decodeDefault&&!privateProps.get(this).defaultValue.hasTrait("Identifier")&&(privateProps.get(this).defaultValue=privateProps.get(this).type.decode(privateProps.get(this).defaultValue,this));break;case _typeName2["default"].INT_ENUM:case _typeName2["default"].STRING_ENUM:if(null===privateProps.get(this).instance)throw new Error("Field ["+privateProps.get(this).name+"] requires an instance.");decodeDefault&&!privateProps.get(this).defaultValue.hasTrait("Enum")&&(privateProps.get(this).defaultValue=privateProps.get(this).type.decode(privateProps.get(this).defaultValue,this))}}null!==privateProps.get(this).defaultValue&&"function"!=typeof privateProps.get(this).defaultValue&&guardDefault.bind(this)(privateProps.get(this).defaultValue)}function guardDefault(defaultValue){if(this.isASingleValue())return void this.guardValue(defaultValue);if(null!==defaultValue||!Array.isArray(defaultValue))throw new Error("Field ["+privateProps.get(this).name+"] default must be an array.");if(null!==defaultValue){if(this.isAMap()&&!_arrayUtils2["default"].isAssoc(defaultValue))throw new Error("Field ["+privateProps.get(this).name+"] default must be an associative array.");_arrayUtils2["default"].each(defaultValue,function(value,key){if(null===value)throw new Error("Field ["+privateProps.get(this).name+"] default for key ["+value+"] cannot be null.");this.guardValue(value)}.bind(this))}}Object.defineProperty(exports,"__esModule",{value:!0}),exports.VALID_NAME_PATTERN=void 0;var _toArray2=_interopRequireDefault(_toArray),_arrayUtils2=_interopRequireDefault(_arrayUtils),_numberUtils2=_interopRequireDefault(_numberUtils),_systemUtils2=_interopRequireDefault(_systemUtils),_typeName2=_interopRequireDefault(_typeName),_fieldRule2=_interopRequireDefault(_fieldRule),_format2=_interopRequireDefault(_format),_createClass=function(){function defineProperties(target,props){for(var i=0;iname.length||name.length>127)throw new Error("Name length must be between 1 to 127.");if(!VALID_NAME_PATTERN.test(name))throw new Error("Field ["+name+"] must match pattern ["+VALID_NAME_PATTERN+"].");if(!type||!type.hasTrait("Type"))throw new Error('Class "'+type+'" was expected to be instanceof of "Type" but is not.');if("boolean"!=typeof required)throw new Error("Required value must be boolean.");if("boolean"!=typeof useTypeDefault)throw new Error("UseTypeDefault value must be boolean.");if("boolean"!=typeof overridable)throw new Error("Overridable value must be boolean.");return type.getTypeName()!==_typeName2["default"].MESSAGE&&(anyOfInstances=null),privateProps.set(_this,{name:name,type:type,rule:null,required:required||!1,minLength:null,maxLength:null,pattern:null,format:null,min:null,max:null,precision:10,scale:2,defaultValue:null,useTypeDefault:useTypeDefault,instance:instance,anyOfInstances:anyOfInstances,assertion:assertion,overridable:overridable||!1}),applyFieldRule.bind(_this)(rule),applyStringOptions.bind(_this)(minLength,maxLength,pattern,format),applyNumericOptions.bind(_this)(min,max,precision,scale),applyDefault.bind(_this)(defaultValue),_this}return _inherits(Field,_SystemUtils$mixinCla),_createClass(Field,[{key:"getName",value:function(){return privateProps.get(this).name}},{key:"getType",value:function(){return privateProps.get(this).type}},{key:"getRule",value:function(){return privateProps.get(this).rule}},{key:"isASingleValue",value:function(){return _fieldRule2["default"].A_SINGLE_VALUE===privateProps.get(this).rule}},{key:"isASet",value:function(){return _fieldRule2["default"].A_SET===privateProps.get(this).rule}},{key:"isAList",value:function(){return _fieldRule2["default"].A_LIST===privateProps.get(this).rule}},{key:"isAMap",value:function(){return _fieldRule2["default"].A_MAP===privateProps.get(this).rule}},{key:"isRequired",value:function(){return privateProps.get(this).required}},{key:"getMinLength",value:function(){return privateProps.get(this).minLength||0}},{key:"getMaxLength",value:function(){return privateProps.get(this).maxLength?privateProps.get(this).maxLength:privateProps.get(this).type.getMaxBytes()}},{key:"getPattern",value:function(){return privateProps.get(this).pattern}},{key:"getFormat",value:function(){return privateProps.get(this).format}},{key:"getMin",value:function(){return privateProps.get(this).min?privateProps.get(this).min:privateProps.get(this).type.getMin()}},{key:"getMax",value:function(){return privateProps.get(this).max?privateProps.get(this).max:privateProps.get(this).type.getMax()}},{key:"getPrecision",value:function(){return privateProps.get(this).precision}},{key:"getScale",value:function(){return privateProps.get(this).scale}},{key:"getDefault",value:function(){var message=arguments.length<=0||void 0===arguments[0]?null:arguments[0];if(null===privateProps.get(this).defaultValue)return privateProps.get(this).useTypeDefault?this.isASingleValue()?privateProps.get(this).type.getDefault():[]:this.isASingleValue()?null:[];if("function"==typeof privateProps.get(this).defaultValue){var defaultValue=privateProps.get(this).defaultValue(message,this);return guardDefault.bind(this)(defaultValue),null===defaultValue?privateProps.get(this).useTypeDefault?this.isASingleValue()?privateProps.get(this).type.getDefault():[]:this.isASingleValue()?null:[]:defaultValue}return privateProps.get(this).defaultValue}},{key:"hasInstance",value:function(){return null!==privateProps.get(this).instance}},{key:"getInstance",value:function(){return privateProps.get(this).instance}},{key:"hasAnyOfInstances",value:function(){return null!==privateProps.get(this).anyOfInstances}},{key:"getAnyOfInstances",value:function(){return privateProps.get(this).anyOfInstances}},{key:"isOverridable",value:function(){return privateProps.get(this).overridable}},{key:"guardValue",value:function(value){if(privateProps.get(this).required&&null===value)throw new Error("Field ["+privateProps.get(this).name+"] is required and cannot be null.");null!==value&&privateProps.get(this).type.guard(value,this),null!==privateProps.get(this).assertion&&privateProps.get(this).assertion(value,this)}},{key:"toArray",value:function(){return{name:privateProps.get(this).name,type:privateProps.get(this).type.getTypeValue(),rule:privateProps.get(this).rule.getName(),required:privateProps.get(this).required,min_length:privateProps.get(this).minLength,max_length:privateProps.get(this).maxLength,pattern:privateProps.get(this).pattern,format:privateProps.get(this).format.getValue(),min:privateProps.get(this).min,max:privateProps.get(this).max,precision:privateProps.get(this).precision,scale:privateProps.get(this).scale,"default":this.getDefault(),use_type_default:privateProps.get(this).useTypeDefault,instance:privateProps.get(this).instance,any_of_instances:privateProps.get(this).anyOfInstances,has_assertion:null!==privateProps.get(this).assertion,overridable:privateProps.get(this).overridable}}},{key:"isCompatibleForMerge",value:function(other){return privateProps.get(this).name!==other.name?!1:privateProps.get(this).type!==other.type?!1:privateProps.get(this).rule!==other.rule?!1:privateProps.get(this).instance!==other.instance?!1:0===privateProps.get(this).anyOfInstances.filter(function(k){return-1!=other.anyOfInstances.indexOf(k)}).length?!1:!0}},{key:"isCompatibleForOverride",value:function(other){return privateProps.get(this).overridable?privateProps.get(this).name!==other.name?!1:privateProps.get(this).type!==other.type?!1:privateProps.get(this).rule!==other.rule?!1:privateProps.get(this).required!==other.required?!1:!0:!1}}]),Field}(_systemUtils2["default"].mixinClass(null,_toArray2["default"]));exports["default"]=Field}),define("gdbots/pbj/message-ref",["exports","gdbots/common/util/system-utils","gdbots/common/from-array","gdbots/common/to-array","gdbots/pbj/exception/invalid-argument-exception","gdbots/pbj/exception/logic-exception","gdbots/pbj/schema-curie"],function(exports,_systemUtils,_fromArray,_toArray,_invalidArgumentException,_logicException,_schemaCurie){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{"default":obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}Object.defineProperty(exports,"__esModule",{value:!0});var _systemUtils2=_interopRequireDefault(_systemUtils),_fromArray2=_interopRequireDefault(_fromArray),_toArray2=_interopRequireDefault(_toArray),_invalidArgumentException2=_interopRequireDefault(_invalidArgumentException),_logicException2=_interopRequireDefault(_logicException),_schemaCurie2=_interopRequireDefault(_schemaCurie),_createClass=function(){function defineProperties(target,props){for(var i=0;i145)throw new Error("Schema curie cannot be greater than 145 chars.");var matches=curie.match(VALID_PATTERN);if(null===matches)throw new _invalidSchemaCurie2["default"]("Schema curie ["+curie+"] is invalid. It must match the pattern ["+VALID_PATTERN+"].");return _instances[curie]=new this(matches[1],matches[2],matches[3],matches[4]),_instances[curie]}}]),SchemaCurie}();exports["default"]=SchemaCurie}),define("gdbots/pbj/schema-id",["exports","gdbots/pbj/exception/invalid-schema-id","gdbots/pbj/schema-curie","gdbots/pbj/schema-version"],function(exports,_invalidSchemaId,_schemaCurie,_schemaVersion){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{"default":obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(exports,"__esModule",{value:!0}),exports.VALID_PATTERN=void 0;var _invalidSchemaId2=_interopRequireDefault(_invalidSchemaId),_schemaCurie2=_interopRequireDefault(_schemaCurie),_schemaVersion2=_interopRequireDefault(_schemaVersion),_createClass=function(){function defineProperties(target,props){for(var i=0;i145)throw new Error("Schema id cannot be greater than 150 chars.");var matches=schemaId.match(VALID_PATTERN);if(null===matches)throw new _invalidSchemaId2["default"]("Schema id ["+schemaId+"] is invalid. It must match the pattern ["+VALID_PATTERN+"].");return _instances[schemaId]=new this(matches[1],matches[2],matches[3],matches[4],matches[5]),_instances[schemaId]}}]),SchemaId}();exports["default"]=SchemaId}),define("gdbots/pbj/schema-q-name",["exports","gdbots/pbj/exception/invalid-schema-q-name"],function(exports,_invalidSchemaQName){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{"default":obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(exports,"__esModule",{value:!0}),exports.VALID_PATTERN=void 0;var _createClass=(_interopRequireDefault(_invalidSchemaQName),function(){function defineProperties(target,props){for(var i=0;i=minLength&&maxLength>=length;if(!okay)throw new Error("Field ["+field.getName()+"] must be between ["+minLength+"] and ["+maxLength+"] bytes, ["+length+"] bytes given.")}},{key:"encode",value:function(value,field){arguments.length<=2||void 0===arguments[2]?null:arguments[2];return value=value.trim(),""===value?null:privateProps.get(this).encodeToBase64?_urlUtils2["default"].base64Encode(value):value}},{key:"decode",value:function(value,field){arguments.length<=2||void 0===arguments[2]?null:arguments[2];if(value=value.trim(),""===value)return null;if(!privateProps.get(this).decodeFromBase64)return value;if(value=_urlUtils2["default"].base64Decode(value),!1===value)throw new _decodeValueFailed2["default"](value,field,"Strict base64_decode failed.");return value}},{key:"isBinary",value:function(){return!0}},{key:"isString",value:function(){return!0}}]),AbstractBinaryType}(_systemUtils2["default"].mixinClass(_type2["default"]));exports["default"]=AbstractBinaryType}),define("gdbots/pbj/type/abstract-int-type",["exports","gdbots/common/util/number-utils","gdbots/common/util/system-utils","gdbots/pbj/type/type"],function(exports,_numberUtils,_systemUtils,_type){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{"default":obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}Object.defineProperty(exports,"__esModule",{value:!0});var _numberUtils2=_interopRequireDefault(_numberUtils),_systemUtils2=_interopRequireDefault(_systemUtils),_type2=_interopRequireDefault(_type),_createClass=function(){function defineProperties(target,props){for(var i=0;i=min&&max>=value;if(!okay)throw new Error("Field ["+field.getName()+"] value must be between ["+min+"] and ["+max+"], ["+value+"] given.")}},{key:"encode",value:function(value,field){arguments.length<=2||void 0===arguments[2]?null:arguments[2];return parseInt(value)}},{key:"decode",value:function(value,field){arguments.length<=2||void 0===arguments[2]?null:arguments[2];return parseInt(value)}},{key:"getDefault",value:function(){return 0}},{key:"isNumeric",value:function(){return!0}}]),AbstractIntType}(_systemUtils2["default"].mixinClass(_type2["default"]));exports["default"]=AbstractIntType}),define("gdbots/pbj/type/abstract-string-type",["exports","gdbots/common/util/number-utils","gdbots/common/util/system-utils","gdbots/pbj/type/type"],function(exports,_numberUtils,_systemUtils,_type){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{"default":obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}Object.defineProperty(exports,"__esModule",{value:!0});var _numberUtils2=_interopRequireDefault(_numberUtils),_systemUtils2=_interopRequireDefault(_systemUtils),_type2=_interopRequireDefault(_type),_createClass=function(){function defineProperties(target,props){for(var i=0;i=minLength&&maxLength>=length;if(!okay)throw new Error("Field ["+field.getName()+"] must be between ["+minLength+"] and ["+maxLength+"] bytes, ["+length+"] bytes given.")}},{key:"encode",value:function(value,field){arguments.length<=2||void 0===arguments[2]?null:arguments[2];return value=value.trim(),""===value?null:value}},{key:"decode",value:function(value,field){arguments.length<=2||void 0===arguments[2]?null:arguments[2];return value=value.trim(),""===value?null:value}},{key:"isString",value:function(){return!0}}]),AbstractStringType}(_systemUtils2["default"].mixinClass(_type2["default"]));exports["default"]=AbstractStringType}),define("gdbots/pbj/type/big-int-type",["exports","gdbots/common/util/system-utils","gdbots/pbj/well-known/big-number","gdbots/pbj/type/type"],function(exports,_systemUtils,_bigNumber,_type){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{"default":obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}Object.defineProperty(exports,"__esModule",{value:!0});var _systemUtils2=_interopRequireDefault(_systemUtils),_bigNumber2=_interopRequireDefault(_bigNumber),_type2=_interopRequireDefault(_type),_createClass=function(){function defineProperties(target,props){for(var i=0;i0&&maxBytes>=length;if(!okay)throw new Error("Field ["+field.getName()+"] must be between [1] and ["+maxBytes+"] bytes, ["+length+"] bytes given.")}},{key:"encode",value:function(value,field){arguments.length<=2||void 0===arguments[2]?null:arguments[2];return value.hasTrait("Identifier")?value.toString():null}},{key:"decode",value:function(value,field){arguments.length<=2||void 0===arguments[2]?null:arguments[2];if(!value||0===value.length)return null;var instance=field.getInstance();try{return instance.fromString(value)}catch(e){throw new _decodeValueFailed2["default"](value,field,e)}}},{key:"isScalar",value:function(){return!1}},{key:"isString",value:function(){return!0}},{key:"getMaxBytes",value:function(){return 100}}]),IdentifierType}(_systemUtils2["default"].mixinClass(_type2["default"]));exports["default"]=IdentifierType}),define("gdbots/pbj/type/int-enum-type",["exports","gdbots/common/util/array-utils","gdbots/common/util/system-utils","gdbots/pbj/type/type","gdbots/pbj/exception/decode-value-failed"],function(exports,_arrayUtils,_systemUtils,_type,_decodeValueFailed){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{"default":obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}Object.defineProperty(exports,"__esModule",{value:!0});var _arrayUtils2=_interopRequireDefault(_arrayUtils),_systemUtils2=_interopRequireDefault(_systemUtils),_type2=_interopRequireDefault(_type),_decodeValueFailed2=_interopRequireDefault(_decodeValueFailed),_createClass=function(){function defineProperties(target,props){for(var i=0;ithis.getMax())throw new Error('Number "'+value.getValue()+'" was expected to be at least "'+value.getMin()+'" and at most "'+value.getMax()+'".')}},{key:"encode",value:function(value,field){arguments.length<=2||void 0===arguments[2]?null:arguments[2];return value.hasTrait("Enum")?parseInt(value.getValue()):0}},{key:"decode",value:function(value,field){arguments.length<=2||void 0===arguments[2]?null:arguments[2];if(null===value)return null;var instance=field.getInstance(),enumValue=null;if(_arrayUtils2["default"].each(instance.enumValues,function(item){value===parseInt(item.getValue())&&(enumValue=item)}),null===enumValue)throw new _decodeValueFailed2["default"](value,field,e);return enumValue}},{key:"isScalar",value:function(){return!1}},{key:"isNumeric",value:function(){return!0}},{key:"getMin",value:function(){return 0}},{key:"getMax",value:function(){return 65535}}]),IntEnumType}(_systemUtils2["default"].mixinClass(_type2["default"]));exports["default"]=IntEnumType}),define("gdbots/pbj/type/int-type",["exports","gdbots/common/util/system-utils","gdbots/pbj/type/abstract-int-type"],function(exports,_systemUtils,_abstractIntType){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{"default":obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}Object.defineProperty(exports,"__esModule",{value:!0});var _systemUtils2=_interopRequireDefault(_systemUtils),_abstractIntType2=_interopRequireDefault(_abstractIntType),_createClass=function(){function defineProperties(target,props){for(var i=0;i0&&maxBytes>=length;if(!okay)throw new Error("Field ["+field.getName()+"] must be between [1] and ["+maxBytes+"] bytes, ["+length+"] bytes given.")}},{key:"encode",value:function(value,field){arguments.length<=2||void 0===arguments[2]?null:arguments[2];return value.hasTrait("Enum")?String(value.getValue()):null}},{key:"decode",value:function(value,field){arguments.length<=2||void 0===arguments[2]?null:arguments[2];if(null===value)return null;var instance=field.getInstance(),enumValue=null;if(_arrayUtils2["default"].each(instance.enumValues,function(item){value===String(item.getValue())&&(enumValue=item)}),null===enumValue)throw new _decodeValueFailed2["default"](value,field,e);return enumValue}},{key:"isScalar",value:function(){return!1}},{key:"isString",value:function(){return!0}},{key:"getMaxBytes",value:function(){return 100}}]),StringEnumType}(_systemUtils2["default"].mixinClass(_type2["default"]));exports["default"]=StringEnumType}),define("gdbots/pbj/type/string-type",["exports","gdbots/common/util/date-utils","gdbots/common/util/hashtag-utils","gdbots/common/util/system-utils","gdbots/pbj/type/abstract-string-type","gdbots/pbj/enum/format"],function(exports,_dateUtils,_hashtagUtils,_systemUtils,_abstractStringType,_format){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{"default":obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}Object.defineProperty(exports,"__esModule",{value:!0});var _dateUtils2=_interopRequireDefault(_dateUtils),_hashtagUtils2=_interopRequireDefault(_hashtagUtils),_systemUtils2=_interopRequireDefault(_systemUtils),_abstractStringType2=_interopRequireDefault(_abstractStringType),_format2=_interopRequireDefault(_format),_createClass=function(){function defineProperties(target,props){for(var i=0;i()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/.test(value))throw new Error("Field ["+field.getName()+"] must be a valid ["+field.getFormat().getValue()+"].");break;case _format2["default"].HASHTAG:if(!_hashtagUtils2["default"].isValid(value))throw new Error("Field ["+field.getName()+"] must be a valid hashtag. @see HashtagUtils.isValid.");break;case _format2["default"].IPV4:if(!/^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$/.test(value))throw new Error("Field ["+field.getName()+"] must be a valid ["+field.getFormat().getValue()+"].");break;case _format2["default"].IPV6:if(!/^((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7}$/.test(value))throw new Error("Field ["+field.getName()+"] must be a valid ["+field.getFormat().getValue()+"].");break;case _format2["default"].HOSTNAME:case _format2["default"].URI:case _format2["default"].URL:if(!/(https?:\/\/(?:www\.|(?!www))[^\s\.]+\.[^\s]{2,}|www\.[^\s]+\.[^\s]{2,})/.test(value))throw new Error("Field ["+field.getName()+"] must be a valid ["+field.getFormat().getValue()+"].");break;case _format2["default"].UUID:if(!/[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/.test(value))throw new Error("Field ["+field.getName()+"] must be a valid ["+field.getFormat().getValue()+"].")}}},{key:"getMaxBytes",value:function(){return 255}}]),StringType}(_systemUtils2["default"].mixinClass(_abstractStringType2["default"]));exports["default"]=StringType}),define("gdbots/pbj/type/text-type",["exports","gdbots/common/util/system-utils","gdbots/pbj/type/abstract-string-type"],function(exports,_systemUtils,_abstractStringType){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{"default":obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call; -}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}Object.defineProperty(exports,"__esModule",{value:!0});var _systemUtils2=_interopRequireDefault(_systemUtils),_abstractStringType2=_interopRequireDefault(_abstractStringType),_createClass=function(){function defineProperties(target,props){for(var i=0;iname.length||name.length>127)throw new Error("DynamicField name length must be between 1 to 127.");if(!VALID_NAME_PATTERN.test(name))throw new Error("DynamicField name ["+name+"] must match pattern ["+VALID_NAME_PATTERN+"].");var field=createField(kind.getValue());return privateProps.set(_this,{name:name,kind:kind.getValue(),value:field.getType().decode(value,field)}),field.guardValue(privateProps.get(_this).value),_this}return _inherits(DynamicField,_SystemUtils$mixinCla),_createClass(DynamicField,[{key:"toArray",value:function(){var field=createField(privateProps.get(this).kind),data={name:privateProps.get(this).name};return data[privateProps.get(this).kind]=field.getType().encode(privateProps.get(this).value,field),data}},{key:"toString",value:function(){return JSON.stringify(this)}},{key:"getName",value:function(){return privateProps.get(this).name}},{key:"getKind",value:function(){return privateProps.get(this).kind}},{key:"getField",value:function(){return createField(privateProps.get(this).kind)}},{key:"getValue",value:function(){return privateProps.get(this).value}},{key:"equals",value:function(other){return privateProps.get(this).name===privateProps.get(other).name&&privateProps.get(this).kind===privateProps.get(other).kind&&privateProps.get(this).value===privateProps.get(other).value}}],[{key:"createBoolVal",value:function(name){var value=arguments.length<=1||void 0===arguments[1]?!1:arguments[1];return new this(name,_dynamicFieldKind2["default"].BOOL_VAL,value)}},{key:"createDateVal",value:function(name,value){return new this(name,_dynamicFieldKind2["default"].DATE_VAL,value)}},{key:"createFloatVal",value:function(name){var value=arguments.length<=1||void 0===arguments[1]?0:arguments[1];return new this(name,_dynamicFieldKind2["default"].FLOAT_VAL,value)}},{key:"createIntVal",value:function(name){var value=arguments.length<=1||void 0===arguments[1]?0:arguments[1];return new this(name,_dynamicFieldKind2["default"].INT_VAL,value)}},{key:"createStringVal",value:function(name,value){return new this(name,_dynamicFieldKind2["default"].STRING_VAL,value)}},{key:"createTextVal",value:function(name,value){return new this(name,_dynamicFieldKind2["default"].TEXT_VAL,value)}},{key:"fromArray",value:function(){var data=arguments.length<=0||void 0===arguments[0]?{}:arguments[0];if(void 0===data.name)throw new _invalidArgumentException2["default"]('DynamicField "name" property must be set.');var name=data.name;delete data.name;var kind=Array.keys(data)[0];try{kind=_dynamicFieldKind2["default"][kind.toUpperCase()]}catch(e){throw new _invalidArgumentException2["default"]('DynamicField "'+kind+'" is not a valid kind.')}return new this(name,kind,data[kind.getValue()])}}]),DynamicField}(_systemUtils2["default"].mixinClass(null,_fromArray2["default"],_toArray2["default"]));exports["default"]=DynamicField}),define("gdbots/pbj/well-known/generates-identifier",["exports"],function(exports){"use strict";function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(exports,"__esModule",{value:!0});var _createClass=function(){function defineProperties(target,props){for(var i=0;i90||privateProps.get(_this).latitude<-90)throw new _invalidArgumentException2["default"]("Latitude must be within range [-90.0, 90.0]");if(privateProps.get(_this).longitude>180||privateProps.get(_this).longitude<-180)throw new _invalidArgumentException2["default"]("Longitude must be within range [-180.0, 180.0]");return _this}return _inherits(GeoPoint,_SystemUtils$mixinCla),_createClass(GeoPoint,[{key:"getLatitude",value:function(){return privateProps.get(this).latitude}},{key:"getLongitude",value:function(){return privateProps.get(this).longitude}},{key:"toArray", -value:function(){return{type:"Point",coordinates:[privateProps.get(this).longitude,privateProps.get(this).latitude]}}},{key:"toString",value:function(){return privateProps.get(this).latitude+","+privateProps.get(this).longitude}}],[{key:"fromArray",value:function(){var data=arguments.length<=0||void 0===arguments[0]?{}:arguments[0];if(void 0!==data.coordinates)return new this(data.coordinates[1],data.coordinates[0]);throw new _invalidArgumentException2["default"]('Payload must be a GeoJson "Point" type.')}},{key:"fromString",value:function(string){return string=string.split(","),new this(string[0],string[1])}}]),GeoPoint}(_systemUtils2["default"].mixinClass(null,_fromArray2["default"],_toArray2["default"]));exports["default"]=GeoPoint}),define("gdbots/pbj/well-known/identifier",["exports"],function(exports){"use strict";function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(exports,"__esModule",{value:!0});var _createClass=function(){function defineProperties(target,props){for(var i=0;ilen||len>16)throw new _invalidArgumentException2["default"]("Input ["+int+"] must be between 13 and 16 digits, ["+len+"] given.");16>len&&(int=_stringUtils2["default"].strPad(int,16,"0"));var m=new this;return privateProps.get(m)["int"]=parseInt(int),privateProps.get(m).sec=parseInt(int.substring(0,10)),privateProps.get(m).usec=parseInt(int.slice(-6)),m}}]),Microtime}();exports["default"]=Microtime}),define("gdbots/pbj/well-known/slug-identifier",["exports","gdbots/common/util/slug-utils","gdbots/common/util/string-utils","gdbots/common/util/system-utils","gdbots/pbj/exception/invalid-argument-exception","gdbots/pbj/well-known/identifier"],function(exports,_slugUtils,_stringUtils,_systemUtils,_invalidArgumentException,_identifier){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{"default":obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}Object.defineProperty(exports,"__esModule",{value:!0});var _slugUtils2=_interopRequireDefault(_slugUtils),_stringUtils2=_interopRequireDefault(_stringUtils),_systemUtils2=_interopRequireDefault(_systemUtils),_invalidArgumentException2=_interopRequireDefault(_invalidArgumentException),_identifier2=_interopRequireDefault(_identifier),_createClass=function(){function defineProperties(target,props){for(var i=0;i 0) { + this.minLength = clamp(this.minLength, 0, this.maxLength); + } else { + this.minLength = clamp(this.minLength, 0, this.type.getMaxBytes()); + } + + this.pattern = pattern ? new RegExp(trim(pattern, '/')) : null; + + if (format) { + try { + this.format = Format.create(`${format}`); + } catch (e) { + this.format = Format.UNKNOWN; + } + } else { + this.format = Format.UNKNOWN; + } + } + + /** + * @private + * + * @param {?number} min + * @param {?number} max + * @param {?number} precision + * @param {?number} scale + */ + applyNumericOptions(min = null, max = null, precision = 10, scale = 2) { + this.min = min; + this.max = max; + + if (this.max !== null) { + this.max = toInteger(this.max); + } + + if (this.min !== null) { + this.min = toInteger(this.min); + if (this.max !== null && this.min > this.max) { + this.min = this.max; + } + } + + this.precision = clamp(precision, 1, 65); + this.scale = clamp(scale, 0, this.precision); + } + + /** + * @private + * + * @param {*} defaultValue + */ + applyDefault(defaultValue = null) { + this.defaultValue = defaultValue; + const defaultValueIsAFunction = isFunction(this.defaultValue); + + if (this.type.isScalar()) { + if (this.type.getTypeName() !== TypeName.TIMESTAMP) { + this.useTypeDefault = true; + } + } else { + const decodeDefault = this.defaultValue !== null && !defaultValueIsAFunction; + switch (this.type.getTypeValue()) { + case TypeName.IDENTIFIER.getValue(): + if (!this.hasClassProto()) { + throw new AssertionFailed(`Field [${this.name}] requires a classProto.`); + } + + if (decodeDefault && !(this.defaultValue instanceof Identifier)) { + this.defaultValue = this.type.decode(this.defaultValue, this); + } + + break; + + case TypeName.INT_ENUM.getValue(): + case TypeName.STRING_ENUM.getValue(): + if (!this.hasClassProto()) { + throw new AssertionFailed(`Field [${this.name}] requires a classProto.`); + } + + if (decodeDefault && !(this.defaultValue instanceof Enum)) { + this.defaultValue = this.type.decode(this.defaultValue, this); + } + + break; + + default: + break; + } + } + + if (this.defaultValue !== null && !defaultValueIsAFunction) { + this.guardDefault(this.defaultValue); + } + } + + /** + * @returns {string} + */ + getName() { + return this.name; + } + + /** + * @returns {Type} + */ + getType() { + return this.type; + } + + /** + * @returns FieldRule + */ + getRule() { + return this.rule; + } + + /** + * @returns {boolean} + */ + isASingleValue() { + return FieldRule.A_SINGLE_VALUE === this.rule; + } + + /** + * @returns {boolean} + */ + isASet() { + return FieldRule.A_SET === this.rule; + } + + /** + * @returns {boolean} + */ + isAList() { + return FieldRule.A_LIST === this.rule; + } + + /** + * @returns {boolean} + */ + isAMap() { + return FieldRule.A_MAP === this.rule; + } + + /** + * @returns {boolean} + */ + isRequired() { + return this.required; + } + + /** + * @returns {number} + */ + getMinLength() { + return this.minLength; + } + + /** + * @returns {number} + */ + getMaxLength() { + return this.maxLength > 0 ? this.maxLength : this.type.getMaxBytes(); + } + + /** + * @returns {?RegExp} + */ + getPattern() { + return this.pattern; + } + + /** + * @returns {Format} + */ + getFormat() { + return this.format; + } + + /** + * @returns {number} + */ + getMin() { + return this.min === null ? this.type.getMin() : this.min; + } + + /** + * @returns {number} + */ + getMax() { + return this.max === null ? this.type.getMax() : this.max; + } + + /** + * @returns {number} + */ + getPrecision() { + return this.precision; + } + + /** + * @returns {number} + */ + getScale() { + return this.scale; + } + + /** + * @param {?Message} message + */ + getDefault(message = null) { + if (this.defaultValue === null) { + if (this.useTypeDefault) { + return this.isASingleValue() ? this.type.getDefault() : []; + } + + return this.isASingleValue() ? null : []; + } + + if (!isFunction(this.defaultValue)) { + return this.defaultValue; + } + + const dynamicDefault = this.defaultValue(message, this); + this.guardDefault(dynamicDefault); + if (dynamicDefault === null) { + if (this.useTypeDefault) { + return this.isASingleValue() ? this.type.getDefault() : []; + } + + return this.isASingleValue() ? null : []; + } + + return dynamicDefault; + } + + /** + * @private + * + * @param {*} defaultValue + * + * @throws {AssertionFailed} + */ + guardDefault(defaultValue) { + if (defaultValue === null) { + return; + } + + if (this.isASingleValue()) { + this.guardValue(defaultValue); + return; + } + + if (this.isAMap() && !isMap(defaultValue)) { + throw new AssertionFailed(`Field [${this.name}] default must be a Map.`); + } else if (this.isASet() && !isSet(defaultValue)) { + throw new AssertionFailed(`Field [${this.name}] default must be a Set.`); + } else if (this.isAList() && !isArray(defaultValue)) { + throw new AssertionFailed(`Field [${this.name}] default must be an Array.`); + } + + defaultValue.forEach((v, k) => { + if (v === null) { + throw new AssertionFailed(`Field [${this.name}] default for key [${k}] cannot be null.`); + } + + this.guardValue(v); + }); + } + + /** + * @returns {boolean} + */ + hasClassProto() { + return this.classProto !== null && isObject(this.classProto) && !isPlainObject(this.classProto); + } + + /** + * @returns {?Enum|Object|Message|Identifier} + */ + getClassProto() { + return this.classProto; + } + + /** + * @returns {string[]} + */ + getAnyOfCuries() { + return this.anyOfCuries; + } + + /** + * @returns {boolean} + */ + isOverridable() { + return this.overridable; + } + + /** + * @param {*} value + * + * @throws {AssertionFailed} + */ + guardValue(value) { + if (this.required && value === null) { + throw new AssertionFailed(`Field [${this.name}] is required and cannot be null.`); + } + + if (value !== null) { + this.type.guard(value, this); + } + + if (this.assertion) { + this.assertion(value, this); + } + } + + /** + * @returns {string} + */ + toString() { + return JSON.stringify(this); + } + + /** + * @returns {Object} + */ + toObject() { + return { + name: this.name, + type: this.type.getTypeValue(), + rule: this.rule.getName(), + required: this.required, + min_length: this.minLength, + max_length: this.maxLength, + pattern: `${this.pattern}`, + format: this.format.getValue(), + min: this.min, + max: this.max, + precision: this.precision, + scale: this.scale, + default_value: this.defaultValue, + use_type_default: this.useTypeDefault, + class_proto: this.classProto, + any_of_curies: this.anyOfCuries, + has_assertion: this.assertion !== null, + overridable: this.overridable, + }; + } + + /** + * @returns {Object} + */ + toJSON() { + return this.toObject(); + } + + /** + * @returns {string} + */ + valueOf() { + return this.toString(); + } + + /** + * Returns true if this field is likely compatible with the + * provided field during a mergeFrom operation. + * + * todo: implement/test isCompatibleForMerge + * + * @param {Field} other + * + * @returns {boolean} + */ + isCompatibleForMerge(other) { + if (this.name !== other.name) { + return false; + } + + if (this.type !== other.type) { + return false; + } + + if (this.rule !== other.rule) { + return false; + } + + return intersection(this.anyOfCuries, other.anyOfCuries).length; + } + + /** + * Returns true if the provided field can be used as an + * override to this field. + * + * @param {Field} other + * + * @returns {boolean} + */ + isCompatibleForOverride(other) { + if (!this.overridable) { + return false; + } + + if (this.name !== other.name) { + return false; + } + + if (this.type !== other.type) { + return false; + } + + if (this.rule !== other.rule) { + return false; + } + + return this.required === other.required; + } +} diff --git a/src/FieldBuilder.js b/src/FieldBuilder.js new file mode 100644 index 0000000..bb5aaf5 --- /dev/null +++ b/src/FieldBuilder.js @@ -0,0 +1,227 @@ +import toInteger from 'lodash/toInteger'; +import FieldRule from './enums/FieldRule'; +import Field from './Field'; + +export default class FieldBuilder { + /** + * @param {string} name + * @param {Type} type + */ + constructor(name, type) { + this.config = { + name, + type, + rule: FieldRule.A_SINGLE_VALUE, + required: false, + precision: 10, + scale: 2, + useTypeDefault: true, + overridable: false, + }; + } + + /** + * @param {string} name + * @param {Type} type + * + * @returns {FieldBuilder} + */ + static create(name, type) { + return new FieldBuilder(name, type); + } + + /** + * @returns {FieldBuilder} + */ + required() { + this.config.required = true; + return this; + } + + /** + * @returns {FieldBuilder} + */ + optional() { + this.config.required = false; + return this; + } + + /** + * @returns {FieldBuilder} + */ + asASingleValue() { + this.config.rule = FieldRule.A_SINGLE_VALUE; + return this; + } + + /** + * @returns {FieldBuilder} + */ + asASet() { + this.config.rule = FieldRule.A_SET; + return this; + } + + /** + * @returns {FieldBuilder} + */ + asAList() { + this.config.rule = FieldRule.A_LIST; + return this; + } + + /** + * @returns {FieldBuilder} + */ + asAMap() { + this.config.rule = FieldRule.A_MAP; + return this; + } + + /** + * @param {number} minLength + * + * @returns {FieldBuilder} + */ + minLength(minLength) { + this.config.minLength = toInteger(minLength); + return this; + } + + /** + * @param {number} maxLength + * + * @returns {FieldBuilder} + */ + maxLength(maxLength) { + this.config.maxLength = toInteger(maxLength); + return this; + } + + /** + * @param {string} pattern + * + * @returns {FieldBuilder} + */ + pattern(pattern) { + this.config.pattern = pattern; + return this; + } + + /** + * @param {string} format + * + * @returns {FieldBuilder} + */ + format(format) { + this.config.format = format; + return this; + } + + /** + * @param {number} min + * + * @returns {FieldBuilder} + */ + min(min) { + this.config.min = toInteger(min); + return this; + } + + /** + * @param {number} max + * + * @returns {FieldBuilder} + */ + max(max) { + this.config.max = toInteger(max); + return this; + } + + /** + * @param {number} precision + * + * @returns {FieldBuilder} + */ + precision(precision) { + this.config.precision = toInteger(precision); + return this; + } + + /** + * @param {number} scale + * + * @returns {FieldBuilder} + */ + scale(scale) { + this.config.scale = toInteger(scale); + return this; + } + + /** + * @param {*} defaultValue + * + * @returns {FieldBuilder} + */ + withDefault(defaultValue) { + this.config.defaultValue = defaultValue; + return this; + } + + /** + * @param {boolean} useTypeDefault + * + * @returns {FieldBuilder} + */ + useTypeDefault(useTypeDefault) { + this.config.useTypeDefault = useTypeDefault; + return this; + } + + /** + * @param {Function} classProto + * + * @returns {FieldBuilder} + */ + classProto(classProto) { + this.config.classProto = classProto; + return this; + } + + /** + * @param {string[]} anyOfCuries + * + * @returns {FieldBuilder} + */ + anyOfCuries(anyOfCuries) { + this.config.anyOfCuries = anyOfCuries; + return this; + } + + /** + * @param {Function} assertion + * + * @returns {FieldBuilder} + */ + assertion(assertion) { + this.config.assertion = assertion; + return this; + } + + /** + * @param {boolean} overridable + * + * @returns {FieldBuilder} + */ + overridable(overridable) { + this.config.overridable = overridable; + return this; + } + + /** + * @returns {Field} + */ + build() { + return new Field(Object.assign({}, this.config)); + } +} diff --git a/src/Message.js b/src/Message.js new file mode 100644 index 0000000..d95243a --- /dev/null +++ b/src/Message.js @@ -0,0 +1,852 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ +import isArray from 'lodash/isArray'; +import isBoolean from 'lodash/isBoolean'; +import isEmpty from 'lodash/isEmpty'; +import isMap from 'lodash/isMap'; +import toSafeInteger from 'lodash/toSafeInteger'; +import trim from 'lodash/trim'; +import md5 from 'md5'; +import AssertionFailed from './exceptions/AssertionFailed'; +import FrozenMessageIsImmutable from './exceptions/FrozenMessageIsImmutable'; +import LogicException from './exceptions/LogicException'; +import RequiredFieldNotSet from './exceptions/RequiredFieldNotSet'; +import SchemaNotDefined from './exceptions/SchemaNotDefined'; +import MessageRef from './MessageRef'; +import Schema, { PBJ_FIELD_NAME } from './Schema'; +import JsonSerializer from './serializers/JsonSerializer'; +import ObjectSerializer from './serializers/ObjectSerializer'; + +/** + * Stores all message instances so data is kept + * private and cannot be mutated directly. + * + * @type {WeakMap} + */ +const msgs = new WeakMap(); + +/** + * Schemas only need to be defined once per message. + * This maps contains all references, keyed by the + * message class itself. + * + * @type {Map} + */ +const schemas = new Map(); + +/** + * Ensures a frozen message can't be modified. + * + * @param {Message} message + * + * @throws {FrozenMessageIsImmutable} + */ +function guardFrozenMessage(message) { + if (message.isFrozen()) { + throw new FrozenMessageIsImmutable(message); + } +} + +/** + * Populates the default on a single field if it's not already set + * and the default generated is not a null value or empty array. + * + * @param {Message} message + * @param {Field} field + * + * @returns {boolean} Returns true if a non null/empty default was applied or already present. + */ +function populateDefault(message, field) { + const fieldName = field.getName(); + if (message.has(fieldName)) { + return true; + } + + const defaultValue = field.getDefault(message); + if (defaultValue === null) { + return false; + } + + const msg = msgs.get(message); + + if (field.isASingleValue()) { + msg.data.set(fieldName, defaultValue); + msg.clearedFields.delete(fieldName); + return true; + } + + if (isEmpty(defaultValue)) { + return false; + } + + if (field.isASet()) { + message.addToSet(fieldName, Array.from(defaultValue)); + return true; + } + + msg.data.set(fieldName, defaultValue); + msg.clearedFields.delete(fieldName); + return true; +} + +export default class Message { + /** + * Nothing fancy on new messages... we let the serializers or application code get fancy. + */ + constructor() { + msgs.set(this, { + /** @var {Map} */ + data: new Map(), + + /** + * A set of fields that have been cleared or set to null that + * must be included when serialized so it's clear that the + * value has been unset. + * + * @var {Set} + */ + clearedFields: new Set(), + + /** + * @see Message.freeze + * + * @var {boolean} + */ + isFrozen: false, + + /** + * @see Message.isReplay + * + * @var {?boolean} + */ + isReplay: null, + }); + } + + /** + * @returns {Schema} + * + * @throws {SchemaNotDefined} + */ + static schema() { + if (!schemas.has(this)) { + const schema = this.defineSchema(); + + if (!(schema instanceof Schema)) { + throw new SchemaNotDefined( + `Message [${this.name}] must return a Schema from the defineSchema method.`, + ); + } + + if (schema.getClassProto() !== this) { + throw new SchemaNotDefined( + `Schema [${schema.getId()}] returned from defineSchema must be for class [${this.name}], not [${schema.getClassProto().name}].`, + ); + } + + schemas.set(this, schema); + return schema; + } + + return schemas.get(this); + } + + /** + * @private + * + * @returns {Schema} + * + * @throws {SchemaNotDefined} + */ + static defineSchema() { + throw new SchemaNotDefined(`Message [${this.constructor.name}] must return a Schema from the defineSchema method.`); + } + + /** + * @returns {Schema} + */ + schema() { + return this.constructor.schema(); + } + + /** + * Creates a new message with the defaults populated. + * + * @returns {Message} + */ + static create() { + const message = new this(); + return message.populateDefaults(); + } + + /** + * Returns a new message from the provided object using the ObjectSerializer. + * @see ObjectSerializer.deserialize + * + * @param {Object} obj + * + * @returns {Message} + * + * @throws {AssertionFailed} + */ + static fromObject(obj = {}) { + if (!obj[PBJ_FIELD_NAME]) { + // eslint-disable-next-line no-param-reassign + obj[PBJ_FIELD_NAME] = this.schema().getId().toString(); + } + + return ObjectSerializer.deserialize(obj); + } + + /** + * Generates an md5 hash of the json representation of the current message. + * + * @param {string[]} ignoredFields + * + * @returns {string} + */ + generateEtag(ignoredFields = []) { + const obj = ObjectSerializer.serialize(this, { includeAllFields: true }); + if (!ignoredFields.length) { + return md5(JSON.stringify(obj)); + } + + ignoredFields.forEach(f => delete obj[f]); + return md5(JSON.stringify(obj)); + } + + /** + * Generates a reference to this message with an optional tag. + * This method must be implemented in the concrete class or a mixin. + * + * @param {?string} tag + * + * @returns {MessageRef} + */ + generateMessageRef(tag = null) { + throw new LogicException('You must implement "generateMessageRef" in your schema.'); + } + + /** + * Returns an object that can be used in a uri template to generate + * a uri/url for this message. + * + * @link https://tools.ietf.org/html/rfc6570 + * @link https://medialize.github.io/URI.js/uri-template.html + * + * @returns {Object} + */ + getUriTemplateVars() { + return {}; + } + + /** + * Verifies all required fields have been populated. + * todo: recursively validate nested messages? + * + * @returns {Message} + * + * @throws {RequiredFieldNotSet} + */ + validate() { + this.schema().getRequiredFields().forEach((field) => { + if (!this.has(field.getName())) { + throw new RequiredFieldNotSet(this, field); + } + }); + + return this; + } + + /** + * Freezes the message, making it immutable. The message must be valid + * before it can be frozen so this may throw an exception if some required + * fields have not been populated. + * + * @returns {Message} + * + * @throws {RequiredFieldNotSet} + */ + freeze() { + if (this.isFrozen()) { + return this; + } + + this.validate(); + const msg = msgs.get(this); + msg.isFrozen = true; + + this.schema().getFields().forEach((field) => { + if (!field.getType().isMessage()) { + return; + } + + /** @var {Message|Message[]} value */ + const value = this.get(field.getName()); + if (value instanceof Message) { + value.freeze(); + return; + } + + if (isEmpty(value)) { + return; + } + + if (field.isAMap()) { + Object.keys(value).forEach(k => value[k].freeze()); + return; + } + + value.forEach(m => m.freeze()); + }); + + return this; + } + + /** + * Returns true if the message has been frozen. A frozen message is + * immutable and cannot be modified. + * + * @returns {boolean} + */ + isFrozen() { + return msgs.get(this).isFrozen; + } + + /** + * Returns true if the data of the message matches. + * + * @param {Message} other + * + * @returns {boolean} + */ + equals(other) { + // This could probably use some work. :) low level serialization string match. + return `${this}` === `${other}`; + } + + /** + * Returns true if this message is being replayed. Providing a value + * will set the flag but this can only be done once. Note that + * setting a message as being "replayed" will also freeze the message. + * + * @param {?boolean} replay + * + * @returns {boolean} + * + * @throws {LogicException} + */ + isReplay(replay = null) { + const msg = msgs.get(this); + + if (replay === null) { + if (msg.isReplay === null) { + msg.isReplay = false; + } + + return msg.isReplay; + } + + if (msg.isReplay === null) { + msg.isReplay = isBoolean(replay) ? replay : false; + if (msg.isReplay) { + this.freeze(); + } + + return msg.isReplay; + } + + throw new LogicException('You can only set the replay mode "on" one time.'); + } + + /** + * Populates the defaults on all fields or just the fieldName provided. + * Operation will NOT overwrite any fields already set. + * + * @param {?string} fieldName + * + * @returns {Message} + */ + populateDefaults(fieldName = null) { + guardFrozenMessage(this); + + if (!isEmpty(fieldName)) { + populateDefault(this, this.schema().getField(fieldName)); + return this; + } + + this.schema().getFields().forEach(field => populateDefault(this, field)); + return this; + } + + /** + * Returns true if the field has been populated. + * + * @param {string} fieldName + * + * @returns {boolean} + */ + has(fieldName) { + const msg = msgs.get(this); + if (!msg.data.has(fieldName)) { + return false; + } + + const value = msg.data.get(fieldName); + if (isArray(value) || isMap(value)) { + return !isEmpty(value); + } + + return value !== null && value !== undefined; + } + + /** + * Returns the value for the given field. If the field has not + * been set you will get a null value. + * + * @param {string} fieldName + * @param {*} defaultValue + * + * @returns {*} + */ + get(fieldName, defaultValue = null) { + if (!this.has(fieldName)) { + return defaultValue; + } + + const field = this.schema().getField(fieldName); + const msg = msgs.get(this); + + if (field.isASingleValue()) { + return msg.data.get(fieldName); + } + + if (field.isAList()) { + return [...msg.data.get(fieldName)]; + } + + // a set is stored as a Map internally but really + // is just a simple array when serialized. + if (field.isASet()) { + return Array.from(msg.data.get(fieldName).values()); + } + + // maps must return as a plain object. + const obj = {}; + msg.data.get(fieldName).forEach((v, k) => obj[k] = v); // eslint-disable-line no-return-assign + return obj; + } + + /** + * Clears the value of a field. + * + * @param {string} fieldName + * + * @returns {Message} + */ + clear(fieldName) { + guardFrozenMessage(this); + const field = this.schema().getField(fieldName); + const msg = msgs.get(this); + msg.data.delete(fieldName); + msg.clearedFields.add(fieldName); + populateDefault(this, field); + return this; + } + + /** + * Returns true if the field has been cleared. + * + * @param {string} fieldName + * + * @returns {boolean} + */ + hasClearedField(fieldName) { + return msgs.get(this).clearedFields.has(fieldName); + } + + /** + * Returns an array of field names that have been cleared. + * + * @returns {string[]} + */ + getClearedFields() { + return Array.from(msgs.get(this).clearedFields.values()); + } + + /** + * Sets a single value field. + * + * @param {string} fieldName + * @param {*} value + * + * @returns {Message} + * + * @throws {GdbotsPbjException} + */ + set(fieldName, value) { + guardFrozenMessage(this); + const field = this.schema().getField(fieldName); + if (!field.isASingleValue()) { + throw new AssertionFailed(`Field [${fieldName}] must be a single value.`); + } + + if (value === null) { + return this.clear(fieldName); + } + + field.guardValue(value); + const msg = msgs.get(this); + msg.data.set(fieldName, value); + msg.clearedFields.delete(fieldName); + + return this; + } + + /** + * Returns true if the provided value is in the set of values. + * + * @param {string} fieldName + * @param {*} value + * + * @returns {boolean} + */ + isInSet(fieldName, value) { + if (!this.has(fieldName)) { + return false; + } + + /** @var {string} key */ + const key = trim(value); + if (!key.length) { + return false; + } + + return msgs.get(this).data.get(fieldName).has(key.toLowerCase()); + } + + /** + * Adds an array of unique values to an unsorted set of values. + * + * @param {string} fieldName + * @param {Array} values + * + * @returns {Message} + * + * @throws {GdbotsPbjException} + */ + addToSet(fieldName, values) { + guardFrozenMessage(this); + const field = this.schema().getField(fieldName); + if (!field.isASet()) { + throw new AssertionFailed(`Field [${fieldName}] must be a set.`); + } + + const msg = msgs.get(this); + if (!msg.data.has(fieldName)) { + msg.data.set(fieldName, new Map()); + } + + const store = msg.data.get(fieldName); + values.forEach((value) => { + /** @var {string} key */ + const key = trim(value); + if (!key.length) { + return; + } + + field.guardValue(value); + store.set(key.toLowerCase(), value); + }); + + if (store.size) { + msg.clearedFields.delete(fieldName); + } + + return this; + } + + /** + * Removes an array of values from a set. + * + * @param {string} fieldName + * @param {Array} values + * + * @returns {Message} + * + * @throws {GdbotsPbjException} + */ + removeFromSet(fieldName, values) { + guardFrozenMessage(this); + const field = this.schema().getField(fieldName); + if (!field.isASet()) { + throw new AssertionFailed(`Field [${fieldName}] must be a set.`); + } + + const msg = msgs.get(this); + if (!msg.data.has(fieldName)) { + msg.clearedFields.add(fieldName); + return this; + } + + const store = msg.data.get(fieldName); + values.forEach((value) => { + /** @var {string} key */ + const key = trim(value); + if (!key.length) { + return; + } + + store.delete(key.toLowerCase()); + }); + + if (!store.size) { + msg.data.delete(fieldName); + msg.clearedFields.add(fieldName); + } + + return this; + } + + /** + * Returns true if the provided value is in the list of values. + * Uses SameValueZero for comparison. + * + * @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes?v=control + * + * @param {string} fieldName + * @param {*} value + * + * @returns {boolean} + */ + isInList(fieldName, value) { + if (!this.has(fieldName)) { + return false; + } + + return msgs.get(this).data.get(fieldName).includes(value); + } + + /** + * Returns an item in a list or null if it doesn't exist. + * + * @param {string} fieldName + * @param {number} index + * @param {*} defaultValue + * + * @returns {*} + */ + getFromListAt(fieldName, index, defaultValue = null) { + if (!this.has(fieldName)) { + return defaultValue; + } + + const value = msgs.get(this).data.get(fieldName)[toSafeInteger(index)]; + return value === undefined ? defaultValue : value; + } + + /** + * Adds an array of values to an unsorted list/array (not unique). + * + * @param {string} fieldName + * @param {Array} values + * + * @returns {Message} + * + * @throws {GdbotsPbjException} + */ + addToList(fieldName, values) { + guardFrozenMessage(this); + const field = this.schema().getField(fieldName); + if (!field.isAList()) { + throw new AssertionFailed(`Field [${fieldName}] must be a list.`); + } + + const msg = msgs.get(this); + if (!msg.data.has(fieldName)) { + msg.data.set(fieldName, []); + } + + const store = msg.data.get(fieldName); + values.forEach((value) => { + field.guardValue(value); + store.push(value); + }); + + if (store.length) { + msg.clearedFields.delete(fieldName); + } + + return this; + } + + /** + * Removes the element from the array at the index. + * + * @param {string} fieldName + * @param {number} index + * + * @returns {Message} + * + * @throws {GdbotsPbjException} + */ + removeFromListAt(fieldName, index) { + guardFrozenMessage(this); + const field = this.schema().getField(fieldName); + if (!field.isAList()) { + throw new AssertionFailed(`Field [${fieldName}] must be a list.`); + } + + const msg = msgs.get(this); + if (!msg.data.has(fieldName)) { + msg.clearedFields.add(fieldName); + return this; + } + + const store = msg.data.get(fieldName); + store.splice(toSafeInteger(index), 1); + + if (!store.length) { + msg.data.delete(fieldName); + msg.clearedFields.add(fieldName); + } + + return this; + } + + /** + * Returns true if the map contains the provided key. + * + * @param {string} fieldName + * @param {string} key + * + * @returns {boolean} + */ + isInMap(fieldName, key) { + if (!this.has(fieldName)) { + return false; + } + + return msgs.get(this).data.get(fieldName).has(key); + } + + /** + * Returns the value of a key in a map or null if it doesn't exist. + * + * @param {string} fieldName + * @param {string} key + * @param {*} defaultValue + * + * @returns {*} + */ + getFromMap(fieldName, key, defaultValue = null) { + if (!this.isInMap(fieldName, key)) { + return defaultValue; + } + + return msgs.get(this).data.get(fieldName).get(key); + } + + /** + * Adds a key/value pair to a map. + * + * @param {string} fieldName + * @param {string} key + * @param {*} value + * + * @returns {Message} + * + * @throws {GdbotsPbjException} + */ + addToMap(fieldName, key, value) { + guardFrozenMessage(this); + const field = this.schema().getField(fieldName); + if (!field.isAMap()) { + throw new AssertionFailed(`Field [${fieldName}] must be a map.`); + } + + if (value === null) { + return this.removeFromMap(fieldName, key); + } + + const msg = msgs.get(this); + if (!msg.data.has(fieldName)) { + msg.data.set(fieldName, new Map()); + } + + const store = msg.data.get(fieldName); + field.guardValue(value); + store.set(key, value); + msg.clearedFields.delete(fieldName); + + return this; + } + + /** + * Removes a key/value pair from a map. + * + * @param {string} fieldName + * @param {string} key + * + * @returns {Message} + * + * @throws {GdbotsPbjException} + */ + removeFromMap(fieldName, key) { + guardFrozenMessage(this); + const field = this.schema().getField(fieldName); + if (!field.isAMap()) { + throw new AssertionFailed(`Field [${fieldName}] must be a map.`); + } + + const msg = msgs.get(this); + if (!msg.data.has(fieldName)) { + msg.clearedFields.add(fieldName); + return this; + } + + const store = msg.data.get(fieldName); + store.delete(key); + + if (!store.size) { + msg.data.delete(fieldName); + msg.clearedFields.add(fieldName); + } + + return this; + } + + /** + * @returns {string} + */ + toString() { + return JsonSerializer.serialize(this); + } + + /** + * @returns {Object} + */ + toObject() { + return ObjectSerializer.serialize(this); + } + + /** + * @returns {Object} + */ + toJSON() { + return this.toObject(); + } + + /** + * @returns {string} + */ + valueOf() { + return this.toString(); + } + + /** + * @returns {Message} + */ + clone() { + return ObjectSerializer.deserialize(ObjectSerializer.serialize(this)); + } +} diff --git a/src/MessageRef.js b/src/MessageRef.js new file mode 100644 index 0000000..a46a27f --- /dev/null +++ b/src/MessageRef.js @@ -0,0 +1,175 @@ +/* eslint-disable no-useless-escape */ + +import trim from 'lodash/trim'; +import AssertionFailed from './exceptions/AssertionFailed'; +import SchemaCurie from './SchemaCurie'; + +/** + * Regular expression pattern for matching a valid id string. + * @type {RegExp} + */ +export const VALID_ID_PATTERN = /^[\w\/\.:-]+$/; + +/** + * Represents a reference to a message. Typically used to link messages + * together via a correlator or "links". Format for a reference: + * vendor:package:category:message:id#tag (tag is optional) + */ +export default class MessageRef { + /** + * @param {SchemaCurie} curie - A curie which fully qualifies what this reference is linked to. + * @param {string} id - Identifier to the message. + * @param {?string} tag - Tag/relationship qualifier for this ref. + * NOTE: Tag is automatically normalized to a slug-formatted-string. + * + * @throws {AssertionFailed} + */ + constructor(curie, id, tag = null) { + this.curie = curie; + // note: this is left to match php lib which at one point had literal 'null' values + // in the id to account for (de)serialization failures. in some future version this + // should be removed, it's very rare. + this.id = trim(id) || 'null'; + this.tag = trim(tag) || null; + + if (!VALID_ID_PATTERN.test(this.id)) { + throw new AssertionFailed(`MessageRef.id [${this.id}] is invalid. It must match the pattern [${VALID_ID_PATTERN}].`); + } + + if (this.tag !== null) { + this.tag = this.tag.replace(/[^\w\.-]/g, '-').toLowerCase(); + } + + if (this.curie.isMixin()) { + throw new AssertionFailed('Mixins cannot be used in a MessageRef.'); + } + + Object.freeze(this); + } + + /** + * @param {string} str + * + * @returns {MessageRef} + */ + static fromString(str) { + const [ref, tag = null] = str.split('#'); + const [vendor, pkg, category, message, ...id] = ref.split(':'); + const curie = SchemaCurie.fromString(`${vendor}:${pkg}:${category}:${message}`); + return new MessageRef(curie, id.join(':'), tag); + } + + /** + * @param {string} json + * + * @returns {MessageRef} + * + * @throws {AssertionFailed} + */ + static fromJSON(json) { + let obj; + + try { + obj = JSON.parse(json); + } catch (e) { + throw new AssertionFailed('Invalid JSON.'); + } + + return MessageRef.fromObject(obj); + } + + /** + * @param {Object} obj + * + * @returns {MessageRef} + * + * @throws {AssertionFailed} + */ + static fromObject(obj = {}) { + if (obj.curie && obj.id) { + return new MessageRef(SchemaCurie.fromString(obj.curie), obj.id, obj.tag || null); + } + + throw new AssertionFailed('MessageRef is invalid.'); + } + + /** + * @returns {SchemaCurie} + */ + getCurie() { + return this.curie; + } + + /** + * @returns {boolean} + */ + hasId() { + return this.id !== 'null'; + } + + /** + * @returns {?string} + */ + getId() { + return this.hasId() ? this.id : null; + } + + /** + * @returns {boolean} + */ + hasTag() { + return this.tag !== null; + } + + /** + * @returns {?string} + */ + getTag() { + return this.tag; + } + + /** + * @returns {string} + */ + toString() { + if (this.hasTag()) { + return `${this.curie}:${this.id}#${this.tag}`; + } + + return `${this.curie}:${this.id}`; + } + + /** + * @returns {Object} + */ + toObject() { + if (this.hasTag()) { + return { curie: this.curie.toString(), id: this.id, tag: this.tag }; + } + + return { curie: this.curie.toString(), id: this.id }; + } + + /** + * @returns {Object} + */ + toJSON() { + return this.toObject(); + } + + /** + * @returns {string} + */ + valueOf() { + return this.toString(); + } + + /** + * @param {MessageRef} other + * + * @returns {boolean} + */ + equals(other) { + return `${this}` === `${other}`; + } +} diff --git a/src/MessageResolver.js b/src/MessageResolver.js new file mode 100644 index 0000000..cb9d2e6 --- /dev/null +++ b/src/MessageResolver.js @@ -0,0 +1,219 @@ +import MoreThanOneMessageForMixin from './exceptions/MoreThanOneMessageForMixin'; +import NoMessageForCurie from './exceptions/NoMessageForCurie'; +import NoMessageForMixin from './exceptions/NoMessageForMixin'; +import NoMessageForQName from './exceptions/NoMessageForQName'; +import NoMessageForSchemaId from './exceptions/NoMessageForSchemaId'; +import SchemaCurie from './SchemaCurie'; +import SchemaId from './SchemaId'; + +/** + * A map of all the available messages keyed by the schema resolver key + * and curies for resolution that is only major version specific. + * + * @type {Map} + */ +const messages = new Map(); + +/** + * An map of resolved messages in this request/process. + * + * @type {Map} + */ +const resolved = new Map(); + +/** + * A map of resolved lookups by mixin, keyed by the mixin id with major rev + * and optionally a package and category (for faster lookups) + * @see SchemaId.getCurieMajor + * + * @type {Map} + */ +const resolvedMixins = new Map(); + +/** + * A map of resolved lookups by qname. + * + * @type {Map} + */ +const resolvedQnames = new Map(); + +export default class MessageResolver { + /** + * Returns all of the registered messages. + * + * @returns {Message[]} + */ + static all() { + return Array.from(messages.values()); + } + + /** + * Returns the message to be used for the provided schema id. + * + * @param {SchemaId} id + * + * @returns {Message} + * + * @throws {NoMessageForSchemaId} + */ + static resolveId(id) { + const curieMajor = id.getCurieMajor(); + if (resolved.has(curieMajor)) { + return resolved.get(curieMajor); + } + + if (messages.has(curieMajor)) { + const message = messages.get(curieMajor); + resolved.set(curieMajor, message); + return message; + } + + const curie = id.getCurie().toString(); + if (messages.has(curie)) { + const message = messages.get(curie); + resolved.set(curieMajor, message); + resolved.set(curie, message); + return message; + } + + throw new NoMessageForSchemaId(id); + } + + /** + * Returns the message to be used for the provided curie. + * + * @param {SchemaCurie} curie + * + * @returns {Message} + * + * @throws {NoMessageForCurie} + */ + static resolveCurie(curie) { + const key = curie.toString(); + if (resolved.has(key)) { + return resolved.get(key); + } + + if (messages.has(key)) { + const message = messages.get(key); + resolved.set(key, message); + return message; + } + + throw new NoMessageForCurie(curie); + } + + /** + * Returns the SchemaCurie for the given SchemaQName. + * + * @param {SchemaQName} qname + * + * @returns {SchemaCurie} + * + * @throws {NoMessageForQName} + */ + static resolveQName(qname) { + const key = qname.toString(); + if (resolvedQnames.has(key)) { + return resolvedQnames.get(key); + } + + const qvendor = qname.getVendor(); + const qmessage = qname.getMessage(); + + const keys = Array.from(messages.keys()); + const l = keys.length; + for (let i = 0; i < l; i += 1) { + const [vendor, pkg, category, message] = keys[i].split(':'); + if (qvendor === vendor && qmessage === message) { + const curie = SchemaCurie.fromString(`${vendor}:${pkg}:${category}:${message}`); + resolvedQnames.set(key, curie); + return curie; + } + } + + throw new NoMessageForQName(qname); + } + + /** + * Adds a single schema and class proto to the resolver. + * @see SchemaId.getCurieMajor + * + * @param {SchemaId|string} id - A SchemaId instance, curie string or curie major string. + * @param {Message} classProto - The Message class itself, not an instance. + */ + static register(id, classProto) { + const key = id instanceof SchemaId ? id.getCurieMajor() : `${id}`; + messages.set(key, classProto); + } + + /** + * Return the one schema expected to be using the provided mixin. + * + * @param {Mixin} mixin + * @param {?string} inPackage + * @param {?string} inCategory + * + * @returns {Schema} + * + * @throws {MoreThanOneMessageForMixin} + * @throws {NoMessageForMixin} + */ + static findOneUsingMixin(mixin, inPackage = null, inCategory = null) { + const schemas = this.findAllUsingMixin(mixin, inPackage, inCategory); + if (schemas.length !== 1) { + throw new MoreThanOneMessageForMixin(mixin, schemas); + } + + return schemas[0]; + } + + /** + * Returns an array of Schemas expected to be using the provided mixin. + * + * @param {Mixin} mixin + * @param {?string} inPackage + * @param {?string} inCategory + * + * @return {Schema[]} + * + * @throws {NoMessageForMixin} + */ + static findAllUsingMixin(mixin, inPackage = null, inCategory = null) { + const mixinId = mixin.getId().getCurieMajor(); + const key = `${mixinId}${inPackage}${inCategory}`; + let schemas; + + if (!resolvedMixins.has(key)) { + const filtered = inPackage || inCategory; + schemas = []; + messages.forEach((message, id) => { + if (filtered) { + const [, pkg, category] = id.split(':'); + if (inPackage && inPackage !== pkg) { + return; + } + + if (inCategory && inCategory !== category) { + return; + } + } + + const schema = message.schema(); + if (schema.hasMixin(mixinId)) { + schemas.push(schema); + } + }); + + resolvedMixins.set(key, schemas); + } else { + schemas = resolvedMixins.get(key); + } + + if (!schemas.length) { + throw new NoMessageForMixin(mixin); + } + + return schemas; + } +} diff --git a/src/Mixin.js b/src/Mixin.js new file mode 100644 index 0000000..a3705fc --- /dev/null +++ b/src/Mixin.js @@ -0,0 +1,82 @@ +/* eslint-disable class-methods-use-this */ +import LogicException from './exceptions/LogicException'; + +/** + * We store all Mixin instances to accomplish a loose flyweight strategy. + * Loose because we're not strictly enforcing it, but internally in this + * library we only use the factory create method to create mixins. + * + * @type {Map} + */ +const instances = new Map(); + +export default class Mixin { + constructor() { + Object.freeze(this); + } + + /** + * @returns {Mixin} + */ + static create() { + if (!instances.has(this)) { + instances.set(this, new this()); + } + + return instances.get(this); + } + + /** + * @returns {SchemaId} + */ + getId() { + throw new LogicException('You must implement "getId" in your Mixin.'); + } + + /** + * @returns {Field[]} + */ + getFields() { + return []; + } + + /** + * @returns {string} + */ + toString() { + return this.getId().toString(); + } + + /** + * @returns {Object} + */ + toObject() { + return { + id: this.getId(), + fields: this.getFields(), + }; + } + + /** + * @returns {Object} + */ + toJSON() { + return this.toObject(); + } + + /** + * @returns {string} + */ + valueOf() { + return this.toString(); + } + + /** + * @param {Mixin} other + * + * @returns {boolean} + */ + equals(other) { + return `${this}` === `${other}`; + } +} diff --git a/src/Schema.js b/src/Schema.js new file mode 100644 index 0000000..f617315 --- /dev/null +++ b/src/Schema.js @@ -0,0 +1,310 @@ +import camelCase from 'lodash/camelCase'; +import isEmpty from 'lodash/isEmpty'; +import upperFirst from 'lodash/upperFirst'; +import FieldAlreadyDefined from './exceptions/FieldAlreadyDefined'; +import FieldNotDefined from './exceptions/FieldNotDefined'; +import FieldOverrideNotCompatible from './exceptions/FieldOverrideNotCompatible'; +import MixinAlreadyAdded from './exceptions/MixinAlreadyAdded'; +import MixinNotDefined from './exceptions/MixinNotDefined'; +import Fb from './FieldBuilder'; +import SchemaId, { VALID_PATTERN } from './SchemaId'; +import StringType from './types/StringType'; + +export const PBJ_FIELD_NAME = '_schema'; + +export default class Schema { + /** + * @param {SchemaId|string} id + * @param {Message} classProto + * @param {Field[]} fields + * @param {Mixin[]} mixins + */ + constructor(id, classProto, fields = [], mixins = []) { + this.id = id instanceof SchemaId ? id : SchemaId.fromString(id); + this.classProto = classProto; + this.fields = new Map(); + this.requiredFields = new Map(); + this.mixins = new Map(); + this.mixinsByCurie = new Map(); + this.classNameMethod = camelCase(this.id.getCurie().getMessage()); + this.classNameMethodMajor = `${this.classNameMethod}V${this.id.getVersion().getMajor()}`; + this.className = upperFirst(this.classNameMethodMajor); + + this.addField( + Fb.create(PBJ_FIELD_NAME, StringType.create()) + .required() + .pattern(VALID_PATTERN) + .withDefault(this.id.toString()) + .build(), + ); + + mixins.forEach(m => this.addMixin(m)); + fields.forEach(f => this.addField(f)); + + this.mixinIds = Array.from(this.mixins.keys()); + this.mixinCuries = Array.from(this.mixinsByCurie.keys()); + + Object.freeze(this); + } + + /** + * @private + * + * @param {Field} field + * + * @throws {FieldAlreadyDefined} + * @throws {FieldOverrideNotCompatible} + */ + addField(field) { + const fieldName = field.getName(); + + if (this.hasField(fieldName)) { + const existingField = this.getField(fieldName); + if (!existingField.isOverridable()) { + throw new FieldAlreadyDefined(this, fieldName); + } + + if (!existingField.isCompatibleForOverride(field)) { + throw new FieldOverrideNotCompatible(this, fieldName, field); + } + } + + this.fields.set(fieldName, field); + if (field.isRequired()) { + this.requiredFields.set(fieldName, field); + } + } + + /** + * @private + * + * @param {Mixin} mixin + * + * @throws {MixinAlreadyAdded} + */ + addMixin(mixin) { + const id = mixin.getId(); + const curieStr = id.getCurie().toString(); + + if (this.mixinsByCurie.has(curieStr)) { + throw new MixinAlreadyAdded(this, this.mixinsByCurie.get(curieStr), mixin); + } + + this.mixins.set(id.getCurieMajor(), mixin); + this.mixinsByCurie.set(curieStr, mixin); + mixin.getFields().forEach(f => this.addField(f)); + } + + /** + * @returns {Message} + */ + getClassProto() { + return this.classProto; + } + + /** + * @returns {string} + */ + getClassName() { + return this.className; + } + + /** + * Convenience method to return the name of the method that should + * exist to handle this message. + * + * For example, an ImportUserV1 message would be handled by: + * SomeClass.importUserV1(command) + * + * @param {boolean} withMajor + * + * @returns {string} + */ + getHandlerMethodName(withMajor = false) { + return withMajor ? this.classNameMethodMajor : this.classNameMethod; + } + + /** + * @returns {SchemaId} + */ + getId() { + return this.id; + } + + /** + * @returns {SchemaCurie} + */ + getCurie() { + return this.id.getCurie(); + } + + /** + * @returns {string} + */ + getCurieMajor() { + return this.id.getCurieMajor(); + } + + /** + * @returns {SchemaQName} + */ + getQName() { + return this.id.getCurie().getQName(); + } + + /** + * Convenience method that creates a message instance with this schema. + * + * @param {Object} data + * + * @returns {Message} + */ + createMessage(data = {}) { + if (isEmpty(data)) { + return this.classProto.create(); + } + + return this.classProto.fromObject(data); + } + + /** + * @param {string} fieldName + * + * @returns {boolean} + */ + hasField(fieldName) { + return this.fields.has(fieldName); + } + + /** + * @param {string} fieldName + * + * @returns {Field} + * + * @throws {FieldNotDefined} + */ + getField(fieldName) { + if (!this.fields.has(fieldName)) { + throw new FieldNotDefined(this, fieldName); + } + + return this.fields.get(fieldName); + } + + /** + * @returns {Field[]} + */ + getFields() { + return Array.from(this.fields.values()); + } + + /** + * @returns {Field[]} + */ + getRequiredFields() { + return Array.from(this.requiredFields.values()); + } + + /** + * Returns true if the mixin is on this schema. Id provided can be + * qualified to major rev or just the curie. + * @see SchemaId.getCurieMajor + * + * @param {string} mixinId + * + * @returns {boolean} + */ + hasMixin(mixinId) { + return this.mixins.has(mixinId) || this.mixinsByCurie.has(mixinId); + } + + /** + * @param {string} mixinId + * + * @returns {Mixin} + * + * @throws {MixinNotDefined} + */ + getMixin(mixinId) { + if (this.mixins.has(mixinId)) { + return this.mixins.get(mixinId); + } + + if (this.mixinsByCurie.has(mixinId)) { + return this.mixinsByCurie.get(mixinId); + } + + throw new MixinNotDefined(this, mixinId); + } + + /** + * @returns {Mixin[]} + */ + getMixins() { + return Array.from(this.mixins.values()); + } + + /** + * Returns an array of curies with the major rev. + * @see SchemaId.getCurieMajor + * + * @returns {string[]} + */ + getMixinIds() { + return this.mixinIds; + } + + /** + * Returns an array of curies (string version). + * + * @returns {string[]} + */ + getMixinCuries() { + return this.mixinCuries; + } + + /** + * @returns {string} + */ + toString() { + return this.id.toString(); + } + + /** + * @returns {Object} + */ + toObject() { + return { + id: this.id.toString(), + curie: this.getCurie().toString(), + curie_major: this.getCurieMajor(), + qname: this.getQName().toString(), + class_name: this.className, + mixins: this.getMixins().map(m => m.getId()), + fields: this.getFields(), + }; + } + + /** + * @returns {Object} + */ + toJSON() { + return this.toObject(); + } + + /** + * @returns {string} + */ + valueOf() { + return this.toString(); + } + + /** + * @param {Schema} other + * + * @returns {boolean} + */ + equals(other) { + return `${this}` === `${other}`; + } +} diff --git a/src/SchemaCurie.js b/src/SchemaCurie.js new file mode 100644 index 0000000..2dbaef9 --- /dev/null +++ b/src/SchemaCurie.js @@ -0,0 +1,157 @@ +/* eslint-disable no-useless-escape */ + +import InvalidSchemaCurie from './exceptions/InvalidSchemaCurie'; +import SchemaQName from './SchemaQName'; + +/** + * We store all SchemaCurie instances to accomplish a loose flyweight strategy. + * Loose because we're not strictly enforcing it, but internally in this + * library we only use the factory from* methods to create curies. + * + * @type {Map} + */ +const instances = new Map(); + +/** + * Regular expression pattern for matching a valid SchemaCurie string. + * @type {RegExp} + */ +export const VALID_PATTERN = /^([a-z0-9-]+):([a-z0-9\.-]+):([a-z0-9-]+)?:([a-z0-9-]+)$/; + +/** + * Schemas can be fully qualified by the schema id (which includes the version) + * or the short form which is called a CURIE or "compact uri". + * @link http://en.wikipedia.org/wiki/CURIE + * + * Schema Curie Format: + * vendor:package:category:message + * + * @see SchemaId + * + */ +export default class SchemaCurie { + /** + * @param {string} vendor + * @param {string} pkg + * @param {?string} category + * @param {string} message + * + * @throws {InvalidSchemaCurie} + */ + constructor(vendor, pkg, category, message) { + this.vendor = vendor || ''; + this.pkg = pkg || ''; + this.category = `${category}`.trim() || null; + this.message = message || ''; + this.curie = `${this.vendor}:${this.pkg}:${this.category || ''}:${this.message}`; + + if (!VALID_PATTERN.test(this.curie)) { + throw new InvalidSchemaCurie( + `SchemaCurie [${this.curie}] is invalid. It must match the pattern [${VALID_PATTERN}].`, + ); + } + + if (this.curie.length > 145) { + throw new InvalidSchemaCurie('SchemaCurie cannot be greater than 145 chars.'); + } + + this.qname = SchemaQName.fromCurie(this); + Object.freeze(this); + instances.set(this.curie, this); + } + + /** + * @param {string} curie + * + * @returns {SchemaCurie} + */ + static fromString(curie) { + const key = `${curie}`; + if (instances.has(key)) { + return instances.get(key); + } + + return new SchemaCurie(...key.split(':')); + } + + /** + * @param {SchemaId} id + * + * @returns {SchemaCurie} + */ + static fromId(id) { + return SchemaCurie.fromString(id.toString().replace(`:${id.getVersion()}`, '').substr(4)); + } + + /** + * @returns {string} + */ + getVendor() { + return this.vendor; + } + + /** + * @returns {string} + */ + getPackage() { + return this.pkg; + } + + /** + * @returns {?string} + */ + getCategory() { + return this.category; + } + + /** + * @returns {string} + */ + getMessage() { + return this.message; + } + + /** + * @returns {boolean} + */ + isMixin() { + return this.category === 'mixin'; + } + + /** + * @return {SchemaQName} + */ + getQName() { + return this.qname; + } + + /** + * @returns {string} + */ + toString() { + return this.curie; + } + + /** + * @returns {string} + */ + toJSON() { + return this.toString(); + } + + /** + * @returns {string} + */ + valueOf() { + return this.toString(); + } + + /** + * @param {SchemaCurie} other + * + * @returns {boolean} + */ + equals(other) { + return `${this}` === `${other}`; + } +} diff --git a/src/SchemaId.js b/src/SchemaId.js new file mode 100644 index 0000000..eb4e131 --- /dev/null +++ b/src/SchemaId.js @@ -0,0 +1,212 @@ +/* eslint-disable no-useless-escape */ + +import InvalidSchemaId from './exceptions/InvalidSchemaId'; +import SchemaCurie from './SchemaCurie'; +import SchemaVersion from './SchemaVersion'; + +/** + * We store all SchemaId instances to accomplish a loose flyweight strategy. + * Loose because we're not strictly enforcing it, but internally in this + * library we only use the factory from* methods to create curies. + * + * @type {Map} + */ +const instances = new Map(); + +/** + * Regular expression pattern for matching a valid SchemaId string. + * @type {RegExp} + */ +export const VALID_PATTERN = /^pbj:([a-z0-9-]+):([a-z0-9\.-]+):([a-z0-9-]+)?:([a-z0-9-]+):([0-9]+-[0-9]+-[0-9]+)$/; + +/** + * Schemas have fully qualified names, similar to a "urn". This is combination of ideas from: + * + * Amazon Resource Names (ARNs) and AWS Service Namespaces + * @link http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html + * + * SnowPlow Analytics (Iglu) + * @link http://snowplowanalytics.com/blog/2014/07/01/iglu-schema-repository-released/ + * + * @link http://en.wikipedia.org/wiki/CURIE + * + * And of course the various package managers like composer, npm, etc. + * + * Schema Id Format: + * pbj:vendor:package:category:message:version + * + * Schema Curie Format: + * vendor:package:category:message + * + * Schema Curie Major Format: + * vendor:package:category:message:v# + * + * Schema QName Format: + * vendor:message + * + * Formats: + * VENDOR: [a-z0-9-]+ + * PACKAGE: [a-z0-9\.-]+ + * CATEGORY: ([a-z0-9-]+)? + * (clarifies the intent of the message, e.g. command, request, event, response, etc.) + * + * MESSAGE: [a-z0-9-]+ + * VERSION: @see SchemaVersion VALID_PATTERN + * + * Examples of fully qualified schema ids: + * pbj:acme:videos:event:video-uploaded:1-0-0 + * pbj:acme:users:command:register-user:1-1-0 + * pbj:acme:api.videos:request:get-video:1-0-0 + * + * The fully qualified schema identifier corresponds to a json schema implementing the + * Gdbots PBJ Json Schema. + * + * The schema id must be resolveable to a class that should be able to read and write + * messages with payloads that validate using the json schema. The target class is ideally + * major revision specific. As in GetVideoV1, GetVideoV2, etc. Only "major" revisions + * should require a unique class since all other schema changes should not break anything. + * + * @see SchemaVersion + * + */ +export default class SchemaId { + /** + * @param {string} vendor + * @param {string} pkg + * @param {?string} category + * @param {string} message + * @param {string} version + * + * @throws {InvalidSchemaId} + */ + constructor(vendor, pkg, category, message, version) { + this.vendor = vendor || ''; + this.pkg = pkg || ''; + this.category = `${category}`.trim() || null; + this.message = message || ''; + this.version = SchemaVersion.fromString(version); + this.id = `pbj:${this.vendor}:${this.pkg}:${this.category || ''}:${this.message}:${this.version}`; + + if (!VALID_PATTERN.test(this.id)) { + throw new InvalidSchemaId( + `SchemaId [${this.id}] is invalid. It must match the pattern [${VALID_PATTERN}].`, + ); + } + + if (this.id.length > 150) { + throw new InvalidSchemaId('SchemaId cannot be greater than 150 chars.'); + } + + this.curie = SchemaCurie.fromId(this); + Object.freeze(this); + instances.set(this.id, this); + } + + /** + * @param {string} id + * + * @returns {SchemaId} + */ + static fromString(id) { + const key = `${id}`; + if (instances.has(key)) { + return instances.get(key); + } + + return new SchemaId(...key.substr(4).split(':')); + } + + /** + * @returns {string} + */ + getVendor() { + return this.vendor; + } + + /** + * @returns {string} + */ + getPackage() { + return this.pkg; + } + + /** + * @returns {?string} + */ + getCategory() { + return this.category; + } + + /** + * @returns {string} + */ + getMessage() { + return this.message; + } + + /** + * @returns {SchemaVersion} + */ + getVersion() { + return this.version; + } + + /** + * @returns {SchemaCurie} + */ + getCurie() { + return this.curie; + } + + /** + * Returns the major version qualified curie. This should be used by the MessageResolver, + * event dispatchers, etc. where consumers will need to be able to reliably type hint or + * locate classes and provide functionality for a given message, with the expectation + * that a major revision is likely not compatible with another major revision of the + * same message. + * + * e.g. "vendor:package:category:message:v1" + * + * @returns {string} + */ + getCurieMajor() { + return `${this.curie}:v${this.version.getMajor()}`; + } + + /** + * @return {SchemaQName} + */ + getQName() { + return this.curie.getQName(); + } + + /** + * @returns {string} + */ + toString() { + return this.id; + } + + /** + * @returns {string} + */ + toJSON() { + return this.toString(); + } + + /** + * @returns {string} + */ + valueOf() { + return this.toString(); + } + + /** + * @param {SchemaId} other + * + * @returns {boolean} + */ + equals(other) { + return `${this}` === `${other}`; + } +} diff --git a/src/SchemaQName.js b/src/SchemaQName.js new file mode 100644 index 0000000..46bedaa --- /dev/null +++ b/src/SchemaQName.js @@ -0,0 +1,126 @@ +import InvalidSchemaQName from './exceptions/InvalidSchemaQName'; + +/** + * We store all SchemaQName instances to accomplish a loose flyweight strategy. + * Loose because we're not strictly enforcing it, but internally in this + * library we only use the factory from* methods to create qnames. + * + * @type {Map} + */ +const instances = new Map(); + +/** + * Regular expression pattern for matching a valid SchemaQName string. + * @type {RegExp} + */ +export const VALID_PATTERN = /^([a-z0-9-]+):([a-z0-9-]+)$/; + +/** + * Schemas can be referenced in an extremely compact manner using a QName. + * This is NOT 100% reliably unique as the larger your app is the more likely the + * same message name will be duplicated in another service. + * @link https://en.wikipedia.org/wiki/QName + * + * Schema QName Format: + * vendor:message + * + * @see SchemaId + * @see SchemaCurie + * + */ +export default class SchemaQName { + /** + * @param {string} vendor + * @param {string} message + * + * @throws {InvalidSchemaVersion} + */ + constructor(vendor, message) { + this.vendor = vendor || ''; + this.message = message || ''; + this.qname = `${this.vendor}:${this.message}`; + + if (!VALID_PATTERN.test(this.qname)) { + throw new InvalidSchemaQName( + `SchemaQName [${this.qname}] is invalid. It must match the pattern [${VALID_PATTERN}].`, + ); + } + + Object.freeze(this); + instances.set(this.qname, this); + } + + /** + * @param {string} qname - A valid SchemaQName as a string, e.g. vendor:message + * + * @returns {SchemaQName} + * + * @throws {InvalidSchemaQName} + */ + static fromString(qname) { + const key = `${qname}`; + if (instances.has(key)) { + return instances.get(key); + } + + return new SchemaQName(...key.split(':')); + } + + /** + * @param {SchemaId} id + * + * @returns {SchemaQName} + */ + static fromId(id) { + return SchemaQName.fromCurie(id.getCurie()); + } + + /** + * @param {SchemaCurie} curie + * + * @returns {SchemaQName} + */ + static fromCurie(curie) { + const qname = `${curie.getVendor()}:${curie.getMessage()}`; + if (instances.has(qname)) { + return instances.get(qname); + } + + return new SchemaQName(curie.getVendor(), curie.getMessage()); + } + + /** + * @returns {string} + */ + toString() { + return this.qname; + } + + /** + * @returns {string} + */ + toJSON() { + return this.qname; + } + + /** + * @returns {string} + */ + valueOf() { + return this.qname; + } + + /** + * @returns {string} + */ + getVendor() { + return this.vendor; + } + + /** + * @returns {string} + */ + getMessage() { + return this.message; + } +} diff --git a/src/SchemaVersion.js b/src/SchemaVersion.js new file mode 100644 index 0000000..4eeb6d9 --- /dev/null +++ b/src/SchemaVersion.js @@ -0,0 +1,115 @@ +import toInteger from 'lodash/toInteger'; +import InvalidSchemaVersion from './exceptions/InvalidSchemaVersion'; + +/** + * Regular expression pattern for matching a valid SchemaVersion string. + * @type {RegExp} + */ +export const VALID_PATTERN = /^([0-9]+)-([0-9]+)-([0-9]+)$/; + +/** + * Similar to semantic versioning but with dashes and no "alpha, beta, etc." qualifiers. + * + * E.g. 1-0-0 (major-minor-patch) + * + * MAJOR + * Is incremented when a change is made which breaks the rules of Protobuf/Thrift backward + * compatibility, such as changing the type of a field. + * + * MINOR + * Is a change which is backward compatible but not forward compatible. Records created from + * the old version of the schema can be deserialized using the new schema, but not the other way + * around. Example: adding a new field to a union type. + * + * PATCH + * Is a change which is both backward compatible and forward compatible. The previous version of + * the schema can be used to deserialize records created from the new version of the schema, and + * vice versa. Example: adding a new optional field. + * + * @link http://semver.org/ + * @link http://snowplowanalytics.com/blog/2014/05/13/introducing-schemaver-for-semantic-versioning-of-schemas/ + * + */ +export default class SchemaVersion { + /** + * @param {number} major + * @param {number} minor + * @param {number} patch + * + * @throws {InvalidSchemaVersion} + */ + constructor(major = 1, minor = 0, patch = 0) { + this.major = toInteger(major); + this.minor = toInteger(minor); + this.patch = toInteger(patch); + this.version = `${major}-${minor}-${patch}`; + + if (!VALID_PATTERN.test(this.version)) { + throw new InvalidSchemaVersion( + `SchemaVersion [${this.version}] is invalid. It must match the pattern [${VALID_PATTERN}].`, + ); + } + + Object.freeze(this); + } + + /** + * @param {string} version - A valid SchemaVersion as a string, e.g. 1-0-0 + * + * @returns {SchemaVersion} + * + * @throws {InvalidSchemaVersion} + */ + static fromString(version = '1-0-0') { + const matches = `${version}`.match(VALID_PATTERN); + if (matches === null) { + throw new InvalidSchemaVersion( + `SchemaVersion [${version}] is invalid. It must match the pattern [${VALID_PATTERN}].`, + ); + } + + return new SchemaVersion(matches[1], matches[2], matches[3]); + } + + /** + * @returns {string} + */ + toString() { + return this.version; + } + + /** + * @returns {string} + */ + toJSON() { + return this.version; + } + + /** + * @returns {string} + */ + valueOf() { + return this.version; + } + + /** + * @returns {number} + */ + getMajor() { + return this.major; + } + + /** + * @returns {number} + */ + getMinor() { + return this.minor; + } + + /** + * @returns {number} + */ + getPatch() { + return this.patch; + } +} diff --git a/src/codec.js b/src/codec.js deleted file mode 100644 index 0459920..0000000 --- a/src/codec.js +++ /dev/null @@ -1,89 +0,0 @@ -'use strict'; - -import DynamicField from 'gdbots/pbj/well-known/dynamic-field'; -import GeoPoint from 'gdbots/pbj/well-known/geo-point'; -import Message from 'gdbots/pbj/message'; -import MessageRef from 'gdbots/pbj/message-ref'; - -export default class Codec -{ - /** - * @param Message message - * @param Field field - * - * @return mixed - */ - encodeMessage(message, field) { - throw message.toArray(); - } - - /** - * @param mixed value - * @param Field field - * - * @return Message - */ - decodeMessage(value, field) { - return Message.fromArray(value); - } - - /** - * @param MessageRef messageRef - * @param Field field - * - * @return mixed - */ - encodeMessageRef(messageRef, field) { - return messageRef.toArray(); - } - - /** - * @param mixed value - * @param Field field - * - * @return MessageRef - */ - decodeMessageRef(value, field) { - return MessageRef.fromArray(value); - } - - /** - * @param GeoPoint geoPoint - * @param Field field - * - * @return mixed - */ - encodeGeoPoint(geoPoint, field) { - return [geoPoint.getLongitude(), geoPoint.getLatitude()]; - } - - /** - * @param mixed value - * @param Field field - * - * @return GeoPoint - */ - decodeGeoPoint(value, field) { - return new GeoPoint(value[1], value[0]); - } - - /** - * @param DynamicField dynamicField - * @param Field field - * - * @return mixed - */ - encodeDynamicField(dynamicField, field) { - dynamicField.toArray(); - } - - /** - * @param mixed value - * @param Field field - * - * @return DynamicField - */ - decodeDynamicField(value, field) { - DynamicField.fromArray(value); - } -} diff --git a/src/enum/dynamic-field-kind.js b/src/enum/dynamic-field-kind.js deleted file mode 100644 index a73b9bc..0000000 --- a/src/enum/dynamic-field-kind.js +++ /dev/null @@ -1,23 +0,0 @@ -'use strict'; - -import Enum from 'gdbots/common/enum'; -import SystemUtils from 'gdbots/common/util/system-utils'; - -/** - * @method static DynamicFieldKind BOOL_VAL() - * @method static DynamicFieldKind DATE_VAL() - * @method static DynamicFieldKind FLOAT_VAL() - * @method static DynamicFieldKind INT_VAL() - * @method static DynamicFieldKind STRING_VAL() - * @method static DynamicFieldKind TEXT_VAL() - */ -export default class DynamicFieldKind extends SystemUtils.mixinClass(Enum) {} - -DynamicFieldKind.initEnum({ - BOOL_VAL: 'bool_val', - DATE_VAL: 'date_val', - FLOAT_VAL: 'float_val', - INT_VAL: 'int_val', - STRING_VAL: 'string_val', - TEXT_VAL: 'text_val' -}); diff --git a/src/enum/field-rule.js b/src/enum/field-rule.js deleted file mode 100644 index 28a5d57..0000000 --- a/src/enum/field-rule.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; - -import Enum from 'gdbots/common/enum'; -import SystemUtils from 'gdbots/common/util/system-utils'; - -/** - * @method static FieldRule A_SINGLE_VALUE() - * @method static FieldRule A_SET() - * @method static FieldRule A_LIST() - * @method static FieldRule A_MAP() - */ -export default class FieldRule extends SystemUtils.mixinClass(Enum) {} - -FieldRule.initEnum({ - A_SINGLE_VALUE: 1, - A_SET: 2, - A_LIST: 3, - A_MAP: 4 -}); diff --git a/src/enum/format.js b/src/enum/format.js deleted file mode 100644 index 49d570c..0000000 --- a/src/enum/format.js +++ /dev/null @@ -1,37 +0,0 @@ -'use strict'; - -import Enum from 'gdbots/common/enum'; -import SystemUtils from 'gdbots/common/util/system-utils'; - -/** - * @link http://spacetelescope.github.io/understanding-json-schema/reference/string.html#format - * - * @method static Format UNKNOWN() - * @method static Format DATE() - * @method static Format DATE_TIME() - * @method static Format EMAIL() - * @method static Format HASHTAG() - * @method static Format HOSTNAME() - * @method static Format IPV4() - * @method static Format IPV6() - * @method static Format SLUG() - * @method static Format URI() - * @method static Format URL() - * @method static Format UUID() - */ -export default class Format extends SystemUtils.mixinClass(Enum) {} - -Format.initEnum({ - UNKNOWN: 'unknown', - DATE: 'date', - DATE_TIME: 'date-time', - EMAIL: 'email', - HASHTAG: 'hashtag', - HOSTNAME: 'hostname', - IPV4: 'ipv4', - IPV6: 'ipv6', - SLUG: 'slug', - URI: 'uri', - URL: 'url', - UUID: 'uuid' -}); diff --git a/src/enum/type-name.js b/src/enum/type-name.js deleted file mode 100644 index 962fff4..0000000 --- a/src/enum/type-name.js +++ /dev/null @@ -1,77 +0,0 @@ -'use strict'; - -import Enum from 'gdbots/common/enum'; -import SystemUtils from 'gdbots/common/util/system-utils'; - -/** - * @method static TypeName BIG_INT() - * @method static TypeName BINARY() - * @method static TypeName BLOB() - * @method static TypeName BOOLEAN() - * @method static TypeName DATE() - * @method static TypeName DATE_TIME() - * @method static TypeName DECIMAL() - * @method static TypeName DYNAMIC_FIELD() - * @method static TypeName FLOAT() - * @method static TypeName GEO_POINT() - * @method static TypeName IDENTIFIER() - * @method static TypeName INT() - * @method static TypeName INT_ENUM() - * @method static TypeName MEDIUM_BLOB() - * @method static TypeName MEDIUM_INT() - * @method static TypeName MEDIUM_TEXT() - * @method static TypeName MESSAGE() - * @method static TypeName MESSAGE_REF() - * @method static TypeName MICROTIME() - * @method static TypeName SIGNED_BIG_INT() - * @method static TypeName SIGNED_INT() - * @method static TypeName SIGNED_MEDIUM_INT() - * @method static TypeName SIGNED_SMALL_INT() - * @method static TypeName SIGNED_TINY_INT() - * @method static TypeName SMALL_INT() - * @method static TypeName STRING() - * @method static TypeName STRING_ENUM() - * @method static TypeName TEXT() - * @method static TypeName TIME_UUID() - * @method static TypeName TIMESTAMP() - * @method static TypeName TINY_INT() - * @method static TypeName TRINARY() - * @method static TypeName UUID() - */ -export default class TypeName extends SystemUtils.mixinClass(Enum) {} - -TypeName.initEnum({ - BIG_INT: 'big-int', - BINARY: 'binary', - BLOB: 'blob', - BOOLEAN: 'boolean', - DATE: 'date', - DATE_TIME: 'date-time', - DECIMAL: 'decimal', - DYNAMIC_FIELD: 'dynamic-field', - FLOAT: 'float', - GEO_POINT: 'geo-point', - IDENTIFIER: 'identifier', - INT: 'int', - INT_ENUM: 'int-enum', - MEDIUM_BLOB: 'medium-blob', - MEDIUM_INT: 'medium-int', - MEDIUM_TEXT: 'medium-text', - MESSAGE: 'message', - MESSAGE_REF: 'message-ref', - MICROTIME: 'microtime', - SIGNED_BIG_INT: 'signed-big-int', - SIGNED_INT: 'signed-int', - SIGNED_MEDIUM_INT: 'signed-medium-int', - SIGNED_SMALL_INT: 'signed-small-int', - SIGNED_TINY_INT: 'signed-tiny-int', - SMALL_INT: 'small-int', - STRING: 'string', - STRING_ENUM: 'string-enum', - TEXT: 'text', - TIME_UUID: 'time-uuid', - TIMESTAMP: 'timestamp', - TINY_INT: 'tiny-int', - TRINARY: 'trinary', - UUID: 'uuid' -}); diff --git a/src/enums/DynamicFieldKind.js b/src/enums/DynamicFieldKind.js new file mode 100644 index 0000000..df09e6a --- /dev/null +++ b/src/enums/DynamicFieldKind.js @@ -0,0 +1,13 @@ +import Enum from '@gdbots/common/Enum'; + +export default class DynamicFieldKind extends Enum { +} + +DynamicFieldKind.configure({ + BOOL_VAL: 'bool_val', + DATE_VAL: 'date_val', + FLOAT_VAL: 'float_val', + INT_VAL: 'int_val', + STRING_VAL: 'string_val', + TEXT_VAL: 'text_val', +}, 'gdbots:pbj:dynamic-field-kind'); diff --git a/src/enums/FieldRule.js b/src/enums/FieldRule.js new file mode 100644 index 0000000..46223fa --- /dev/null +++ b/src/enums/FieldRule.js @@ -0,0 +1,11 @@ +import Enum from '@gdbots/common/Enum'; + +export default class FieldRule extends Enum { +} + +FieldRule.configure({ + A_SINGLE_VALUE: 1, + A_SET: 2, + A_LIST: 3, + A_MAP: 4, +}, 'gdbots:pbj:field-rule'); diff --git a/src/enums/Format.js b/src/enums/Format.js new file mode 100644 index 0000000..9417dc0 --- /dev/null +++ b/src/enums/Format.js @@ -0,0 +1,22 @@ +import Enum from '@gdbots/common/Enum'; + +/** + * @link http://spacetelescope.github.io/understanding-json-schema/reference/string.html#format + */ +export default class Format extends Enum { +} + +Format.configure({ + UNKNOWN: 'unknown', + DATE: 'date', + DATE_TIME: 'date-time', + EMAIL: 'email', + HASHTAG: 'hashtag', + HOSTNAME: 'hostname', + IPV4: 'ipv4', + IPV6: 'ipv6', + SLUG: 'slug', + URI: 'uri', + URL: 'url', + UUID: 'uuid', +}, 'gdbots:pbj:format'); diff --git a/src/enums/TypeName.js b/src/enums/TypeName.js new file mode 100644 index 0000000..23bcac2 --- /dev/null +++ b/src/enums/TypeName.js @@ -0,0 +1,40 @@ +import Enum from '@gdbots/common/Enum'; + +export default class TypeName extends Enum { +} + +TypeName.configure({ + BIG_INT: 'big-int', + BINARY: 'binary', + BLOB: 'blob', + BOOLEAN: 'boolean', + DATE: 'date', + DATE_TIME: 'date-time', + DECIMAL: 'decimal', + DYNAMIC_FIELD: 'dynamic-field', + FLOAT: 'float', + GEO_POINT: 'geo-point', + IDENTIFIER: 'identifier', + INT: 'int', + INT_ENUM: 'int-enum', + MEDIUM_BLOB: 'medium-blob', + MEDIUM_INT: 'medium-int', + MEDIUM_TEXT: 'medium-text', + MESSAGE: 'message', + MESSAGE_REF: 'message-ref', + MICROTIME: 'microtime', + SIGNED_BIG_INT: 'signed-big-int', + SIGNED_INT: 'signed-int', + SIGNED_MEDIUM_INT: 'signed-medium-int', + SIGNED_SMALL_INT: 'signed-small-int', + SIGNED_TINY_INT: 'signed-tiny-int', + SMALL_INT: 'small-int', + STRING: 'string', + STRING_ENUM: 'string-enum', + TEXT: 'text', + TIME_UUID: 'time-uuid', + TIMESTAMP: 'timestamp', + TINY_INT: 'tiny-int', + TRINARY: 'trinary', + UUID: 'uuid', +}, 'gdbots:pbj:type-name'); diff --git a/src/exception/decode-value-failed.js b/src/exception/decode-value-failed.js deleted file mode 100644 index f82e222..0000000 --- a/src/exception/decode-value-failed.js +++ /dev/null @@ -1,57 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import InvalidArgumentException from 'gdbots/pbj/exception/invalid-argument-exception'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class DecodeValueFailed extends SystemUtils.mixinClass(InvalidArgumentException) -{ - /** - * @param mixed value - * @param Field field - * @param string message - */ - constructor(value, field, message) { - let str = '' + value; - if ('object' === typeof value) { - str = value.toString(); - } - - super('Failed to decode [' + str + '] for field [' + field.getName() + '] to a [' + field.getType().getTypeValue() + ']. Detail: ' + message + '.'); - - privateProps.set(this, { - /** @var mixed */ - value: value, - - /** @var Field */ - field: field - }); - } - - /** - * @return mixed - */ - getValue() { - return privateProps.get(this).value; - } - - /** - * @return Field - */ - getField() { - return privateProps.get(this).field; - } - - /** - * @return string - */ - getFieldName() { - return privateProps.get(this).field.getName(); - } -} diff --git a/src/exception/deserialize-message-failed.js b/src/exception/deserialize-message-failed.js deleted file mode 100644 index b0985c4..0000000 --- a/src/exception/deserialize-message-failed.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import GdbotsPbjException from 'gdbots/pbj/exception/gdbots-pbj-exception'; - -export default class DeserializeMessageFailed extends SystemUtils.mixinClass(GdbotsPbjException) {} diff --git a/src/exception/encode-value-failed.js b/src/exception/encode-value-failed.js deleted file mode 100644 index 3c4d943..0000000 --- a/src/exception/encode-value-failed.js +++ /dev/null @@ -1,57 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import InvalidArgumentException from 'gdbots/pbj/exception/invalid-argument-exception'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class EncodeValueFailed extends SystemUtils.mixinClass(InvalidArgumentException) -{ - /** - * @param mixed value - * @param Field field - * @param string message - */ - constructor(value, field, message) { - let str = '' + value; - if ('object' === typeof value) { - str = value.toString(); - } - - super('Failed to encode [' + str + '] for field [' + field.getName() + '] to a [' + field.getType().getTypeValue() + ']. Detail: ' + message + '.'); - - privateProps.set(this, { - /** @var mixed */ - value: value, - - /** @var Field */ - field: field - }); - } - - /** - * @return mixed - */ - getValue() { - return privateProps.get(this).value; - } - - /** - * @return Field - */ - getField() { - return privateProps.get(this).field; - } - - /** - * @return string - */ - getFieldName() { - return privateProps.get(this).field.getName(); - } -} diff --git a/src/exception/field-already-defined.js b/src/exception/field-already-defined.js deleted file mode 100644 index cc78f5a..0000000 --- a/src/exception/field-already-defined.js +++ /dev/null @@ -1,46 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import GdbotsPbjException from 'gdbots/pbj/exception/gdbots-pbj-exception'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class FieldAlreadyDefined extends SystemUtils.mixinClass(GdbotsPbjException) -{ - /** - * @param Schema schema - * @param string fieldName - */ - constructor(schema, fieldName) { - let field = schema.getField(fieldName); - - super('Field [' + field.getName() + '] is already defined on message [' + schema.getClassName() + '] and is not overridable.'); - - privateProps.set(this, { - /** @var Schema */ - schema: schema, - - /** @var Field */ - field: field - }); - } - - /** - * @return Field - */ - getField() { - return privateProps.get(this).field; - } - - /** - * @return string - */ - getFieldName() { - return privateProps.get(this).field.getName(); - } -} diff --git a/src/exception/field-not-defined.js b/src/exception/field-not-defined.js deleted file mode 100644 index c4d16c2..0000000 --- a/src/exception/field-not-defined.js +++ /dev/null @@ -1,37 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import GdbotsPbjException from 'gdbots/pbj/exception/gdbots-pbj-exception'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class FieldNotDefined extends SystemUtils.mixinClass(GdbotsPbjException) -{ - /** - * @param Schema schema - * @param string fieldName - */ - constructor(schema, fieldName) { - super('Field [' + fieldName + '] is not defined on message [' + schema.getClassName() + '].'); - - privateProps.set(this, { - /** @var Schema */ - schema: schema, - - /** @var string */ - fieldName: fieldName - }); - } - - /** - * @return string - */ - getFieldName() { - return privateProps.get(this).fieldName; - } -} diff --git a/src/exception/field-override-not-compatible.js b/src/exception/field-override-not-compatible.js deleted file mode 100644 index 0122e8c..0000000 --- a/src/exception/field-override-not-compatible.js +++ /dev/null @@ -1,57 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import GdbotsPbjException from 'gdbots/pbj/exception/gdbots-pbj-exception'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class FieldOverrideNotCompatible extends SystemUtils.mixinClass(GdbotsPbjException) -{ - /** - * @param Schema schema - * @param string fieldName - * @param Field overrideField - */ - constructor(schema, fieldName, overrideField) { - let existingField = schema.getField(fieldName); - - super('Field [' + existingField.getName() + '] override for [' + schema.getClassName() + '] is not compatible. Name, Type, Rule and Required must match.'); - - privateProps.set(this, { - /** @var Schema */ - schema: schema, - - /** @var Field */ - existingField: existingField, - - /** @var Field */ - overrideField: overrideField - }); - } - - /** - * @return Field - */ - getExistingField() { - return privateProps.get(this).existingField; - } - - /** - * @return string - */ - getFieldName() { - return privateProps.get(this).existingField.getName(); - } - - /** - * @return Field - */ - getOverrideField() { - return privateProps.get(this).overrideField; - } -} diff --git a/src/exception/frozen-message-is-immutable.js b/src/exception/frozen-message-is-immutable.js deleted file mode 100644 index 586ad25..0000000 --- a/src/exception/frozen-message-is-immutable.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import GdbotsPbjException from 'gdbots/pbj/exception/gdbots-pbj-exception'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class FrozenMessageIsImmutable extends SystemUtils.mixinClass(GdbotsPbjException) -{ - /** - * @param Message type - */ - constructor(type) { - super('Message is frozen and cannot be modified.'); - - privateProps.set(this, { - /** @var Message */ - type: type - }); - } - - /** - * @return Message - */ - getType() { - return privateProps.get(this).type; - } -} diff --git a/src/exception/gdbots-pbj-exception.js b/src/exception/gdbots-pbj-exception.js deleted file mode 100644 index 62a1a96..0000000 --- a/src/exception/gdbots-pbj-exception.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; - -export default class GdbotsPbjException extends SystemUtils.mixinClass(Error) {} diff --git a/src/exception/invalid-argument-exception.js b/src/exception/invalid-argument-exception.js deleted file mode 100644 index 4f1a926..0000000 --- a/src/exception/invalid-argument-exception.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import GdbotsPbjException from 'gdbots/pbj/exception/gdbots-pbj-exception'; - -export default class InvalidArgumentException extends SystemUtils.mixinClass(GdbotsPbjException) {} diff --git a/src/exception/invalid-resolved-schema.js b/src/exception/invalid-resolved-schema.js deleted file mode 100644 index 387f58f..0000000 --- a/src/exception/invalid-resolved-schema.js +++ /dev/null @@ -1,48 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import GdbotsPbjException from 'gdbots/pbj/exception/gdbots-pbj-exception'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class InvalidResolvedSchema extends SystemUtils.mixinClass(GdbotsPbjException) -{ - /** - * @param Schema schema - * @param SchemaId resolvedSchemaId - * @param string resolvedClassName - */ - constructor(schema, resolvedSchemaId, resolvedClassName) { - super('Schema id [' + resolvedSchemaId.toString() + '] with curie [' + resolvedSchemaId.getCurieMajor() + '] was resolved to [' + resolvedClassName + '] but that message has a curie of [' + schema.getId().getCurieMajor() + ']. They must match.'); - - privateProps.set(this, { - /** @var Schema */ - schema: schema, - - /** @var SchemaId */ - resolvedSchemaId: resolvedSchemaId, - - /** @var string */ - resolvedClassName: resolvedClassName - }); - } - - /** - * @return SchemaId - */ - getResolvedSchemaId() { - return privateProps.get(this).resolvedSchemaId; - } - - /** - * @return string - */ - getResolvedClassName() { - return privateProps.get(this).resolvedClassName; - } -} diff --git a/src/exception/invalid-schema-curie.js b/src/exception/invalid-schema-curie.js deleted file mode 100644 index 8699dca..0000000 --- a/src/exception/invalid-schema-curie.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import InvalidArgumentException from 'gdbots/pbj/exception/invalid-argument-exception'; - -export default class InvalidSchemaCurie extends SystemUtils.mixinClass(InvalidArgumentException) {} diff --git a/src/exception/invalid-schema-id.js b/src/exception/invalid-schema-id.js deleted file mode 100644 index cc0f1bf..0000000 --- a/src/exception/invalid-schema-id.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import InvalidArgumentException from 'gdbots/pbj/exception/invalid-argument-exception'; - -export default class InvalidSchemaId extends SystemUtils.mixinClass(InvalidArgumentException) {} diff --git a/src/exception/invalid-schema-q-name.js b/src/exception/invalid-schema-q-name.js deleted file mode 100644 index e44fbf9..0000000 --- a/src/exception/invalid-schema-q-name.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import InvalidArgumentException from 'gdbots/pbj/exception/invalid-argument-exception'; - -export default class InvalidSchemaQName extends SystemUtils.mixinClass(InvalidArgumentException) {} diff --git a/src/exception/invalid-schema-version.js b/src/exception/invalid-schema-version.js deleted file mode 100644 index 135963d..0000000 --- a/src/exception/invalid-schema-version.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import InvalidArgumentException from 'gdbots/pbj/exception/invalid-argument-exception'; - -export default class InvalidSchemaVersion extends SystemUtils.mixinClass(InvalidArgumentException) {} diff --git a/src/exception/logic-exception.js b/src/exception/logic-exception.js deleted file mode 100644 index adc72c0..0000000 --- a/src/exception/logic-exception.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import GdbotsPbjException from 'gdbots/pbj/exception/gdbots-pbj-exception'; - -export default class LogicException extends SystemUtils.mixinClass(GdbotsPbjException) {} diff --git a/src/exception/mixin-already-added.js b/src/exception/mixin-already-added.js deleted file mode 100644 index f20cca2..0000000 --- a/src/exception/mixin-already-added.js +++ /dev/null @@ -1,48 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import GdbotsPbjException from 'gdbots/pbj/exception/gdbots-pbj-exception'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class MixinAlreadyAdded extends SystemUtils.mixinClass(GdbotsPbjException) -{ - /** - * @param Schema schema - * @param Mixin originalMixin - * @param Mixin duplicateMixin - */ - constructor(schema, originalMixin, duplicateMixin) { - super('Mixin with id [' + duplicateMixin.getId().toString() + '] was already added from [' + originalMixin.getId().toString() + '] to message [' + schema.getClassName() + ']. You cannot add multiple versions of the same mixin.'); - - privateProps.set(this, { - /** @var Schema */ - schema: schema, - - /** @var Mixin */ - originalMixin: originalMixin, - - /** @var Mixin */ - duplicateMixin: duplicateMixin - }); - } - - /** - * @return Mixin - */ - getOriginalMixin() { - return privateProps.get(this).originalMixin; - } - - /** - * @return Mixin - */ - getDuplicateMixin() { - return privateProps.get(this).duplicateMixin; - } -} diff --git a/src/exception/mixin-not-defined.js b/src/exception/mixin-not-defined.js deleted file mode 100644 index ce5a21b..0000000 --- a/src/exception/mixin-not-defined.js +++ /dev/null @@ -1,37 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import GdbotsPbjException from 'gdbots/pbj/exception/gdbots-pbj-exception'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class MixinNotDefined extends SystemUtils.mixinClass(GdbotsPbjException) -{ - /** - * @param Schema schema - * @param string mixinId - */ - constructor(schema, mixinId) { - super('Mixin [' + mixinId + '] is not defined on message [' + schema.getClassName() + '].'); - - privateProps.set(this, { - /** @var Schema */ - schema: schema, - - /** @var string */ - mixinId: mixinId - }); - } - - /** - * @return string - */ - getMixinId() { - return privateProps.get(this).mixinId; - } -} diff --git a/src/exception/more-than-one-message-for-mixin.js b/src/exception/more-than-one-message-for-mixin.js deleted file mode 100644 index 2c9c44d..0000000 --- a/src/exception/more-than-one-message-for-mixin.js +++ /dev/null @@ -1,49 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import GdbotsPbjException from 'gdbots/pbj/exception/gdbots-pbj-exception'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class MoreThanOneMessageForMixin extends SystemUtils.mixinClass(GdbotsPbjException) -{ - /** - * @param Mixin mixin - * @param Message[] messages - */ - constructor(mixin, messages) { - let ids = messages.map(function(message) { - let schema = message.schema(); - return schema.getId().toString() + ' => ' + schema.getClassName(); - }); - - super('MessageResolver returned multiple messages using [' + mixin.getId().getCurieMajor() + '] when one was expected. Messages found: ' + "\n" + ids.join("\n")); - - privateProps.set(this, { - /** @var Mixin */ - mixin: mixin, - - /** @var Message[] */ - messages: messages - }); - } - - /** - * @return Mixin - */ - getMixin() { - return privateProps.get(this).mixin; - } - - /** - * @return Message[] - */ - getMessage() { - return privateProps.get(this).messages; - } -} diff --git a/src/exception/no-message-for-curie.js b/src/exception/no-message-for-curie.js deleted file mode 100644 index 771d8ab..0000000 --- a/src/exception/no-message-for-curie.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import GdbotsPbjException from 'gdbots/pbj/exception/gdbots-pbj-exception'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class NoMessageForCurie extends SystemUtils.mixinClass(GdbotsPbjException) -{ - /** - * @param SchemaCurie curie - */ - constructor(curie) { - super('MessageResolver is unable to resolve [' + curie.toString() + '] to a message.'); - - privateProps.set(this, { - /** @var SchemaCurie */ - curie: curie - }); - } - - /** - * @return SchemaCurie - */ - getCurie() { - return privateProps.get(this).curie; - } -} diff --git a/src/exception/no-message-for-mixin.js b/src/exception/no-message-for-mixin.js deleted file mode 100644 index 3527d1f..0000000 --- a/src/exception/no-message-for-mixin.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import GdbotsPbjException from 'gdbots/pbj/exception/gdbots-pbj-exception'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class NoMessageForMixin extends SystemUtils.mixinClass(GdbotsPbjException) -{ - /** - * @param Mixin mixin - */ - constructor(mixin) { - super('MessageResolver is unable to find any messages using [' + mixin.getId().getCurieMajor() + '].'); - - privateProps.set(this, { - /** @var Mixin */ - mixin: mixin - }); - } - - /** - * @return Mixin - */ - getMixin() { - return privateProps.get(this).mixin; - } -} diff --git a/src/exception/no-message-for-schema-id.js b/src/exception/no-message-for-schema-id.js deleted file mode 100644 index 623a339..0000000 --- a/src/exception/no-message-for-schema-id.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import GdbotsPbjException from 'gdbots/pbj/exception/gdbots-pbj-exception'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class NoMessageForSchemaId extends SystemUtils.mixinClass(GdbotsPbjException) -{ - /** - * @param SchemaId schemaId - */ - constructor(schemaId) { - super('MessageResolver is unable to resolve schema id [' + schemaId.toString() + '] using curie [' + schemaId.getCurieMajor() + '] to a message.'); - - privateProps.set(this, { - /** @var SchemaId */ - schemaId: schemaId - }); - } - - /** - * @return SchemaId - */ - getSchemaId() { - return privateProps.get(this).schemaId; - } -} diff --git a/src/exception/required-field-not-set.js b/src/exception/required-field-not-set.js deleted file mode 100644 index 6aa0ee2..0000000 --- a/src/exception/required-field-not-set.js +++ /dev/null @@ -1,54 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import GdbotsPbjException from 'gdbots/pbj/exception/gdbots-pbj-exception'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class RequiredFieldNotSet extends SystemUtils.mixinClass(GdbotsPbjException) -{ - /** - * @param Message type - * @param Field field - */ - constructor(type, field) { - super('Required field [' + field.getName() + '] must be set on message [' + type.constructor.schema().getClassName() + '].'); - - privateProps.set(this, { - /** @var Message */ - type: type, - - /** @var Schema */ - schema: type.constructor.schema(), - - /** @var Field */ - field: field - }); - } - - /** - * @return Message - */ - getType() { - return privateProps.get(this).type; - } - - /** - * @return Field - */ - getField() { - return privateProps.get(this).field; - } - - /** - * @return string - */ - getFieldName() { - return privateProps.get(this).field.getName(); - } -} diff --git a/src/exception/schema-not-defined.js b/src/exception/schema-not-defined.js deleted file mode 100644 index f8e2f9a..0000000 --- a/src/exception/schema-not-defined.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import GdbotsPbjException from 'gdbots/pbj/exception/gdbots-pbj-exception'; - -export default class SchemaNotDefined extends SystemUtils.mixinClass(GdbotsPbjException) {} diff --git a/src/exceptions/AssertionFailed.js b/src/exceptions/AssertionFailed.js new file mode 100644 index 0000000..3c91c14 --- /dev/null +++ b/src/exceptions/AssertionFailed.js @@ -0,0 +1,4 @@ +import InvalidArgumentException from './InvalidArgumentException'; + +export default class AssertionFailed extends InvalidArgumentException { +} diff --git a/src/exceptions/DecodeValueFailed.js b/src/exceptions/DecodeValueFailed.js new file mode 100644 index 0000000..8e689d3 --- /dev/null +++ b/src/exceptions/DecodeValueFailed.js @@ -0,0 +1,36 @@ +import truncate from 'lodash/truncate'; +import InvalidArgumentException from './InvalidArgumentException'; + +export default class DecodeValueFailed extends InvalidArgumentException { + /** + * @param {*} value + * @param {Field} field + * @param {string} message + */ + constructor(value, field, message) { + super(`Failed to decode [${truncate(value)}] for field [${field.getName()}] to a [${field.getType().getTypeName()}]. ${message}`); + this.value = value; + this.field = field; + } + + /** + * @returns {*} + */ + getValue() { + return this.value; + } + + /** + * @returns {Field} + */ + getField() { + return this.field; + } + + /** + * @returns {string} + */ + getFieldName() { + return this.field.getName(); + } +} diff --git a/src/exceptions/FieldAlreadyDefined.js b/src/exceptions/FieldAlreadyDefined.js new file mode 100644 index 0000000..5b48f4e --- /dev/null +++ b/src/exceptions/FieldAlreadyDefined.js @@ -0,0 +1,27 @@ +import SchemaException from './SchemaException'; + +export default class FieldAlreadyDefined extends SchemaException { + /** + * @param {Schema} schema + * @param {string} fieldName + */ + constructor(schema, fieldName) { + super(`Field [${fieldName}] is already defined on message [${schema.getId()}] and is not overridable.`); + this.schema = schema; + this.field = this.schema.getField(fieldName); + } + + /** + * @returns {Field} + */ + getField() { + return this.field; + } + + /** + * @returns {string} + */ + getFieldName() { + return this.field.getName(); + } +} diff --git a/src/exceptions/FieldNotDefined.js b/src/exceptions/FieldNotDefined.js new file mode 100644 index 0000000..b77cf27 --- /dev/null +++ b/src/exceptions/FieldNotDefined.js @@ -0,0 +1,20 @@ +import SchemaException from './SchemaException'; + +export default class FieldNotDefined extends SchemaException { + /** + * @param {Schema} schema + * @param {string} fieldName + */ + constructor(schema, fieldName) { + super(`Field [${fieldName}] is not defined on message [${schema.getId()}].`); + this.schema = schema; + this.fieldName = fieldName; + } + + /** + * @returns {string} + */ + getFieldName() { + return this.fieldName; + } +} diff --git a/src/exceptions/FieldOverrideNotCompatible.js b/src/exceptions/FieldOverrideNotCompatible.js new file mode 100644 index 0000000..61bce8e --- /dev/null +++ b/src/exceptions/FieldOverrideNotCompatible.js @@ -0,0 +1,36 @@ +import SchemaException from './SchemaException'; + +export default class FieldOverrideNotCompatible extends SchemaException { + /** + * @param {Schema} schema + * @param {string} fieldName + * @param {Field} overrideField + */ + constructor(schema, fieldName, overrideField) { + super(`Field [${fieldName}] override for [${schema.getId()}] is not compatible. Name, Type, Rule and Required must match.`); + this.schema = schema; + this.existingField = this.schema.getField(fieldName); + this.overrideField = overrideField; + } + + /** + * @returns {Field} + */ + getExistingField() { + return this.existingField; + } + + /** + * @returns {string} + */ + getFieldName() { + return this.existingField.getName(); + } + + /** + * @returns {Field} + */ + getOverrideField() { + return this.overrideField; + } +} diff --git a/src/exceptions/FrozenMessageIsImmutable.js b/src/exceptions/FrozenMessageIsImmutable.js new file mode 100644 index 0000000..9ec9f02 --- /dev/null +++ b/src/exceptions/FrozenMessageIsImmutable.js @@ -0,0 +1,18 @@ +import LogicException from './LogicException'; + +export default class FrozenMessageIsImmutable extends LogicException { + /** + * @param {Message} pbj + */ + constructor(pbj) { + super('Message is frozen and cannot be modified.'); + this.pbj = pbj; + } + + /** + * @returns {Message} + */ + getPbj() { + return this.pbj; + } +} diff --git a/src/exceptions/GdbotsPbjException.js b/src/exceptions/GdbotsPbjException.js new file mode 100644 index 0000000..f9aced0 --- /dev/null +++ b/src/exceptions/GdbotsPbjException.js @@ -0,0 +1,4 @@ +import Exception from '@gdbots/common/Exception'; + +export default class GdbotsPbjException extends Exception { +} diff --git a/src/exceptions/InvalidArgumentException.js b/src/exceptions/InvalidArgumentException.js new file mode 100644 index 0000000..a09b71e --- /dev/null +++ b/src/exceptions/InvalidArgumentException.js @@ -0,0 +1,12 @@ +import GdbotsPbjException from './GdbotsPbjException'; + +export default class InvalidArgumentException extends GdbotsPbjException { + /** + * @param {string} message + */ + constructor(message) { + // 3 = INVALID_ARGUMENT + // @link https://github.com/gdbots/schemas/blob/master/schemas/gdbots/pbjx/enums.xml#L12 + super(message, 3); + } +} diff --git a/src/exceptions/InvalidResolvedSchema.js b/src/exceptions/InvalidResolvedSchema.js new file mode 100644 index 0000000..42f00fb --- /dev/null +++ b/src/exceptions/InvalidResolvedSchema.js @@ -0,0 +1,20 @@ +import SchemaException from './SchemaException'; + +export default class InvalidResolvedSchema extends SchemaException { + /** + * @param {Schema} schema + * @param {SchemaId} resolvedSchemaId + */ + constructor(schema, resolvedSchemaId) { + super(`Schema id [${resolvedSchemaId}] was resolved to [${schema.getCurieMajor()}]. Curie majors must match.`); + this.schema = schema; + this.resolvedSchemaId = resolvedSchemaId; + } + + /** + * @returns {SchemaId} + */ + getResolvedSchemaId() { + return this.resolvedSchemaId; + } +} diff --git a/src/exceptions/InvalidSchemaCurie.js b/src/exceptions/InvalidSchemaCurie.js new file mode 100644 index 0000000..da8b5ff --- /dev/null +++ b/src/exceptions/InvalidSchemaCurie.js @@ -0,0 +1,4 @@ +import InvalidArgumentException from './InvalidArgumentException'; + +export default class InvalidSchemaCurie extends InvalidArgumentException { +} diff --git a/src/exceptions/InvalidSchemaId.js b/src/exceptions/InvalidSchemaId.js new file mode 100644 index 0000000..9cdaf42 --- /dev/null +++ b/src/exceptions/InvalidSchemaId.js @@ -0,0 +1,4 @@ +import InvalidArgumentException from './InvalidArgumentException'; + +export default class InvalidSchemaId extends InvalidArgumentException { +} diff --git a/src/exceptions/InvalidSchemaQName.js b/src/exceptions/InvalidSchemaQName.js new file mode 100644 index 0000000..cb1a2cb --- /dev/null +++ b/src/exceptions/InvalidSchemaQName.js @@ -0,0 +1,4 @@ +import InvalidArgumentException from './InvalidArgumentException'; + +export default class InvalidSchemaQName extends InvalidArgumentException { +} diff --git a/src/exceptions/InvalidSchemaVersion.js b/src/exceptions/InvalidSchemaVersion.js new file mode 100644 index 0000000..a4bab55 --- /dev/null +++ b/src/exceptions/InvalidSchemaVersion.js @@ -0,0 +1,4 @@ +import InvalidArgumentException from './InvalidArgumentException'; + +export default class InvalidSchemaVersion extends InvalidArgumentException { +} diff --git a/src/exceptions/LogicException.js b/src/exceptions/LogicException.js new file mode 100644 index 0000000..111d425 --- /dev/null +++ b/src/exceptions/LogicException.js @@ -0,0 +1,12 @@ +import GdbotsPbjException from './GdbotsPbjException'; + +export default class LogicException extends GdbotsPbjException { + /** + * @param {string} message + */ + constructor(message) { + // 13 = INTERNAL + // @link https://github.com/gdbots/schemas/blob/master/schemas/gdbots/pbjx/enums.xml#L23 + super(message, 13); + } +} diff --git a/src/exceptions/MixinAlreadyAdded.js b/src/exceptions/MixinAlreadyAdded.js new file mode 100644 index 0000000..9ff5b6f --- /dev/null +++ b/src/exceptions/MixinAlreadyAdded.js @@ -0,0 +1,29 @@ +import SchemaException from './SchemaException'; + +export default class MixinAlreadyAdded extends SchemaException { + /** + * @param {Schema} schema + * @param {Mixin} originalMixin + * @param {Mixin} duplicateMixin + */ + constructor(schema, originalMixin, duplicateMixin) { + super(`Mixin with id [${duplicateMixin.getId()}] was already added from [${originalMixin.getId()}] to message [${schema.getId()}]. You cannot add multiple versions of the same mixin.`); + this.schema = schema; + this.originalMixin = originalMixin; + this.duplicateMixin = duplicateMixin; + } + + /** + * @returns {Mixin} + */ + getOriginalMixin() { + return this.originalMixin; + } + + /** + * @returns {Mixin} + */ + getDuplicateMixin() { + return this.duplicateMixin; + } +} diff --git a/src/exceptions/MixinNotDefined.js b/src/exceptions/MixinNotDefined.js new file mode 100644 index 0000000..bc3a42f --- /dev/null +++ b/src/exceptions/MixinNotDefined.js @@ -0,0 +1,20 @@ +import SchemaException from './SchemaException'; + +export default class MixinNotDefined extends SchemaException { + /** + * @param {Schema} schema + * @param {string} mixinId + */ + constructor(schema, mixinId) { + super(`Mixin [${mixinId}] is not defined on message [${schema.getId()}].`); + this.schema = schema; + this.mixinId = mixinId; + } + + /** + * @returns {string} + */ + getMixinId() { + return this.mixinId; + } +} diff --git a/src/exceptions/MoreThanOneMessageForMixin.js b/src/exceptions/MoreThanOneMessageForMixin.js new file mode 100644 index 0000000..f9ec419 --- /dev/null +++ b/src/exceptions/MoreThanOneMessageForMixin.js @@ -0,0 +1,28 @@ +import LogicException from './LogicException'; + +export default class MoreThanOneMessageForMixin extends LogicException { + /** + * @param {Mixin} mixin + * @param {Schema[]} schemas + */ + constructor(mixin, schemas) { + const ids = schemas.map(s => s.getId().toString()).join('\n - '); + super(`MessageResolver returned multiple schemas using [${mixin.getId().getCurieMajor()}] when one was expected. Schemas found:\n - ${ids}`); + this.mixin = mixin; + this.schemas = schemas; + } + + /** + * @returns {Mixin} + */ + getMixin() { + return this.mixin; + } + + /** + * @returns {Schema[]} + */ + getSchemas() { + return this.schemas; + } +} diff --git a/src/exceptions/NoMessageForCurie.js b/src/exceptions/NoMessageForCurie.js new file mode 100644 index 0000000..9820763 --- /dev/null +++ b/src/exceptions/NoMessageForCurie.js @@ -0,0 +1,18 @@ +import LogicException from './LogicException'; + +export default class NoMessageForCurie extends LogicException { + /** + * @param {SchemaCurie} curie + */ + constructor(curie) { + super(`MessageResolver is unable to resolve schema curie [${curie}] to a class.`); + this.curie = curie; + } + + /** + * @returns {SchemaCurie} + */ + getCurie() { + return this.curie; + } +} diff --git a/src/exceptions/NoMessageForMixin.js b/src/exceptions/NoMessageForMixin.js new file mode 100644 index 0000000..dca144f --- /dev/null +++ b/src/exceptions/NoMessageForMixin.js @@ -0,0 +1,18 @@ +import LogicException from './LogicException'; + +export default class NoMessageForMixin extends LogicException { + /** + * @param {Mixin} mixin + */ + constructor(mixin) { + super(`MessageResolver is unable to find any messages using [${mixin.getId().getCurieMajor()}].`); + this.mixin = mixin; + } + + /** + * @returns {Mixin} + */ + getMixin() { + return this.mixin; + } +} diff --git a/src/exceptions/NoMessageForQName.js b/src/exceptions/NoMessageForQName.js new file mode 100644 index 0000000..46e50c6 --- /dev/null +++ b/src/exceptions/NoMessageForQName.js @@ -0,0 +1,18 @@ +import LogicException from './LogicException'; + +export default class NoMessageForQName extends LogicException { + /** + * @param {SchemaQName} qname + */ + constructor(qname) { + super(`MessageResolver is unable to resolve [${qname}] to a SchemaCurie.`); + this.qname = qname; + } + + /** + * @returns {SchemaQName} + */ + getQName() { + return this.qname; + } +} diff --git a/src/exceptions/NoMessageForSchemaId.js b/src/exceptions/NoMessageForSchemaId.js new file mode 100644 index 0000000..9cbdaba --- /dev/null +++ b/src/exceptions/NoMessageForSchemaId.js @@ -0,0 +1,18 @@ +import LogicException from './LogicException'; + +export default class NoMessageForSchemaId extends LogicException { + /** + * @param {SchemaId} schemaId + */ + constructor(schemaId) { + super(`MessageResolver is unable to resolve schema id [${schemaId}] to a class.`); + this.schemaId = schemaId; + } + + /** + * @returns {SchemaId} + */ + getSchemaId() { + return this.schemaId; + } +} diff --git a/src/exceptions/RequiredFieldNotSet.js b/src/exceptions/RequiredFieldNotSet.js new file mode 100644 index 0000000..71ae065 --- /dev/null +++ b/src/exceptions/RequiredFieldNotSet.js @@ -0,0 +1,35 @@ +import SchemaException from './SchemaException'; + +export default class RequiredFieldNotSet extends SchemaException { + /** + * @param {Message} pbj + * @param {Field} field + */ + constructor(pbj, field) { + super(`Required field [${field.getName()}] must be set on [${pbj.schema().getCurieMajor()}].`); + this.schema = pbj.schema(); + this.pbj = pbj; + this.field = field; + } + + /** + * @returns {Message} + */ + getPbj() { + return this.pbj; + } + + /** + * @returns {Field} + */ + getField() { + return this.field; + } + + /** + * @returns {string} + */ + getFieldName() { + return this.field.getName(); + } +} diff --git a/src/exceptions/SchemaException.js b/src/exceptions/SchemaException.js new file mode 100644 index 0000000..1ca9397 --- /dev/null +++ b/src/exceptions/SchemaException.js @@ -0,0 +1,10 @@ +import LogicException from './LogicException'; + +export default class SchemaException extends LogicException { + /** + * @returns {Schema} + */ + getSchema() { + return this.schema; + } +} diff --git a/src/exceptions/SchemaNotDefined.js b/src/exceptions/SchemaNotDefined.js new file mode 100644 index 0000000..4bc2923 --- /dev/null +++ b/src/exceptions/SchemaNotDefined.js @@ -0,0 +1,4 @@ +import LogicException from './LogicException'; + +export default class SchemaNotDefined extends LogicException { +} diff --git a/src/field-builder.js b/src/field-builder.js deleted file mode 100644 index 41cd26b..0000000 --- a/src/field-builder.js +++ /dev/null @@ -1,306 +0,0 @@ -'use strict'; - -import FieldRule from 'gdbots/pbj/enum/field-rule'; -import Field from 'gdbots/pbj/field'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class FieldBuilder -{ - /** - * @param string name - * @param Type type - */ - constructor(name, type) { - privateProps.set(this, { - /** @var string */ - name: name, - - /** @var Type */ - type: type, - - /** @var FieldRule */ - rule: null, - - /** @var bool */ - required: false, - - /** @var int */ - minLength: null, - - /** @var int */ - maxLength: null, - - /** @var string */ - pattern: null, - - /** @var string */ - format: null, - - /** @var int */ - min: null, - - /** @var int */ - max: null, - - /** @var int */ - precision: 10, - - /** @var int */ - scale: 2, - - /** @var mixed */ - defaultValue: null, - - /** @var bool */ - useTypeDefault: true, - - /** @var string */ - instance: null, - - /** @var array */ - anyOfInstances: null, - - /** @var \Closure */ - assertion: null, - - /** @var bool */ - overridable: false - }); - } - - /** - * @param string name - * @param Type type - * - * @return self - */ - static create(name, type) { - return new this(name, type); - } - - /** - * @return self - */ - required() { - privateProps.get(this).required = true; - return this; - } - - /** - * @return self - */ - optional() { - privateProps.get(this).required = false; - return this; - } - - /** - * @return self - */ - asASingleValue() { - privateProps.get(this).rule = FieldRule.A_SINGLE_VALUE; - return this; - } - - /** - * @return self - */ - asASet() { - privateProps.get(this).rule = FieldRule.A_SET; - return this; - } - - /** - * @return self - */ - asAList() { - privateProps.get(this).rule = FieldRule.A_LIST; - return this; - } - - /** - * @return self - */ - asAMap() { - privateProps.get(this).rule = FieldRule.A_MAP; - return this; - } - - /** - * @param int minLength - * - * @return self - */ - minLength(minLength) { - privateProps.get(this).minLength = parseInt(minLength); - return this; - } - - /** - * @param int maxLength - * - * @return self - */ - maxLength(maxLength) { - privateProps.get(this).maxLength = parseInt(maxLength); - return this; - } - - /** - * @param string pattern - * - * @return self - */ - pattern(pattern) { - privateProps.get(this).pattern = pattern; - return this; - } - - /** - * @param string format - * - * @return self - */ - format(format) { - privateProps.get(this).format = format; - return this; - } - - /** - * @param int min - * - * @return self - */ - min(min) { - privateProps.get(this).min = parseInt(min); - return this; - } - - /** - * @param int max - * - * @return self - */ - max(max) { - privateProps.get(this).max = parseInt(max); - return this; - } - - /** - * @param int precision - * - * @return self - */ - precision(precision) { - privateProps.get(this).precision = parseInt(precision); - return this; - } - - /** - * @param int scale - * - * @return self - */ - scale(scale) { - privateProps.get(this).scale = parseInt(scale); - return this; - } - - /** - * @param mixed defaultValue - * - * @return self - */ - withDefault(defaultValue) { - privateProps.get(this).defaultValue = defaultValue; - return this; - } - - /** - * @param bool useTypeDefault - * - * @return self - */ - useTypeDefault(useTypeDefault) { - privateProps.get(this).useTypeDefault = Boolean(useTypeDefault); - return this; - } - - /** - * @param string instance - * - * @return self - */ - instance(instance) { - privateProps.get(this).instance = instance; - privateProps.get(this).anyOfInstances = null; - return this; - } - - /** - * @param array anyOfInstances - * - * @return self - */ - anyOfInstances(anyOfInstances) { - privateProps.get(this).anyOfInstances = anyOfInstances; - privateProps.get(this).instance = null; - return this; - } - - /** - * @param \Closure assertion - * - * @return self - */ - assertion(assertion) { - privateProps.get(this).assertion = assertion; - return this; - } - - /** - * @param bool overridable - * - * @return self - */ - overridable(overridable) { - privateProps.get(this).overridable = Boolean(overridable); - return this; - } - - /** - * @return Field - */ - build() { - if (null === privateProps.get(this).rule) { - privateProps.get(this).rule = FieldRule.A_SINGLE_VALUE; - } - - return new Field( - privateProps.get(this).name, - privateProps.get(this).type, - privateProps.get(this).rule, - privateProps.get(this).required, - privateProps.get(this).minLength, - privateProps.get(this).maxLength, - privateProps.get(this).pattern, - privateProps.get(this).format, - privateProps.get(this).min, - privateProps.get(this).max, - privateProps.get(this).precision, - privateProps.get(this).scale, - privateProps.get(this).defaultValue, - privateProps.get(this).useTypeDefault, - privateProps.get(this).instance, - privateProps.get(this).anyOfInstances, - privateProps.get(this).assertion, - privateProps.get(this).overridable - ); - } -} diff --git a/src/field.js b/src/field.js deleted file mode 100644 index 2a15eb3..0000000 --- a/src/field.js +++ /dev/null @@ -1,624 +0,0 @@ -'use strict'; - -import ToArray from 'gdbots/common/to-array'; -import ArrayUtils from 'gdbots/common/util/array-utils'; -import NumberUtils from 'gdbots/common/util/number-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import TypeName from 'gdbots/pbj/enum/type-name'; -import FieldRule from 'gdbots/pbj/enum/field-rule'; -import Format from 'gdbots/pbj/enum/format'; - -/** - * Regular expression pattern for matching a valid field name. The pattern allows - * for camelCase fields name but snake_case is recommend. - * - * @constant string - */ -export const VALID_NAME_PATTERN = /^[a-zA-Z_]{1}[a-zA-Z0-9_]*/; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class Field extends SystemUtils.mixinClass(null, ToArray) -{ - /** - * @param string name - * @param Type type - * @param FieldRule rule - * @param bool required - * @param null|int minLength - * @param null|int maxLength - * @param null|string pattern - * @param null|string format - * @param null|int min - * @param null|int max - * @param int precision - * @param int scale - * @param null|mixed defaultValue - * @param bool useTypeDefault - * @param null|string instance - * @param null|array anyOfInstances - * @param bool overridable - */ - constructor( - name, - type, - rule = null, - required = false, - minLength = null, - maxLength = null, - pattern = null, - format = null, - min = null, - max = null, - precision = 10, - scale = 2, - defaultValue = null, - useTypeDefault = true, - instance = null, - anyOfInstances = null, - assertion = null, - overridable = false - ) { - super(); // require before using `this` - - if (1 > name.length || name.length > 127) { - throw new Error('Name length must be between 1 to 127.'); - } - if (!VALID_NAME_PATTERN.test(name)) { - throw new Error('Field [' + name + '] must match pattern [' + VALID_NAME_PATTERN + '].'); - } - if (!type || !type.hasTrait('Type')) { - throw new Error('Class "' + type + '" was expected to be instanceof of "Type" but is not.'); - } - if ('boolean' !== typeof required) { - throw new Error('Required value must be boolean.'); - } - if ('boolean' !== typeof useTypeDefault) { - throw new Error('UseTypeDefault value must be boolean.'); - } - if ('boolean' !== typeof overridable) { - throw new Error('Overridable value must be boolean.'); - } - - /* - * a message type allows for interfaces to be used - * as the "instance". so long as the provided argument - * passes the instanceof check it's okay. - */ - if (type.getTypeName() !== TypeName.MESSAGE) { - - // anyOf is only supported on nested messages - anyOfInstances = null; - } - - privateProps.set(this, { - /** @var string */ - name: name, - - /** @var Type */ - type: type, - - /** @var FieldRule */ - rule: null, - - /** @var bool */ - required: required || false, - - /** @var int */ - minLength: null, - - /** @var int */ - maxLength: null, - - /** - * A regular expression to match against for string types. - * @link http://spacetelescope.github.io/understanding-json-schema/reference/string.html#pattern - * - * @var string - */ - pattern: null, - - /** - * @link http://spacetelescope.github.io/understanding-json-schema/reference/string.html#format - * - * @var Format - */ - format: null, - - /** @var int */ - min: null, - - /** @var int */ - max: null, - - /** @var int */ - precision: 10, - - /** @var int */ - scale: 2, - - /** @var mixed */ - defaultValue: null, - - /** @var bool */ - useTypeDefault: useTypeDefault, - - /** @var string */ - instance: instance, - - /** @var array */ - anyOfInstances: anyOfInstances, - - /** @var \Closure */ - assertion: assertion, - - /** @var bool */ - overridable: overridable || false - }); - - applyFieldRule.bind(this)(rule); - applyStringOptions.bind(this)(minLength, maxLength, pattern, format); - applyNumericOptions.bind(this)(min, max, precision, scale); - applyDefault.bind(this)(defaultValue); - } - - /** - * @return string - */ - getName() { - return privateProps.get(this).name; - } - - /** - * @return Type - */ - getType() { - return privateProps.get(this).type; - } - - /** - * @return FieldRule - */ - getRule() { - return privateProps.get(this).rule; - } - - /** - * @return bool - */ - isASingleValue() { - return FieldRule.A_SINGLE_VALUE === privateProps.get(this).rule; - } - - /** - * @return bool - */ - isASet() { - return FieldRule.A_SET === privateProps.get(this).rule; - } - - /** - * @return bool - */ - isAList() { - return FieldRule.A_LIST === privateProps.get(this).rule; - } - - /** - * @return bool - */ - isAMap() { - return FieldRule.A_MAP === privateProps.get(this).rule; - } - - /** - * @return bool - */ - isRequired() { - return privateProps.get(this).required; - } - - /** - * @return int - */ - getMinLength() { - return privateProps.get(this).minLength || 0; - } - - /** - * @return int - */ - getMaxLength() { - if (!privateProps.get(this).maxLength) { - return privateProps.get(this).type.getMaxBytes(); - } - - return privateProps.get(this).maxLength; - } - - /** - * @return string - */ - getPattern() { - return privateProps.get(this).pattern; - } - - /** - * @return Format - */ - getFormat() { - return privateProps.get(this).format; - } - - /** - * @return int - */ - getMin() { - if (!privateProps.get(this).min) { - return privateProps.get(this).type.getMin(); - } - - return privateProps.get(this).min; - } - - /** - * @return int - */ - getMax() { - if (!privateProps.get(this).max) { - return privateProps.get(this).type.getMax(); - } - - return privateProps.get(this).max; - } - - /** - * @return int - */ - getPrecision() { - return privateProps.get(this).precision; - } - - /** - * @return int - */ - getScale() { - return privateProps.get(this).scale; - } - - /** - * @param Message message - * - * @return mixed - */ - getDefault(message = null) { - if (null === privateProps.get(this).defaultValue) { - if (privateProps.get(this).useTypeDefault) { - return this.isASingleValue() ? privateProps.get(this).type.getDefault() : []; - } - - return this.isASingleValue() ? null : []; - } - - if ('function' === typeof privateProps.get(this).defaultValue) { - let defaultValue = privateProps.get(this).defaultValue(message, this); - - guardDefault.bind(this)(defaultValue); - - if (null === defaultValue) { - if (privateProps.get(this).useTypeDefault) { - return this.isASingleValue() ? privateProps.get(this).type.getDefault() : []; - } - - return this.isASingleValue() ? null : []; - } - - return defaultValue; - } - - - return privateProps.get(this).defaultValue; - } - - /** - * @return bool - */ - hasInstance() { - return null !== privateProps.get(this).instance; - } - - /** - * @return string - */ - getInstance() { - return privateProps.get(this).instance; - } - - /** - * @return bool - */ - hasAnyOfInstances() { - return null !== privateProps.get(this).anyOfInstances; - } - - /** - * @return array - */ - getAnyOfInstances() { - return privateProps.get(this).anyOfInstances; - } - - /** - * @return bool - */ - isOverridable() { - return privateProps.get(this).overridable; - } - - /** - * @param mixed value - * - * @throws AssertionFailed - * @throws \Exception - */ - guardValue(value) { - if (privateProps.get(this).required && null === value) { - throw new Error('Field [' + privateProps.get(this).name + '] is required and cannot be null.'); - } - - if (null !== value) { - privateProps.get(this).type.guard(value, this); - } - - if (null !== privateProps.get(this).assertion) { - privateProps.get(this).assertion(value, this); - } - } - - /** - * @return array - */ - toArray() { - return { - 'name': privateProps.get(this).name, - 'type': privateProps.get(this).type.getTypeValue(), - 'rule': privateProps.get(this).rule.getName(), - 'required': privateProps.get(this).required, - 'min_length': privateProps.get(this).minLength, - 'max_length': privateProps.get(this).maxLength, - 'pattern': privateProps.get(this).pattern, - 'format': privateProps.get(this).format.getValue(), - 'min': privateProps.get(this).min, - 'max': privateProps.get(this).max, - 'precision': privateProps.get(this).precision, - 'scale': privateProps.get(this).scale, - 'default': this.getDefault(), - 'use_type_default': privateProps.get(this).useTypeDefault, - 'instance': privateProps.get(this).instance, - 'any_of_instances': privateProps.get(this).anyOfInstances, - 'has_assertion': null !== privateProps.get(this).assertion, - 'overridable': privateProps.get(this).overridable, - }; - } - - /** - * Returns true if this field is likely compatible with the - * provided field during a mergeFrom operation. - * - * @param Field other - * - * @return bool - */ - isCompatibleForMerge(other) { - if (privateProps.get(this).name !== other.name) { - return false; - } - - if (privateProps.get(this).type !== other.type) { - return false; - } - - if (privateProps.get(this).rule !== other.rule) { - return false; - } - - if (privateProps.get(this).instance !== other.instance) { - return false; - } - - if (privateProps.get(this).anyOfInstances.filter(function(k) { - return other.anyOfInstances.indexOf(k) != -1; - }).length === 0) { - return false; - } - - return true; - } - - /** - * Returns true if the provided field can be used as an - * override to this field. - * - * @param Field other - * - * @return bool - */ - isCompatibleForOverride(other) { - if (!privateProps.get(this).overridable) { - return false; - } - - if (privateProps.get(this).name !== other.name) { - return false; - } - - if (privateProps.get(this).type !== other.type) { - return false; - } - - if (privateProps.get(this).rule !== other.rule) { - return false; - } - - if (privateProps.get(this).required !== other.required) { - return false; - } - - return true; - } -} - -/** - * @param FieldRule rule - * - * @throws AssertionFailed - */ -function applyFieldRule(rule = null) { - privateProps.get(this).rule = rule || FieldRule.A_SINGLE_VALUE; - - if (this.isASet() && !privateProps.get(this).type.allowedInSet()) { - throw new Error('Field [' + privateProps.get(this).name + '] with type [' + privateProps.get(this).type.getTypeValue() + '] cannot be used in a set.'); - } -} - -/** - * @param null|int minLength - * @param null|int maxLength - * @param null|string pattern - * @param null|string format - */ -function applyStringOptions(minLength = null, maxLength = null, pattern = null, format = null) { - privateProps.get(this).minLength = parseInt(minLength); - privateProps.get(this).maxLength = parseInt(maxLength); - - if (maxLength > 0) { - privateProps.get(this).maxLength = maxLength; - privateProps.get(this).minLength = NumberUtils.bound(minLength, 0, privateProps.get(this).maxLength); - } else { - // arbitrary string minimum range - privateProps.get(this).minLength = NumberUtils.bound(minLength, 0, privateProps.get(this).type.getMaxBytes()); - } - - if (null !== pattern) { - privateProps.get(this).pattern = pattern.trim().replace('/', ''); - } - - if (null !== format && Format.enumValueOf(format)) { - privateProps.get(this).format = Format.enumValueOf(format); - } else { - privateProps.get(this).format = Format.UNKNOWN; - } -} - -/** - * @param null|int min - * @param null|int max - * @param int precision - * @param int scale - */ -function applyNumericOptions(min = null, max = null, precision = 10, scale = 2) { - if (null !== max) { - privateProps.get(this).max = parseInt(max); - } - - if (null !== min) { - privateProps.get(this).min = parseInt(min); - if (null !== privateProps.get(this).max) { - if (privateProps.get(this).min > privateProps.get(this).max) { - privateProps.get(this).min = privateProps.get(this).max; - } - } - } - - privateProps.get(this).precision = NumberUtils.bound(parseInt(precision), 1, 65); - privateProps.get(this).scale = NumberUtils.bound(parseInt(scale), 0, privateProps.get(this).precision) -} - -/** - * @param mixed defaultValue - * - * @throws AssertionFailed - * @throws \Exception - */ -function applyDefault(defaultValue = null) { - privateProps.get(this).defaultValue = defaultValue; - - if (privateProps.get(this).type.isScalar()) { - if (privateProps.get(this).type.getTypeName() !== TypeName.TIMESTAMP) { - privateProps.get(this).useTypeDefault = true; - } - } else { - let decodeDefault = null !== privateProps.get(this).defaultValue && 'function' !== typeof privateProps.get(this).defaultValue; - - switch (privateProps.get(this).type.getTypeName()) { - case TypeName.IDENTIFIER: - if (null === privateProps.get(this).instance) { - throw new Error('Field [' + privateProps.get(this).name + '] requires an instance.'); - } - - if (decodeDefault && !privateProps.get(this).defaultValue.hasTrait('Identifier')) { - privateProps.get(this).defaultValue = privateProps.get(this).type.decode(privateProps.get(this).defaultValue, this); - } - break; - - case TypeName.INT_ENUM: - case TypeName.STRING_ENUM: - if (null === privateProps.get(this).instance) { - throw new Error('Field [' + privateProps.get(this).name + '] requires an instance.'); - } - - if (decodeDefault && !privateProps.get(this).defaultValue.hasTrait('Enum')) { - privateProps.get(this).defaultValue = privateProps.get(this).type.decode(privateProps.get(this).defaultValue, this); - } - break; - - default: - break; - } - } - - if (null !== privateProps.get(this).defaultValue && 'function' !== typeof privateProps.get(this).defaultValue) { - guardDefault.bind(this)(privateProps.get(this).defaultValue); - } -} - -/** - * @param mixed defaultValue - * - * @throws AssertionFailed - * @throws \Exception - */ -function guardDefault(defaultValue) { - if (this.isASingleValue()) { - this.guardValue(defaultValue); - - return; - } - - if (null !== defaultValue || !Array.isArray(defaultValue)) { - throw new Error('Field [' + privateProps.get(this).name + '] default must be an array.'); - } - - if (null === defaultValue) { - return; - } - - if (this.isAMap()) { - if (!ArrayUtils.isAssoc(defaultValue)) { - throw new Error('Field [' + privateProps.get(this).name + '] default must be an associative array.'); - } - } - - ArrayUtils.each(defaultValue, function(value, key) { - if (null === value) { - throw new Error('Field [' + privateProps.get(this).name + '] default for key [' + value + '] cannot be null.'); - } - - this.guardValue(value); - }.bind(this)); -} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..422a0df --- /dev/null +++ b/src/index.js @@ -0,0 +1,27 @@ +import Field from './Field'; +import FieldBuilder from './FieldBuilder'; +import Message from './Message'; +import MessageRef from './MessageRef'; +import Mixin from './Mixin'; +import Schema from './Schema'; +import SchemaCurie from './SchemaCurie'; +import SchemaId from './SchemaId'; +import SchemaQName from './SchemaQName'; +import SchemaVersion from './SchemaVersion'; +import Types from './types'; +import WellKnown from './well-known'; + +export default { + Field, + FieldBuilder, + Message, + MessageRef, + Mixin, + Schema, + SchemaCurie, + SchemaId, + SchemaQName, + SchemaVersion, + Types, + WellKnown, +}; diff --git a/src/message-ref.js b/src/message-ref.js deleted file mode 100644 index a9faa9c..0000000 --- a/src/message-ref.js +++ /dev/null @@ -1,170 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import FromArray from 'gdbots/common/from-array'; -import ToArray from 'gdbots/common/to-array'; -import InvalidArgumentException from 'gdbots/pbj/exception/invalid-argument-exception'; -import LogicException from 'gdbots/pbj/exception/logic-exception'; -import SchemaCurie from 'gdbots/pbj/schema-curie'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -/** - * Represents a reference to a message. Typically used to link messages - * together via a correlator or "links". Format for a reference: - * vendor:package:category:message:id#tag (tag is optional) - */ -export default class MessageRef extends SystemUtils.mixinClass(null, FromArray, ToArray) -{ - /** - * @param SchemaCurie curie - * @param string id - * @param string tag The tag will be automatically fixed to a slug-formatted-string. - * - * @throws \Exception - */ - constructor(curie, id, tag = null) { - super(); // require before using `this` - - privateProps.set(this, { - /** @var SchemaCurie */ - curie: curie, - - /** - * Any string matching pattern /^[\w\/\.:-]+/ - * - * @var string - */ - id: id || 'null', - - /** @var string */ - tag: null - }); - - if (false === /^[\w\/\.:-]+/.test(privateProps.get(this).id)) { - throw new Error('MessageRef.id'); - } - - if (null !== tag) { - privateProps.get(this).tag = tag.toString().toLowerCase() - .replace(/\s+/g, '-') // Replace spaces with - - .replace(/[^\w\-]+/g, '') // Remove all non-word chars - .replace(/\-\-+/g, '-') // Replace multiple - with single - - .replace(/^-+/, '') // Trim - from start of text - .replace(/-+$/, ''); // Trim - from end of text - } - - if (privateProps.get(this).curie.isMixin()) { - throw new LogicException('Mixins cannot be used in a MessageRef.'); - } - } - - /** - * {@inheritdoc} - */ - static fromArray(data = {}) { - if (data.curie || false) { - let id = data.id || 'null'; - let tag = data.tag || null; - - return new this(SchemaCurie.fromString(data.curie), id, tag); - } - - throw new InvalidArgumentException('Payload must be a MessageRef type.'); - } - - /** - * {@inheritdoc} - */ - toArray() { - if (null !== privateProps.get(this).tag) { - return { - 'curie': privateProps.get(this).curie.toString(), - 'id': privateProps.get(this).id, - 'tag': privateProps.get(this).tag - }; - } - - return { - 'curie': privateProps.get(this).curie.toString(), - 'id': privateProps.get(this).id - }; - } - - /** - * @param string string A string with format curie:id#tag - * - * @return self - */ - static fromString(string) { - let parts = string.split('#', 2); - let ref = parts[0]; - let tag = parts[1] || null; - - parts = ref.split(':', 5); - let id = parts.pop(); - let curie = SchemaCurie.fromString(parts.join(':')); - - return new this(curie, id, tag); - } - - /** - * @return string - */ - toString() { - if (null !== privateProps.get(this).tag) { - return privateProps.get(this).curie.toString() + ':' + privateProps.get(this).id + '#' + privateProps.get(this).tag; - } - - return privateProps.get(this).curie.toString() + ':' + privateProps.get(this).id; - } - - /** - * @return SchemaCurie - */ - getCurie() { - return privateProps.get(this).curie; - } - - /** - * @return bool - */ - hasId() { - return 'null' != privateProps.get(this).id; - } - - /** - * @return string - */ - getId() { - return privateProps.get(this).id; - } - - /** - * @return bool - */ - hasTag() { - return null !== privateProps.get(this).tag; - } - - /** - * @return string - */ - getTag() { - return privateProps.get(this).tag; - } - - /** - * @param MessageRef other - * - * @return bool - */ - equals(other) { - return this.toString() === other.toString(); - } -} diff --git a/src/message-resolver.js b/src/message-resolver.js deleted file mode 100644 index 1c66352..0000000 --- a/src/message-resolver.js +++ /dev/null @@ -1,256 +0,0 @@ -'use strict'; - -import ArrayUtils from 'gdbots/common/util/array-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import NoMessageForCurie from 'gdbots/pbj/exception/no-message-for-curie'; -import NoMessageForSchemaId from 'gdbots/pbj/exception/no-message-for-schema-id'; -import NoMessageForMixin from 'gdbots/pbj/exception/no-message-for-mixin'; -import MoreThanOneMessageForMixin from 'gdbots/pbj/exception/more-than-one-message-for-mixin'; - -let _registerPromise = null; -let _messages = {}; -let _resolved = {}; -let _resolvedMixins = {}; - -export default class MessageResolver -{ - /** - * Used when dynamically loading messages. - * - * @see self::registerMap - * - * @var Promise - */ - static registerPromise() { - return _registerPromise || Promise.resolve(true); - } - - /** - * An array of all the available messages keyed by the schema resolver key - * and curies for resolution that is not version specific. - * - * @var array - */ - static messages() { - return _messages; - } - - /** - * An array of resolved messages in this request. - * - * @var array - */ - static resolved() { - return _resolved; - } - - /** - * An array of resolved lookups by mixin, keyed by the mixin id with major rev - * and optionally a package and category (for faster lookups) - * - * @see SchemaId::getCurieMajor - * - * @var Message[] - */ - static resolvedMixins() { - return _resolvedMixins; - } - - /** - * Returns all of the registed schemas. - * - * @var Message[] - */ - static all() { - return _messages; - } - - /** - * Returns the Message to be used for the provided schema id. - * - * @param SchemaId id - * - * @return Message - * - * @throws NoMessageForSchemaId - */ - static resolveId(id) { - let curieMajor = id.getCurieMajor(); - if (-1 !== Object.keys(_resolved).indexOf(curieMajor)) { - return _resolved[curieMajor]; - } - - let message = null; - - if (-1 !== Object.keys(_messages).indexOf(curieMajor)) { - message = _messages[curieMajor]; - _resolved[curieMajor] = message; - return message; - } - - let curie = id.getCurie().toString(); - if (-1 !== Object.keys(_messages).indexOf(curie)) { - message = _messages[curie]; - _resolved[curieMajor] = message; - _resolved[curie] = message; - return message; - } - - throw new NoMessageForSchemaId(id); - } - - /** - * Returns the Message to be used for the provided curie. - * - * @param SchemaCurie curie - * - * @return Message - * - * @throws NoMessageForCurie - */ - static resolveCurie(curie) { - let key = curie.toString(); - if (-1 !== Object.keys(_resolved).indexOf(key)) { - return _resolved[key]; - } - - if (-1 !== Object.keys(_messages).indexOf(key)) { - let message = _messages[key]; - _resolved[key] = message; - return message; - } - - throw new NoMessageForCurie(curie); - } - - /** - * Adds a single message to the resolver. This is used in tests or dynamic - * message creation (not a typical use case). - * - * @param Message message - * @param Schema schema - */ - static registerSchema(message, schema) { - _messages[schema.getId().getCurieMajor()] = message; - } - - /** - * Adds a single schema id and message. - * - * @see SchemaId::getCurieMajor - * - * @param SchemaId|string id - * @param Message message - */ - static register(id, message) { - if ('SchemaId' === SystemUtils.getClass(id)) { - id = id.getCurieMajor(); - } - - _messages[id] = message; - } - - /** - * Registers an array of id => messagePath values to the resolver. - * - * @param array map - */ - static registerMap(map = {}) { - let promises = []; - - ArrayUtils.each(map, function(value, key) { - if (map.hasOwnProperty(key)) { - if ('object' === typeof value && value.hasTrait('Message')) { - _messages[message.schema().getId().getCurieMajor()] = message; - } else { - promises.push(SystemUtils.import(value)); - } - } - }); - - _registerPromise = Promise.all(promises).then(function(messages) { - ArrayUtils.each(messages, function(message) { - - // @todo: check removing the `default` property - message = message.default; - - _messages[message.schema().getId().getCurieMajor()] = message; - }.bind(this)); - - _registerPromise = null; - }.bind(this)); - } - - /** - * Return the one message expected to be using the provided mixin. - * - * @param Mixin mixin - * @param string inPackage - * @param string inCategory - * - * @return Message - * - * @throws MoreThanOneMessageForMixin - * @throws NoMessageForMixin - */ - static findOneUsingMixin(mixin, inPackage = null, inCategory = null) { - let messages = this.findAllUsingMixin(mixin, inPackage, inCategory); - if (1 !== messages.length) { - throw new MoreThanOneMessageForMixin(mixin, messages); - } - - return messages[0]; - } - - /** - * Returns an array of messages expected to be using the provided mixin. - * - * @param Mixin mixin - * @param string inPackage - * @param string inCategory - * - * @return Message[] - * - * @throws NoMessageForMixin - */ - static findAllUsingMixin(mixin, inPackage = null, inCategory = null) { - let mixinId = mixin.getId().getCurieMajor(); - let key = mixinId + inPackage + inCategory; - - /** @var Message[] */ - let messages = []; - - if (-1 === Object.keys(_resolvedMixins).indexOf(key)) { - let filtered = (inPackage && inPackage.length) || (inCategory && inCategory.length); - - ArrayUtils.each(_messages, function(message, id) { - if (filtered) { - let curie = id.split(':'); - - if (inPackage && inPackage.length && curie[1] != inPackage) { - return; - } - - if (inCategory && inCategory.length && curie[2] != inCategory) { - return; - } - } - - let schema = message.schema(); - if (schema.hasMixin(mixinId)) { - messages.push(message); - } - }); - - _resolvedMixins[key] = messages; - } else { - messages = _resolvedMixins[key]; - } - - if (!messages || messages.length === 0) { - throw new NoMessageForMixin(mixin); - } - - return messages; - } -} diff --git a/src/message.js b/src/message.js deleted file mode 100644 index 866c8bc..0000000 --- a/src/message.js +++ /dev/null @@ -1,911 +0,0 @@ -'use strict'; - -import FromArray from 'gdbots/common/from-array'; -import ToArray from 'gdbots/common/to-array'; -import ArrayUtils from 'gdbots/common/util/array-utils'; -import StringUtils from 'gdbots/common/util/string-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import SchemaNotDefined from 'gdbots/pbj/exception/schema-not-defined'; -import RequiredFieldNotSet from 'gdbots/pbj/exception/required-field-not-set'; -import FrozenMessageIsImmutable from 'gdbots/pbj/exception/frozen-message-is-immutable'; -import LogicException from 'gdbots/pbj/exception/logic-exception'; -import ArraySerializer from 'gdbots/pbj/serializer/array-serializer'; -import {PBJ_FIELD_NAME} from 'gdbots/pbj/schema'; - -/** - * An array of schemas per message type. - * ['Fully\Qualified\ClassName' => [ array of Schema objects ] - * - * @var array - */ -let _schemas = {}; - -/** @var ArraySerializer */ -let serializer = null; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class Message extends SystemUtils.mixinClass(null, FromArray, ToArray) -{ - /** - * Nothing fancy on new messages... we let the serializers or application code get fancy. - */ - constructor() { - super(); // require before using `this` - - privateProps.set(this, { - /** @var array */ - data: {}, - - /** - * An array of fields that have been cleared or set to null that - * must be included when serialized so it's clear that the - * value has been unset. - * - * @var array - */ - clearedFields: {}, - - /** - * @see Message::freeze - * - * @var bool - */ - isFrozen: false, - - /** - * @see Message::isReplay - * - * @var bool - */ - isReplay: false - }); - } - - /** - * @return Schema - * - * @throws SchemaNotDefined - */ - static schema() { - let type = this.name; - if (undefined !== _schemas[type]) { - return _schemas[type]; - } - - let schema = this.defineSchema(); - if ('Schema' !== SystemUtils.getClass(schema)) { - throw new SchemaNotDefined('Message [' + type + '] must return a Schema from the defineSchema method.'); - } - - if (schema.getClassName() !== type) { - throw new SchemaNotDefined('Schema [' + schema.getId().toString() + '] returned from defineSchema must be for class [' + type + '], not [' + schema.getClassName() + ']'); - } - - _schemas[type] = schema; - return _schemas[type]; - } - - /** - * @return Schema - * - * @throws SchemaNotDefined - */ - static defineSchema() { - throw new SchemaNotDefined('Message [' + this.name + '] must return a Schema from the defineSchema method.'); - } - - /** - * Creates a new message with the defaults populated. - * - * @return static - */ - static create() { - /** @var Message message */ - let message = new this(); - return message.populateDefaults(); - } - - /** - * Returns a new message from the provided array using the Array Serializer. - * @see Gdbots\Pbj\Serializer\ArraySerializer::deserialize - * - * @param array data - * - * @return static - */ - static fromArray(data = {}) { - if (null === serializer) { - serializer = new ArraySerializer(); - } - - if (undefined === data[PBJ_FIELD_NAME]) { - data[PBJ_FIELD_NAME] = this.schema().getId().toString(); - } - - return serializer.deserialize(data); - } - - /** - * Returns the message as an associative array using the Array Serializer. - * @see Gdbots\Pbj\Serializer\ArraySerializer::serialize - * - * @return array - */ - toArray() { - if (null === serializer) { - serializer = new ArraySerializer(); - } - - return serializer.serialize(this); - } - - /** - * Returns a Yaml string version of the message. - * Useful for debugging or logging. - * - * @param array options - * - * @return string - */ - toYaml(options = {}) { - throw new Error('Not yet implemented.'); - } - - /** - * Returns the message as a human readable string. - * - * @return string - */ - toString() { - return this.toArray(); - } - - /** - * Generates an md5 hash of the json representation of the current message. - * - * @param string[] ignoredFields - * - * @return string - */ - generateEtag(ignoredFields = []) { - if (null === serializer) { - serializer = new ArraySerializer(); - } - - let array = serializer.serialize(this, { 'includeAllFields': true }); - - if (ignoredFields.length === 0) { - return StringUtils.md5(JSON.stringify(array)); - } - - ArrayUtils.each(ignoredFields, function(value, key) { - delete array[ignoredFields[key]]; - }); - - return StringUtils.md5(JSON.stringify(array)); - } - - /** - * Generates a reference to this message with an optional tag. - * - * @param string tag - * - * @return MessageRef - */ - generateMessageRef(tag = null) { - throw new Error('Interface function.'); - } - - /** - * Returns an array that can be used in a uri template to generate - * a uri/url for this message. - * @link https://tools.ietf.org/html/rfc6570 - * @link https://github.com/gdbots/uri-template-php - * - * @return array - */ - getUriTemplateVars() { - throw new Error('Interface function.'); - } - - /** - * Verifies all required fields have been populated. - * - * @return static - * - * @throws GdbotsPbjException - * @throws RequiredFieldNotSet - */ - validate() { - ArrayUtils.each(this.constructor.schema().getRequiredFields(), function(field) { - if (!this.has(field.getName())) { - throw new RequiredFieldNotSet(this, field); - } - }.bind(this)); - - return this; - } - - /** - * Freezes the message, making it immutable. The message must be valid - * before it can be frozen so this may throw an exception if some required - * fields have not been populated. - * - * @return static - * - * @throws GdbotsPbjException - * @throws RequiredFieldNotSet - */ - freeze() { - if (privateProps.get(this).isFrozen) { - return this; - } - - this.validate(); - privateProps.get(this).isFrozen = true; - - ArrayUtils.each(this.constructor.schema().getFields(), function(field) { - if (field.getType().isMessage()) { - /** @var self value */ - let value = this.get(field.getName()); - if (!value || value.length === 0) { - return; - } - - if (!Array.isArray(value) && value.hasTrait('Message')) { - value.freeze(); - return; - } - - /** @var self value[v] */ - ArrayUtils.each(value, function(v, k) { - value[k].freeze(); - }); - } - }.bind(this)); - - return this; - } - - /** - * Returns true if the message has been frozen. A frozen message is - * immutable and cannot be modified. - * - * @return bool - */ - isFrozen() { - return privateProps.get(this).isFrozen; - } - - /** - * Returns true if the data of the message matches. - * - * @param Message other - * - * @return bool - */ - equals(other) { - return JSON.stringify(this) === JSON.stringify(other); - } - - /** - * Returns true if this message is being replayed. Providing a value - * will set the flag but this can only be done once. Note that - * setting a message as being "replayed" will also freeze the message. - * - * @param bool|null replay - * - * @return bool - * - * @throws LogicException - */ - isReplay(replay = null) { - if (null === replay) { - if (null === privateProps.get(this).isReplay) { - privateProps.get(this).isReplay = false; - } - return privateProps.get(this).isReplay; - } - - if (null === privateProps.get(this).isReplay) { - privateProps.get(this).isReplay = Boolean(replay); - if (privateProps.get(this).isReplay) { - this.freeze(); - } - return privateProps.get(this).isReplay; - } - - throw new LogicException('You can only set the replay mode on one time.'); - } - - /** - * Populates the defaults on all fields or just the fieldName provided. - * Operation will NOT overwrite any fields already set. - * - * @param string|null fieldName - * - * @return static - */ - populateDefaults(fieldName = null) { - guardFrozenMessage.bind(this)(); - - if (fieldName) { - populateDefault.bind(this)(this.constructor.schema().getField(fieldName)); - - return this; - } - - ArrayUtils.each(this.constructor.schema().getFields(), function(field) { - populateDefault.bind(this)(field); - }.bind(this)); - - return this; - } - - /** - * Returns true if the field has been populated. - * - * @param string fieldName - * - * @return bool - */ - has(fieldName) { - if (undefined === privateProps.get(this).data[fieldName]) { - return false; - } - - if (Array.isArray(privateProps.get(this).data[fieldName])) { - return privateProps.get(this).data[fieldName] && privateProps.get(this).data[fieldName].length; - } - - return true; - } - - /** - * Returns the value for the given field. If the field has not - * been set you will get a null value. - * - * @param string fieldName - * @param mixed defaultValue - * - * @return mixed - */ - get(fieldName, defaultValue = null) { - if (!this.has(fieldName)) { - return defaultValue; - } - - let field = this.constructor.schema().getField(fieldName); - if (field.isASet()) { - return Object.keys(privateProps.get(this).data[fieldName]).map(function(v) { - return privateProps.get(this).data[fieldName][v]; - }.bind(this)); - } - - return privateProps.get(this).data[fieldName]; - } - - /** - * Clears the value of a field. - * - * @param string fieldName - * - * @return static - * - * @throws GdbotsPbjException - * @throws RequiredFieldNotSet - */ - clear(fieldName) { - guardFrozenMessage.bind(this)(); - - let field = this.constructor.schema().getField(fieldName); - - delete privateProps.get(this).data[fieldName]; - - privateProps.get(this).clearedFields[fieldName] = true; - - populateDefault.bind(this)(field); - - return this; - } - - /** - * Returns true if the field has been cleared. - * - * @param string fieldName - * - * @return bool - */ - hasClearedField(fieldName) { - return undefined !== privateProps.get(this).clearedFields[fieldName]; - } - - /** - * Returns an array of field names that have been cleared. - * - * @return array - */ - getClearedFields() { - return Object.keys(privateProps.get(this).clearedFields); - } - - /** - * @deprecated Use "set" instead, the method signature is the same. - * - * @param string fieldName - * @param mixed value - * - * @return static - * - * @throws GdbotsPbjException - */ - setSingleValue(fieldName, value) { - return this.set(fieldName, value); - } - - /** - * Sets a single value field. - * - * @param string fieldName - * @param mixed value - * - * @return static - * - * @throws GdbotsPbjException - */ - set(fieldName, value) { - guardFrozenMessage.bind(this)(); - - let field = this.constructor.schema().getField(fieldName); - if (!field.isASingleValue()) { - throw new Error('Field [' + fieldName + '] must be a single value.'); - } - - if (null === value) { - return this.clear(fieldName); - } - - field.guardValue(value); - - privateProps.get(this).data[fieldName] = value; - - delete privateProps.get(this).clearedFields[fieldName]; - - return this; - } - - /** - * Returns true if the provided value is in the set of values. - * - * @param string fieldName - * @param mixed value - * - * @return bool - */ - isInSet(fieldName, value) { - if (!privateProps.get(this).data[fieldName] - || privateProps.get(this).data[fieldName].length === 0 - || 'object' !== typeof privateProps.get(this).data[fieldName] - ) { - return false; - } - - if (!(/object|boolean|number|string/).test(typeof value)) { - return false; - } - - let key = null; - try { - key = value.toString(); - } catch (e) { - key = value; - } - key = String(key).trim().toLowerCase(); - - if (0 === key.length) { - return false; - } - - return undefined !== privateProps.get(this).data[fieldName][String(key).trim().toLowerCase()]; - } - - /** - * Adds an array of unique values to an unsorted set of values. - * - * @param string fieldName - * @param array values - * - * @return static - * - * @throws GdbotsPbjException - */ - addToSet(fieldName, values) { - guardFrozenMessage.bind(this)(); - - let field = this.constructor.schema().getField(fieldName); - if (!field.isASet()) { - throw new Error('Field [' + fieldName + '] must be a set.'); - } - - if (undefined === privateProps.get(this).data[fieldName]) { - privateProps.get(this).data[fieldName] = {}; - } - - ArrayUtils.each(values, function(value) { - if (0 === value.length) { - return; - } - - field.guardValue(value); - - let key = null; - try { - key = value.toString(); - } catch (e) { - key = value; - } - key = String(key).trim().toLowerCase(); - - privateProps.get(this).data[fieldName][key] = value; - }.bind(this)); - - if (privateProps.get(this).data[fieldName] && privateProps.get(this).data[fieldName].length) { - delete privateProps.get(this).clearedFields[fieldName]; - } - - return this; - } - - /** - * Removes an array of values from a set. - * - * @param string fieldName - * @param array values - * - * @return static - * - * @throws GdbotsPbjException - */ - removeFromSet(fieldName, values) { - guardFrozenMessage.bind(this)(); - - let field = this.constructor.schema().getField(fieldName); - if (!field.isASet()) { - throw new Error('Field [' + fieldName + '] must be a set.'); - } - - ArrayUtils.each(values, function(value) { - if (0 === value.length) { - return; - } - - let key = null; - try { - key = value.toString(); - } catch (e) { - key = value; - } - key = String(key).trim().toLowerCase(); - - if (undefined !== privateProps.get(this).data[fieldName][key]) { - delete privateProps.get(this).data[fieldName][key]; - } - }.bind(this)); - - if (!privateProps.get(this).data[fieldName] || privateProps.get(this).data[fieldName].length === 0) { - privateProps.get(this).clearedFields[fieldName] = true - } - - return this; - } - - /** - * Returns true if the provided value is in the list of values. - * This is a NOT a strict comparison, it uses "==". - * @link http://php.net/manual/en/function.in-array.php - * - * @param string fieldName - * @param mixed value - * - * @return bool - */ - isInList(fieldName, value) { - if (!privateProps.get(this).data[fieldName] - || privateProps.get(this).data[fieldName].length === 0 - || !Array.isArray(privateProps.get(this).data[fieldName]) - ) { - return false; - } - - return -1 !== privateProps.get(this).data[fieldName].indexOf(value); - } - - /** - * Returns an item in a list or null if it doesn't exist. - * - * @param string fieldName - * @param int index - * @param mixed defaultValue - * - * @return mixed - */ - getFromListAt(fieldName, index, defaultValue = null) { - index = parseInt(index); - - if (!privateProps.get(this).data[fieldName] - || privateProps.get(this).data[fieldName].length === 0 - || !Array.isArray(privateProps.get(this).data[fieldName]) - || undefined === privateProps.get(this).data[fieldName][index] - ) { - return defaultValue; - } - - return privateProps.get(this).data[fieldName][index]; - } - - /** - * Adds an array of values to an unsorted list/array (not unique). - * - * @param string fieldName - * @param array values - * - * @return static - * - * @throws GdbotsPbjException - */ - addToList(fieldName, values) { - guardFrozenMessage.bind(this)(); - - let field = this.constructor.schema().getField(fieldName); - if (!field.isAList()) { - throw new Error('Field [' + fieldName + '] must be a list.'); - } - - if (undefined === privateProps.get(this).data[fieldName]) { - privateProps.get(this).data[fieldName] = []; - } - - ArrayUtils.each(values, function(value) { - field.guardValue(value); - - privateProps.get(this).data[fieldName].push(value); - }.bind(this)); - - delete privateProps.get(this).clearedFields[fieldName]; - - return this; - } - - /** - * Removes the element from the array at the index. - * - * @param string fieldName - * @param int index - * - * @return static - * - * @throws GdbotsPbjException - */ - removeFromListAt(fieldName, index) { - guardFrozenMessage.bind(this)(); - - let field = this.constructor.schema().getField(fieldName); - if (!field.isAList()) { - throw new Error('Field [' + fieldName + '] must be a list.'); - } - - index = parseInt(index); - - if (!privateProps.get(this).data[fieldName] || privateProps.get(this).data[fieldName].length === 0) { - return this; - } - - if (undefined !== privateProps.get(this).data[fieldName][index]) { - delete privateProps.get(this).data[fieldName][index]; - } - - if (!privateProps.get(this).data[fieldName] || privateProps.get(this).data[fieldName].length === 0) { - privateProps.get(this).clearedFields[fieldName] = true; - - return this; - } - - privateProps.get(this).data[fieldName] = Object.keys(privateProps.get(this).data[fieldName]).map(function(v) { - return privateProps.get(this).data[fieldName][v]; - }); - - return this; - } - - /** - * Returns true if the map contains the provided key. - * - * @param string fieldName - * @param string key - * - * @return bool - */ - isInMap(fieldName, key) { - if (!privateProps.get(this).data[fieldName] - || Object.keys(privateProps.get(this).data[fieldName]).length === 0 - || 'object' !== typeof privateProps.get(this).data[fieldName] - || 'string' !== typeof key - ) { - return false; - } - - return undefined !== privateProps.get(this).data[fieldName][key]; - } - - /** - * Returns the value of a key in a map or null if it doesn't exist. - * - * @param string fieldName - * @param string key - * @param mixed defaultValue - * - * @return mixed - */ - getFromMap(fieldName, key, defaultValue = null) { - if (!this.isInMap(fieldName, key)) { - return defaultValue; - } - - return privateProps.get(this).data[fieldName][key]; - } - - /** - * Adds a key/value pair to a map. - * - * @param string fieldName - * @param string key - * @param mixed value - * - * @return static - * - * @throws GdbotsPbjException - */ - addToMap(fieldName, key, value) { - guardFrozenMessage.bind(this)(); - - let field = this.constructor.schema().getField(fieldName); - if (!field.isAMap()) { - throw new Error('Field [' + fieldName + '] must be a map.'); - } - - if (null === value) { - return this.removeFromMap(fieldName, key); - } - - field.guardValue(value); - - if (undefined === privateProps.get(this).data[fieldName]) { - privateProps.get(this).data[fieldName] = {}; - } - - privateProps.get(this).data[fieldName][key] = value; - - delete privateProps.get(this).clearedFields[fieldName]; - - return this; - } - - /** - * Removes a key/value pair from a map. - * - * @param string fieldName - * @param string key - * - * @return static - * - * @throws GdbotsPbjException - */ - removeFromMap(fieldName, key) { - guardFrozenMessage.bind(this)(); - - let field = this.constructor.schema().getField(fieldName); - if (!field.isAMap()) { - throw new Error('Field [' + fieldName + '] must be a map.'); - } - - delete privateProps.get(this).data[fieldName][key]; - - if (!privateProps.get(this).data[fieldName] || privateProps.get(this).data[fieldName].length === 0) { - privateProps.get(this).clearedFields[fieldName] = true; - } - - return this; - } -} - -/** - * Recursively unfreezes this object and any of its children. - * Used internally during the clone process. - */ -function unFreeze() { - privateProps.get(this).isFrozen = false; - privateProps.get(this).isReplay = null; - - ArrayUtils.each(this.constructor.schema().getFields(), function(field) { - if (field.getType().isMessage()) { - /** @var self value */ - let value = this.get(field.getName()); - if (!value || value.length === 0) { - return; - } - - if (value.hasTrait('Message')) { - unFreeze.bind(value)(); - return; - } - - /** @var self value[v] */ - ArrayUtils.each(value, function(v, k) { - unFreeze.bind(value[k])(); - }); - } - }.bind(this)); -} - -/** - * Ensures a frozen message can't be modified. - * - * @throws FrozenMessageIsImmutable - */ -function guardFrozenMessage() { - if (privateProps.get(this).isFrozen) { - throw new FrozenMessageIsImmutable(this); - } -} - -/** - * Populates the default on a single field if it's not already set - * and the default generated is not a null value or empty array. - * - * @param Field field - * - * @return bool Returns true if a non null/empty default was applied or already present. - */ -function populateDefault(field) { - if (this.has(field.getName())) { - return true; - } - - let defaultValue = field.getDefault(this); - if (null === defaultValue) { - return false; - } - - if (field.isASingleValue()) { - privateProps.get(this).data[field.getName()] = defaultValue; - - delete privateProps.get(this).clearedFields[field.getName()]; - - return true; - } - - if (!defaultValue || defaultValue.length === 0) { - return false; - } - - /* - * sets have a special handling to deal with unique values - */ - if (field.isASet()) { - this.addToSet(field.getName(), defaultValue); - - return true; - } - - privateProps.get(this).data[field.getName()] = defaultValue; - - delete privateProps.get(this).clearedFields[field.getName()]; - - return true; -} diff --git a/src/mixin.js b/src/mixin.js deleted file mode 100644 index 53553d5..0000000 --- a/src/mixin.js +++ /dev/null @@ -1,60 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import ToArray from 'gdbots/common/to-array'; - -let _instances = {}; - -export default class Mixin extends SystemUtils.mixinClass(null, ToArray) -{ - /** - * @return Mixin - */ - static create() { - let type = this.name; - if (undefined === _instances[type]) { - _instances[type] = new this(); - } - - return _instances[type]; - } - - /** - * Returns the id for this mixin. - * - * @return SchemaId - */ - getId() { - return null; - } - - /** - * Returns an array of fields that the mixin provides. - * - * @return Field[] - */ - getFields() { - return []; - } - - /** - * @return array - */ - toArray() { - return { - id: this.getId(), - fields: this.getFields() - }; - } - - /** - * @return string - */ - toString() { - if (this.getId()) { - return this.getId().toString(); - } - - return null; - } -} diff --git a/src/schema-curie.js b/src/schema-curie.js deleted file mode 100644 index 0b60e7a..0000000 --- a/src/schema-curie.js +++ /dev/null @@ -1,158 +0,0 @@ -'use strict'; - -import InvalidSchemaCurie from 'gdbots/pbj/exception/invalid-schema-curie'; -import SchemaQName from 'gdbots/pbj/schema-q-name'; - -/** @var array */ -let _instances = {}; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -/** - * Regular expression pattern for matching a valid SchemaCurie string. - * @constant string - */ -export const VALID_PATTERN = /^([a-z0-9-]+):([a-z0-9\.-]+):([a-z0-9-]+)?:([a-z0-9-]+)/; - -/** - * Schemas can be fully qualified by the schema id (which includes the version) - * or the short form which is called a CURIE or "compact uri". - * @link http://en.wikipedia.org/wiki/CURIE - * - * Schema Curie Format: - * vendor:package:category:message - * - * @see SchemaId - * - */ -export default class SchemaCurie -{ - /** - * @param string vendor - * @param string packageName - * @param string category - * @param string message - */ - constructor(vendor, packageName, category, message) { - if (!category) { - category = ''; - } - - privateProps.set(this, { - /** @var string */ - vendor: vendor, - - /** @var string */ - package: packageName, - - /** @var string */ - category: category, - - /** @var string */ - message: message, - - /** @var string */ - curie: vendor + ':' + packageName + ':' + category + ':' + message, - - /** @var SchemaQName */ - qname: null - }); - - privateProps.get(this).qname = SchemaQName.fromCurie(this); - } - - /** - * @param SchemaId id - * - * @return SchemaCurie - */ - static fromId(id) { - let curie = id.toString().replace(':' + id.getVersion().toString(), '').substr(4); - - if (undefined !== _instances[curie]) { - return _instances[curie]; - } - - _instances[curie] = new this(id.getVendor(), id.getPackage(), id.getCategory(), id.getMessage()); - return _instances[curie]; - } - - /** - * @param string curie - * - * @return SchemaCurie - * - * @throws InvalidSchemaCurie - */ - static fromString(curie) { - if (undefined !== _instances[curie]) { - return _instances[curie]; - } - - if (curie.length > 145) { - throw new Error('Schema curie cannot be greater than 145 chars.'); - } - - let matches = curie.match(VALID_PATTERN); - if (null === matches) { - throw new InvalidSchemaCurie('Schema curie [' + curie + '] is invalid. It must match the pattern [' + VALID_PATTERN + '].'); - } - - _instances[curie] = new this(matches[1], matches[2], matches[3], matches[4]); - return _instances[curie]; - } - - /** - * @return string - */ - toString() { - return privateProps.get(this).curie; - } - - /** - * @return string - */ - getVendor() { - return privateProps.get(this).vendor; - } - - /** - * @return string - */ - getPackage() { - return privateProps.get(this).package; - } - - /** - * @return string - */ - getCategory() { - return privateProps.get(this).category; - } - - /** - * @return string - */ - getMessage() { - return privateProps.get(this).message; - } - - /** - * @return bool - */ - isMixin() { - return 'mixin' === privateProps.get(this).category; - } - - /** - * @return SchemaQName - */ - getQName() { - return privateProps.get(this).qname; - } -} diff --git a/src/schema-id.js b/src/schema-id.js deleted file mode 100644 index 9863568..0000000 --- a/src/schema-id.js +++ /dev/null @@ -1,203 +0,0 @@ -'use strict'; - -import InvalidSchemaId from 'gdbots/pbj/exception/invalid-schema-id'; -import SchemaCurie from 'gdbots/pbj/schema-curie'; -import SchemaVersion from 'gdbots/pbj/schema-version'; - -/** @var array */ -let _instances = {}; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -/** - * Regular expression pattern for matching a valid SchemaId string. - * @constant string - */ -export const VALID_PATTERN = /^pbj:([a-z0-9-]+):([a-z0-9\.-]+):([a-z0-9-]+)?:([a-z0-9-]+):([0-9]+-[0-9]+-[0-9]+)/; - -/** - * Schemas have fully qualified names, similar to a "urn". This is combination of ideas from: - * - * Amazon Resource Names (ARNs) and AWS Service Namespaces - * @link http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html - * - * SnowPlow Analytics (Iglu) - * @link http://snowplowanalytics.com/blog/2014/07/01/iglu-schema-repository-released/ - * - * @link http://en.wikipedia.org/wiki/CURIE - * - * And of course the various package managers like composer, npm, etc. - * - * Schema Id Format: - * pbj:vendor:package:category:message:version - * - * Schema Curie Format: - * vendor:package:category:message - * - * Schema Curie Major Format: - * vendor:package:category:message:v# - * - * Schema QName Format: - * vendor:message - * - * Formats: - * VENDOR: [a-z0-9-]+ - * PACKAGE: [a-z0-9\.-]+ - * CATEGORY: ([a-z0-9-]+)? (clarifies the intent of the message, e.g. command, request, event, response, etc.) - * MESSAGE: [a-z0-9-]+ - * VERSION: @see SchemaVersion::VALID_PATTERN - * - * Examples of fully qualified schema ids: - * pbj:acme:videos:event:video-uploaded:1-0-0 - * pbj:acme:users:command:register-user:1-1-0 - * pbj:acme:api.videos:request:get-video:1-0-0 - * - * The fully qualified schema identifier corresponds to a json schema implementing the Gdbots PBJ Json Schema. - * - * The schema id must be resolveable to a php class that should be able to read and write - * messages with payloads that validate using the json schema. The target class is ideally - * major revision specific. As in GetVideoV1, GetVideoV2, etc. Only "major" revisions - * should require a unique class since all other schema changes should not break anything. - * - * @see SchemaVersion - * - */ -export default class SchemaId -{ - /** - * @param string vendor - * @param string packageName - * @param string category - * @param string message - * @param string version - */ - constructor(vendor, packageName, category, message, version) { - if (!category) { - category = ''; - } - - privateProps.set(this, { - /** @var string */ - vendor: vendor, - - /** @var string */ - package: packageName, - - /** @var string */ - category: category, - - /** @var string */ - message: message, - - /** @var SchemaVersion */ - version: SchemaVersion.fromString(version), - - /** @var string */ - id: 'pbj:' + vendor + ':' + packageName + ':' + category + ':' + message + ':' + version.toString(), - - /** - * The curie is the short name for the schema (without the version) that can be used - * to reference another message without fully qualifying the version. - * - * @var SchemaCurie - */ - curie: null - }); - - privateProps.get(this).curie = SchemaCurie.fromId(this); - } - - /** - * @param string schemaId - * - * @return SchemaId - * - * @throws InvalidSchemaId - */ - static fromString(schemaId) { - if (undefined !== _instances[schemaId]) { - return _instances[schemaId]; - } - - if (schemaId.length > 145) { - throw new Error('Schema id cannot be greater than 150 chars.'); - } - - let matches = schemaId.match(VALID_PATTERN); - if (null === matches) { - throw new InvalidSchemaId('Schema id [' + schemaId + '] is invalid. It must match the pattern [' + VALID_PATTERN + '].'); - } - - _instances[schemaId] = new this(matches[1], matches[2], matches[3], matches[4], matches[5]); - return _instances[schemaId]; - } - - /** - * @return string - */ - toString() { - return privateProps.get(this).id; - } - - /** - * @return string - */ - getVendor() { - return privateProps.get(this).vendor; - } - - /** - * @return string - */ - getPackage() { - return privateProps.get(this).package; - } - - /** - * @return string - */ - getCategory() { - return privateProps.get(this).category; - } - - /** - * @return string - */ - getMessage() { - return privateProps.get(this).message; - } - - /** - * @return SchemaVersion - */ - getVersion() { - return privateProps.get(this).version; - } - - /** - * @return SchemaCurie - */ - getCurie() { - return privateProps.get(this).curie; - } - - /** - * Returns the major version qualified curie. This should be used by the MessageResolver, - * event dispatchers, etc. where consumers will need to be able to reliably type hint or - * locate classes and provide functionality for a given message, with the expectation - * that a major revision is likely not compatible with another major revision of the - * same message. - * - * e.g. "vendor:package:category:message:v1" - * - * @return string - */ - getCurieMajor() { - return privateProps.get(this).curie.toString() + ':v' + privateProps.get(this).version.getMajor(); - } -} diff --git a/src/schema-q-name.js b/src/schema-q-name.js deleted file mode 100644 index 0f4094b..0000000 --- a/src/schema-q-name.js +++ /dev/null @@ -1,119 +0,0 @@ -'use strict'; - -import InvalidSchemaQName from 'gdbots/pbj/exception/invalid-schema-q-name'; - -/** @var array */ -let _instances = {}; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -/** - * Regular expression pattern for matching a valid SchemaCurie string. - * @constant string - */ -export const VALID_PATTERN = /^([a-z0-9-]+):([a-z0-9-]+)$/; - -/** - * Schemas can be referenced in an extremely compact manner using a QName. - * This is NOT 100% reliably unique as the larger your app is the more likely the - * same message name will be duplicated in another service. - * @link https://en.wikipedia.org/wiki/QName - * - * Schema QName Format: - * vendor:message - * - * @see SchemaId - * @see SchemaCurie - * - */ -export default class SchemaQName -{ - /** - * @param string vendor - * @param string message - */ - constructor(vendor, message) { - privateProps.set(this, { - /** @var string */ - vendor: vendor, - - /** @var string */ - message: message, - - /** @var string */ - qname: vendor + ':' + message - }); - } - - /** - * @param SchemaId id - * - * @return SchemaQName - */ - static fromId(id) { - return this.fromCurie(id.getCurie()); - } - - /** - * @param SchemaCurie curie - * - * @return SchemaQName - */ - static fromCurie(curie) { - let qname = curie.getVendor() + ':' + curie.getMessage(); - - if (undefined !== _instances[qname]) { - return _instances[qname]; - } - - _instances[qname] = new this(curie.getVendor(), curie.getMessage()); - return _instances[qname]; - } - - /** - * @param string qname - * - * @return SchemaQName - * - * @throws InvalidSchemaQName - */ - static fromString(qname) { - if (undefined !== _instances[qname]) { - return _instances[qname]; - } - - let matches = qname.match(VALID_PATTERN); - if (null === matches) { - throw new InvalidSchemaCurie('SchemaQName [' + qname + '] is invalid. It must match the pattern [' + VALID_PATTERN + '].'); - } - - _instances[qname] = new this(matches[1], matches[4]); - return _instances[qname]; - } - - /** - * @return string - */ - toString() { - return privateProps.get(this).qname; - } - - /** - * @return string - */ - getVendor() { - return privateProps.get(this).vendor; - } - - /** - * @return string - */ - getMessage() { - return privateProps.get(this).message; - } -} diff --git a/src/schema-version.js b/src/schema-version.js deleted file mode 100644 index f0e0767..0000000 --- a/src/schema-version.js +++ /dev/null @@ -1,115 +0,0 @@ -'use strict'; - -import InvalidSchemaVersion from 'gdbots/pbj/exception/invalid-schema-version'; - -/** - * Regular expression pattern for matching a valid SchemaVersion string. - * @constant string - */ -export const VALID_PATTERN = /^([0-9]+)-([0-9]+)-([0-9]+)/; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -/** - * Similar to semantic versioning but with dashes and no "alpha, beta, etc." qualifiers. - * - * E.g. 1-0-0 (major-minor-patch) - * - * MAJOR - * Is incremented when a change is made which breaks the rules of Protobuf/Thrift backward compatibility, - * such as changing the type of a field. - * - * MINOR - * Is a change which is backward compatible but not forward compatible. Records created from - * the old version of the schema can be deserialized using the new schema, but not the other way - * around. Example: adding a new field to a union type. - * - * PATCH - * Is a change which is both backward compatible and forward compatible. The previous version of - * the schema can be used to deserialize records created from the new version of the schema, and - * vice versa. Example: adding a new optional field. - * - * @link http://semver.org/ - * @link http://snowplowanalytics.com/blog/2014/05/13/introducing-schemaver-for-semantic-versioning-of-schemas/ - * - */ -export default class SchemaVersion -{ - /** - * @param int major - * @param int minor - * @param int patch - */ - constructor(major = 1, minor = 0, patch = 0) { - major = parseInt(major); - minor = parseInt(minor); - patch = parseInt(patch); - - privateProps.set(this, { - /** @var int */ - major: major, - - /** @var int */ - minor: minor, - - /** @var int */ - patch: patch, - - /** - * E.g. 1-0-0 (major-minor-patch) - * - * @var string - */ - version: major + '-' + minor + '-' + patch - }); - } - - /** - * @param string version SchemaVersion string, e.g. 1-0-0 - * - * @return SchemaVersion - * - * @throws InvalidSchemaVersion - */ - static fromString(version = '1-0-0') { - let matches = version.match(VALID_PATTERN); - if (null === matches) { - throw new InvalidSchemaVersion('Schema version [' + version + '] is invalid. It must match the pattern [' + VALID_PATTERN + '].'); - } - - return new this(matches[1], matches[2], matches[3]); - } - - /** - * @return string - */ - toString() { - return privateProps.get(this).version; - } - - /** - * @return int - */ - getMajor() { - return privateProps.get(this).major; - } - - /** - * @return int - */ - getMinor() { - return privateProps.get(this).minor; - } - - /** - * @return int - */ - getPatch() { - return privateProps.get(this).patch; - } -} diff --git a/src/schema.js b/src/schema.js deleted file mode 100644 index 2c5377d..0000000 --- a/src/schema.js +++ /dev/null @@ -1,294 +0,0 @@ -'use strict'; - -import ToArray from 'gdbots/common/to-array'; -import ArrayUtils from 'gdbots/common/util/array-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import FieldAlreadyDefined from 'gdbots/pbj/exception/field-already-defined'; -import FieldNotDefined from 'gdbots/pbj/exception/field-not-defined'; -import FieldOverrideNotCompatible from 'gdbots/pbj/exception/field-override-not-compatible'; -import MixinAlreadyAdded from 'gdbots/pbj/exception/mixin-already-added'; -import MixinNotDefined from 'gdbots/pbj/exception/mixin-not-defined'; -import SchemaId from 'gdbots/pbj/schema-id'; -import FieldBuilder from 'gdbots/pbj/field-builder'; -import StringType from 'gdbots/pbj/type/string-type'; - -export const PBJ_FIELD_NAME = '_schema'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class Schema extends SystemUtils.mixinClass(null, ToArray) -{ - /** - * @param SchemaId|string id - * @param string className - * @param Field[] fields - * @param Mixin[] mixins - */ - constructor(id, className, fields = [], mixins = []) { - super(); // require before using `this` - - privateProps.set(this, { - /** @var string */ - id: 'SchemaId' === SystemUtils.getClass(id) ? id : SchemaId.fromString(id), - - /** @var string */ - className: className, - - /** @var Mixin[] */ - mixins: {}, - mixinsByCurie: {}, - - /** @var Field[] */ - fields: {}, - requiredFields: {}, - - /** @var string[] */ - mixinIds: [], - mixinCuries: [] - }); - - addField.bind(this)( - FieldBuilder.create(PBJ_FIELD_NAME, StringType.create()) - .required() - .pattern(SchemaId.VALID_PATTERN) - .withDefault(privateProps.get(this).id.toString()) - .build() - ); - - ArrayUtils.each(mixins, function(mixin) { - addMixin.bind(this)(mixin); - }.bind(this)); - - ArrayUtils.each(fields, function(field) { - addField.bind(this)(field); - }.bind(this)); - - /** @var string[] */ - privateProps.get(this).mixinIds = Object.keys(privateProps.get(this).mixins); - - /** @var string[] */ - privateProps.get(this).mixinCuries = Object.keys(privateProps.get(this).mixinsByCurie); - } - - /** - * @return string - */ - toString() { - return privateProps.get(this).id.toString(); - } - - /** - * @return array - */ - toArray() { - return { - 'id': privateProps.get(this).id, - 'curie': this.getCurie(), - 'curie_major': this.getCurieMajor(), - 'qname': this.getQName(), - 'class_name': privateProps.get(this).className, - 'mixins': privateProps.get(this).mixins.map( - function(mixin) { - return mixin.getId(); - } - ), - 'fields': privateProps.get(this).fields - }; - } - - /** - * @return SchemaId - */ - getId() { - return privateProps.get(this).id; - } - - /** - * @return SchemaCurie - */ - getCurie() { - return privateProps.get(this).id.getCurie(); - } - - /** - * @param string|SchemaCurie curie - * - * @return bool - */ - isA(curie) { - if ('SchemaCurie' === SystemUtils.getClass(curie)) { - curie = curie.toString(); - } - - return curie === this.getCurie().toString(); - } - - /** - * @see SchemaId::getCurieMajor - * - * @return string - */ - getCurieMajor() { - return privateProps.get(this).id.getCurieMajor(); - } - - /** - * @return SchemaQName - */ - getQName() { - return privateProps.get(this).id.getCurie().getQName(); - } - - /** - * @return string - */ - getClassName() { - return privateProps.get(this).className; - } - - /** - * @param string fieldName - * - * @return bool - */ - hasField(fieldName) { - return undefined !== privateProps.get(this).fields[fieldName]; - } - - /** - * @param string fieldName - * - * @return Field - * - * @throws FieldNotDefined - */ - getField(fieldName) { - if (undefined === privateProps.get(this).fields[fieldName]) { - throw new FieldNotDefined(this, fieldName); - } - - return privateProps.get(this).fields[fieldName]; - } - - /** - * @return Field[] - */ - getFields() { - return privateProps.get(this).fields; - } - - /** - * @return Field[] - */ - getRequiredFields() { - return privateProps.get(this).requiredFields; - } - - /** - * Returns true if the mixin is on this schema. Id provided can be - * qualified to major rev or just the curie. - * @see SchemaId::getCurieMajor - * - * @param string mixinId - * - * @return bool - */ - hasMixin(mixinId) { - return undefined !== privateProps.get(this).mixins[mixinId] || undefined !== privateProps.get(this).mixinsByCurie[mixinId]; - } - - /** - * @param string mixinId - * - * @return Mixin - * - * @throws MixinNotDefined - */ - getMixin(mixinId) { - if (undefined !== privateProps.get(this).mixins[mixinId]) { - return privateProps.get(this).mixins[mixinId]; - } - - if (undefined !== privateProps.get(this).mixinsByCurie[mixinId]) { - return privateProps.get(this).mixinsByCurie[mixinId]; - } - - throw new MixinNotDefined(this, mixinId); - } - - /** - * @return Mixin[] - */ - getMixins() { - return privateProps.get(this).mixins; - } - - /** - * Returns an array of curies with the major rev. - * @see SchemaId::getCurieMajor - * - * @return array - */ - getMixinIds() { - return privateProps.get(this).mixinIds; - } - - /** - * Returns an array of curies (string version). - * - * @return array - */ - getMixinCuries() { - return privateProps.get(this).mixinCuries; - } -} - -/** - * @param Field field - * - * @throws FieldAlreadyDefined - * @throws FieldOverrideNotCompatible - */ -function addField(field) { - let fieldName = field.getName(); - if (this.hasField(fieldName)) { - let existingField = this.getField(fieldName); - if (!existingField.isOverridable()) { - throw new FieldAlreadyDefined(this, fieldName); - } - if (!existingField.isCompatibleForOverride(field)) { - throw new FieldOverrideNotCompatible(this, fieldName, field); - } - } - - privateProps.get(this).fields[fieldName] = field; - if (field.isRequired()) { - privateProps.get(this).requiredFields[fieldName] = field; - } -} - -/** - * @param Mixin mixin - * - * @throws MixinAlreadyAdded - */ -function addMixin(mixin) { - let id = mixin.getId(); - let curieStr = id.getCurie().toString(); - - if (undefined !== privateProps.get(this).mixinsByCurie[curieStr]) { - throw new MixinAlreadyAdded(this, privateProps.get(this).mixinsByCurie[curieStr], mixin); - } - - privateProps.get(this).mixins[id.getCurieMajor()] = mixin; - privateProps.get(this).mixinsByCurie[curieStr] = mixin; - - ArrayUtils.each(mixin.getFields(), function(field) { - addField.bind(this)(field); - }.bind(this)); -} diff --git a/src/serializer/array-serializer.js b/src/serializer/array-serializer.js deleted file mode 100644 index b566180..0000000 --- a/src/serializer/array-serializer.js +++ /dev/null @@ -1,273 +0,0 @@ -'use strict'; - -import GeoPoint from 'gdbots/pbj/well-known/geo-point'; -import DynamicField from 'gdbots/pbj/well-known/dynamic-field'; -import ArrayUtils from 'gdbots/common/util/array-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import DeserializeMessageFailed from 'gdbots/pbj/exception/deserialize-message-failed'; -import EncodeValueFailed from 'gdbots/pbj/exception/encode-value-failed'; -import InvalidResolvedSchema from 'gdbots/pbj/exception/invalid-resolved-schema'; -import Serializer from 'gdbots/pbj/serializer/serializer'; -import TypeName from 'gdbots/pbj/enum/type-name'; -import FieldRule from 'gdbots/pbj/enum/field-rule'; -import Codec from 'gdbots/pbj/codec'; -import MessageRef from 'gdbots/pbj/message-ref'; -import MessageResolver from 'gdbots/pbj/message-resolver'; -import SchemaId from 'gdbots/pbj/schema-id'; -import {PBJ_FIELD_NAME} from 'gdbots/pbj/schema'; - -/** - * Options for the serializer to use, e.g. json encoding options, - * 'includeAllFields' which includes fields even if they're not set, etc. - * - * @var array - */ -let _options = {}; - -export default class ArraySerializer extends SystemUtils.mixinClass(Serializer, Codec) -{ - /** - * {@inheritdoc} - */ - serialize(message, options = {}) { - _options = options; - - return doSerialize.bind(this)(message); - } - - /** - * {@inheritdoc} - */ - deserialize(data, options = {}) { - _options = options; - - if (-1 === Object.keys(data).indexOf(PBJ_FIELD_NAME)) { - throw new Error('[' + this.constructor.name + '::deserialize] Array provided must contain the [' + PBJ_FIELD_NAME +'] key.'); - } - - return doDeserialize.bind(this)(data); - } - - /** - * @param Message message - * @param Field field - * - * @return mixed - */ - encodeMessage(message, field) { - return doSerialize.bind(this)(message); - } - - /** - * @param mixed value - * @param Field field - * - * @return Message - */ - decodeMessage(value, field) { - return doDeserialize.bind(this)(value); - } - - /** - * @param MessageRef messageRef - * @param Field field - * - * @return mixed - */ - encodeMessageRef(messageRef, field) { - return messageRef.toArray(); - } - - /** - * @param mixed value - * @param Field field - * - * @return MessageRef - */ - decodeMessageRef(value, field) { - return MessageRef.fromArray(value); - } - - /** - * @param GeoPoint geoPoint - * @param Field field - * - * @return mixed - */ - encodeGeoPoint(geoPoint, field) { - return geoPoint.toArray(); - } - - /** - * @param mixed value - * @param Field field - * - * @return GeoPoint - */ - decodeGeoPoint(value, field) { - return GeoPoint.fromArray(value); - } - - /** - * @param DynamicField dynamicField - * @param Field field - * - * @return mixed - */ - encodeDynamicField(dynamicField, field) { - return dynamicField.toArray(); - } - - /** - * @param mixed value - * @param Field field - * - * @return DynamicField - */ - decodeDynamicField(value, field) { - return DynamicField.fromArray(value); - } -} - -/** - * @param Message message - * - * @return array - */ -function doSerialize(message) { - let schema = message.constructor.schema(); - message.validate(); - - let payload = {}; - let includeAllFields = undefined !== _options.includeAllFields && true === _options.includeAllFields; - - ArrayUtils.each(schema.getFields(), function(field) { - let fieldName = field.getName(); - - if (!message.has(fieldName)) { - if (includeAllFields || message.hasClearedField(fieldName)) { - payload[fieldName] = null; - } - - return; - } - - let value = message.get(fieldName); - let type = field.getType(); - - switch (field.getRule()) { - case FieldRule.A_SINGLE_VALUE: - payload[fieldName] = type.encode(value, field, this); - - break; - - case FieldRule.A_SET: - case FieldRule.A_LIST: - payload[fieldName] = []; - - ArrayUtils.each(value, function(v) { - payload[fieldName].push(type.encode(v, field, this)); - }.bind(this)); - - break; - - case FieldRule.A_MAP: - payload[fieldName] = {}; - - ArrayUtils.each(value, function(v) { - payload[fieldName][k] = type.encode(v, field, this); - }.bind(this)); - - break; - - default: - break; - } - }.bind(this)); - - return payload; -} - -/** - * @param array data - * - * @return Message - * - * @throws \Exception - * @throws GdbotsPbjException - */ -function doDeserialize(data) { - - /** @var SchemaId schemaId */ - let schemaId = SchemaId.fromString(data[PBJ_FIELD_NAME]); - - /** @var Message message */ - let message = MessageResolver.resolveId(schemaId); - if (!message.hasTrait('Message')) { - throw new Error('Invalid message.'); - } - - message = message.create(); - - if (message.constructor.schema().getCurieMajor() !== schemaId.getCurieMajor()) { - throw new InvalidResolvedSchema(message.constructor.schema(), schemaId, message.name); - } - - let schema = message.constructor.schema(); - - ArrayUtils.each(data, function(value, fieldName) { - if (!schema.hasField(fieldName)) { - return; - } - - if (null === value) { - message.clear(fieldName); - return; - } - - let field = schema.getField(fieldName); - let type = field.getType(); - - switch (field.getRule()) { - case FieldRule.A_SINGLE_VALUE: - message.set(fieldName, type.decode(value, field, this)); - break; - - case FieldRule.A_SET: - case FieldRule.A_LIST: - if (!Array.isArray(value)) { - throw new Error('Field [' + fieldName + '] must be an array.'); - } - - let values = []; - - ArrayUtils.each(value, function(v) { - values.push(type.decode(v, field, this)); - }.bind(this)); - - if (field.isASet()) { - message.addToSet(fieldName, values); - } else { - message.addToList(fieldName, values); - } - - break; - - case FieldRule.A_MAP: - if (!ArrayUtils.isAssoc(value)) { - throw new Error('Field [' + fieldName + '] must be an associative array.'); - } - - ArrayUtils.each(value, function(v, k) { - message.addToMap(fieldName, k, type.decode(v, field, this)); - }.bind(this)); - - break; - - default: - break; - } - }.bind(this)); - - return message.set(PBJ_FIELD_NAME, schema.getId().toString()).populateDefaults(); -} diff --git a/src/serializer/json-serializer.js b/src/serializer/json-serializer.js deleted file mode 100644 index 43c76d0..0000000 --- a/src/serializer/json-serializer.js +++ /dev/null @@ -1,58 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import DeserializeMessageFailed from 'gdbots/pbj/exception/deserialize-message-failed'; -import ArraySerializer from 'gdbots/pbj/serializer/array-serializer'; - -export default class JsonSerializer extends SystemUtils.mixinClass(ArraySerializer) -{ - /** - * {@inheritdoc} - */ - serialize(message, options = {}) { - return JSON.stringify(super.serialize(message, options)); - } - - /** - * {@inheritdoc} - */ - deserialize(data, options = {}) { - if ('string' === typeof data) { - try { - data = JSON.parse(data); - } catch (e) { - if (!(e instanceof SyntaxError)) { - throw new Error('Unexpected error type in JSON.parse()') - } - - throw new DeserializeMessageFailed(getLastErrorMessage(4)); - } - } - - return super.deserialize(data, options); - } -} - -/** - * Resolves json_last_error message. - * - * @param int code - * - * @return string - */ -function getLastErrorMessage(code) { - switch (code) { - case 0: //JSON_ERROR_DEPTH - return 'Maximum stack depth exceeded'; - case 2: //JSON_ERROR_STATE_MISMATCH - return 'Underflow or the modes mismatch'; - case 3: //JSON_ERROR_CTRL_CHAR - return 'Unexpected control character found'; - case 4: //JSON_ERROR_SYNTAX - return 'Syntax error, malformed JSON'; - case 5: //JSON_ERROR_UTF8 - return 'Malformed UTF-8 characters, possibly incorrectly encoded'; - default: - return 'Unknown error'; - } -} diff --git a/src/serializer/serializer.js b/src/serializer/serializer.js deleted file mode 100644 index 5ec7ea5..0000000 --- a/src/serializer/serializer.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -export default class Serializer -{ - /** - * @param Message message - * @param array options - * - * @return mixed - */ - serialize(message, options = {}) { - throw new Error('Interface function.'); - } - - /** - * @param mixed data - * @param array options - * - * @return Message - * - * @throws \Exception - * @throws GdbotsPbjException - */ - deserialize(data, options = {}) { - throw new Error('Interface function.'); - } -} diff --git a/src/serializers/JsonSerializer.js b/src/serializers/JsonSerializer.js new file mode 100644 index 0000000..27a38f3 --- /dev/null +++ b/src/serializers/JsonSerializer.js @@ -0,0 +1,36 @@ +import AssertionFailed from '../exceptions/AssertionFailed'; +import ObjectSerializer from './ObjectSerializer'; + +export default class JsonSerializer { + /** + * @param {Message} message + * @param {Object} options + * + * @returns {string} + * + * @throws {GdbotsPbjException} + */ + static serialize(message, options = {}) { + return JSON.stringify(ObjectSerializer.serialize(message, options)); + } + + /** + * @param {string} json + * @param {Object} options + * + * @returns {Message} + * + * @throws {GdbotsPbjException} + */ + static deserialize(json, options = {}) { + let obj; + + try { + obj = JSON.parse(json); + } catch (e) { + throw new AssertionFailed('Invalid JSON.'); + } + + return ObjectSerializer.deserialize(obj, options); + } +} diff --git a/src/serializers/ObjectSerializer.js b/src/serializers/ObjectSerializer.js new file mode 100644 index 0000000..59e75e5 --- /dev/null +++ b/src/serializers/ObjectSerializer.js @@ -0,0 +1,213 @@ +/* eslint-disable no-unused-vars */ +import isArray from 'lodash/isArray'; +import isPlainObject from 'lodash/isPlainObject'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import InvalidResolvedSchema from '../exceptions/InvalidResolvedSchema'; +import DynamicField from '../well-known/DynamicField'; +import GeoPoint from '../well-known/GeoPoint'; +import MessageRef from '../MessageRef'; +import MessageResolver from '../MessageResolver'; +import { PBJ_FIELD_NAME } from '../Schema'; +import SchemaId from '../SchemaId'; + +let opt = {}; + +export default class ObjectSerializer { + /** + * @param {Message} message + * @param {Object} options + * + * @returns {Object} + * + * @throws {GdbotsPbjException} + */ + static serialize(message, options = {}) { + opt = options; + const schema = message.schema(); + message.validate(); + + const payload = {}; + const includeAllFields = opt.includeAllFields || false; + + schema.getFields().forEach((field) => { + const fieldName = field.getName(); + if (!message.has(fieldName)) { + if (includeAllFields || message.hasClearedField(fieldName)) { + payload[fieldName] = null; + } + + return; + } + + const value = message.get(fieldName); + const type = field.getType(); + + if (field.isASingleValue()) { + payload[fieldName] = type.encode(value, field, this); + return; + } + + if (field.isAMap()) { + payload[fieldName] = {}; + // eslint-disable-next-line no-return-assign + Object.keys(value).forEach(k => payload[fieldName][k] = type.encode(value[k], field, this)); + return; + } + + payload[fieldName] = []; + value.forEach(v => payload[fieldName].push(type.encode(v, field, this))); + }); + + return payload; + } + + /** + * @param {Object} obj + * @param {Object} options + * + * @returns {Message} + * + * @throws {GdbotsPbjException} + */ + static deserialize(obj, options = {}) { + opt = options; + if (!obj[PBJ_FIELD_NAME]) { + throw new AssertionFailed(`Object provided must contain the [${PBJ_FIELD_NAME}] key.`); + } + + const schemaId = SchemaId.fromString(obj[PBJ_FIELD_NAME]); + const message = new (MessageResolver.resolveId(schemaId))(); + const schema = message.schema(); + + if (schema.getCurieMajor() !== schemaId.getCurieMajor()) { + throw new InvalidResolvedSchema(schema, schemaId); + } + + Object.keys(obj).forEach((fieldName) => { + if (!schema.hasField(fieldName)) { + return; + } + + const value = obj[fieldName]; + if (value === null) { + message.clear(fieldName); + return; + } + + const field = schema.getField(fieldName); + const type = field.getType(); + + if (field.isASingleValue()) { + message.set(fieldName, type.decode(value, field, this)); + return; + } + + if (field.isASet() || field.isAList()) { + if (!isArray(value)) { + throw new AssertionFailed(`Field [${fieldName}] must be an array.`); + } + + const values = []; + value.forEach(v => values.push(type.decode(v, field, this))); + + if (field.isASet()) { + message.addToSet(fieldName, values); + } else { + message.addToList(fieldName, values); + } + + return; + } + + if (!isPlainObject(value)) { + throw new AssertionFailed(`Field [${fieldName}] must be an object.`); + } + + Object.keys(value).forEach((k) => { + message.addToMap(fieldName, k, type.decode(value[k], field, this)); + }); + }); + + return message.set(PBJ_FIELD_NAME, schema.getId().toString()).populateDefaults(); + } + + /** + * @param {Message} message + * @param {Field} field + * + * @returns {Object} + */ + static encodeMessage(message, field) { + return this.serialize(message, opt); + } + + /** + * @param {Object} value + * @param {Field} field + * + * @returns {Message} + */ + static decodeMessage(value, field) { + return this.deserialize(value, opt); + } + + /** + * @param {MessageRef} messageRef + * @param {Field} field + * + * @returns {Object} + */ + static encodeMessageRef(messageRef, field) { + return messageRef.toObject(); + } + + /** + * @param {Object} value + * @param {Field} field + * + * @returns {MessageRef} + */ + static decodeMessageRef(value, field) { + return MessageRef.fromObject(value); + } + + /** + * @param {GeoPoint} geoPoint + * @param {Field} field + * + * @returns {Object} + */ + static encodeGeoPoint(geoPoint, field) { + return geoPoint.toObject(); + } + + /** + * @param {Object} value + * @param {Field} field + * + * @returns {GeoPoint} + */ + static decodeGeoPoint(value, field) { + return GeoPoint.fromObject(value); + } + + /** + * @param {DynamicField} dynamicField + * @param {Field} field + * + * @returns {Object} + */ + static encodeDynamicField(dynamicField, field) { + return dynamicField.toObject(); + } + + /** + * @param {Object} value + * @param {Field} field + * + * @returns {DynamicField} + */ + static decodeDynamicField(value, field) { + return DynamicField.fromObject(value); + } +} diff --git a/src/type/abstract-binary-type.js b/src/type/abstract-binary-type.js deleted file mode 100644 index ae99f1c..0000000 --- a/src/type/abstract-binary-type.js +++ /dev/null @@ -1,114 +0,0 @@ -'use strict'; - -import NumberUtils from 'gdbots/common/util/number-utils'; -import UrlUtils from 'gdbots/common/util/url-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import Type from 'gdbots/pbj/type/type'; -import DecodeValueFailed from 'gdbots/pbj/exception/decode-value-failed'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class AbstractBinaryType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - constructor(typeName) { - super(typeName); - - privateProps.set(this, { - /** @var bool */ - decodeFromBase64: true, - - /** @var bool */ - encodeToBase64: true - }); - } - - /** - * @param bool useBase64 - */ - decodeFromBase64(useBase64) { - privateProps.get(this).decodeFromBase64 = Boolean(useBase64); - } - - /** - * @param bool useBase64 - */ - encodeToBase64(useBase64) { - privateProps.get(this).encodeToBase64 = Boolean(useBase64); - } - - /** - * {@inheritdoc} - */ - guard(value, field) { - if ('string' !== typeof value) { - throw new Error('Value must be a string.'); - } - - // intentionally using strlen to get byte length, not mb_strlen - let length = privateProps.get(this).encodeToBase64 ? this.encode(value, field).length : value.length; - let minLength = field.getMinLength(); - let maxLength = NumberUtils.bound(field.getMaxLength(), minLength, this.getMaxBytes()); - let okay = length >= minLength && length <= maxLength; - - if (!okay) { - throw new Error('Field [' + field.getName() + '] must be between [' + minLength + '] and [' + maxLength + '] bytes, [' + length + '] bytes given.'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - value = value.trim(); - - if (value === '') { - return null; - } - - return privateProps.get(this).encodeToBase64 ? UrlUtils.base64Encode(value) : value; - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - value = value.trim(); - - if (value === '') { - return null; - } - - if (!privateProps.get(this).decodeFromBase64) { - return value; - } - - value = UrlUtils.base64Decode(value); - if (false === value) { - throw new DecodeValueFailed(value, field, 'Strict base64_decode failed.'); - } - - return value; - } - - /** - * {@inheritdoc} - */ - isBinary() { - return true; - } - - /** - * {@inheritdoc} - */ - isString() { - return true; - } -} diff --git a/src/type/abstract-int-type.js b/src/type/abstract-int-type.js deleted file mode 100644 index f865d66..0000000 --- a/src/type/abstract-int-type.js +++ /dev/null @@ -1,53 +0,0 @@ -'use strict'; - -import NumberUtils from 'gdbots/common/util/number-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import Type from 'gdbots/pbj/type/type'; - -export default class AbstractIntType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if ('number' !== typeof value) { - throw new Error('Value must be a number.'); - } - - let min = NumberUtils.bound(field.getMin(), this.getMin(), this.getMax()); - let max = NumberUtils.bound(field.getMax(), this.getMin(), this.getMax()); - let okay = value >= min && value <= max; - - if (!okay) { - throw new Error('Field [' + field.getName() + '] value must be between [' + min + '] and [' + max + '], [' + value + '] given.'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - return parseInt(value); - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - return parseInt(value); - } - - /** - * {@inheritdoc} - */ - getDefault() { - return 0; - } - - /** - * {@inheritdoc} - */ - isNumeric() { - return true; - } -} diff --git a/src/type/abstract-string-type.js b/src/type/abstract-string-type.js deleted file mode 100644 index 5ca1b65..0000000 --- a/src/type/abstract-string-type.js +++ /dev/null @@ -1,60 +0,0 @@ -'use strict'; - -import NumberUtils from 'gdbots/common/util/number-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import Type from 'gdbots/pbj/type/type'; - -export default class AbstractStringType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if ('string' !== typeof value) { - throw new Error('Value must be a string.'); - } - - // intentionally using strlen to get byte length, not mb_strlen - let length = value.length; - let minLength = field.getMinLength(); - let maxLength = NumberUtils.bound(field.getMaxLength(), minLength, this.getMaxBytes()); - let okay = length >= minLength && length <= maxLength; - - if (!okay) { - throw new Error('Field [' + field.getName() + '] must be between [' + minLength + '] and [' + maxLength + '] bytes, [' + length + '] bytes given.'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - value = value.trim(); - - if (value === '') { - return null; - } - - return value; - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - value = value.trim(); - - if (value === '') { - return null; - } - - return value; - } - - /** - * {@inheritdoc} - */ - isString() { - return true; - } -} diff --git a/src/type/big-int-type.js b/src/type/big-int-type.js deleted file mode 100644 index 976f5d0..0000000 --- a/src/type/big-int-type.js +++ /dev/null @@ -1,68 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import BigNumber from 'gdbots/pbj/well-known/big-number'; -import Type from 'gdbots/pbj/type/type'; - -export default class BigIntType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if ('BigNumber' !== SystemUtils.getClass(value)) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "BigNumber" but is not.'); - } - - if (value.isNegative()) { - throw new Error('Field [' + field.getName() + '] cannot be negative.'); - } - - if (!value.lessThanOrEqualTo('18446744073709551615')) { - throw new Error('Field [' + field.getName() + '] cannot be greater than [18446744073709551615].'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - if ('BigNumber' === SystemUtils.getClass(value)) { - return value.toString(); - } - - return '0'; - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - if (null === value || 'BigNumber' === SystemUtils.getClass(value)) { - return value; - } - - return new BigNumber(value); - } - - /** - * {@inheritdoc} - */ - isScalar() { - return false; - } - - /** - * {@inheritdoc} - */ - getDefault() { - return new BigNumber(0); - } - - /** - * {@inheritdoc} - */ - isNumeric() { - return true; - } -} diff --git a/src/type/binary-type.js b/src/type/binary-type.js deleted file mode 100644 index b571dfd..0000000 --- a/src/type/binary-type.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import AbstractBinaryType from 'gdbots/pbj/type/abstract-binary-type'; - -export default class BinaryType extends SystemUtils.mixinClass(AbstractBinaryType) -{ - /** - * {@inheritdoc} - */ - getMaxBytes() { - return 255; - } -} diff --git a/src/type/blob-type.js b/src/type/blob-type.js deleted file mode 100644 index fa465ea..0000000 --- a/src/type/blob-type.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import AbstractBinaryType from 'gdbots/pbj/type/abstract-binary-type'; - -export default class BlobType extends SystemUtils.mixinClass(AbstractBinaryType) -{ - /** - * {@inheritdoc} - */ - allowedInSet() { - return false; - } -} diff --git a/src/type/boolean-type.js b/src/type/boolean-type.js deleted file mode 100644 index e307bbb..0000000 --- a/src/type/boolean-type.js +++ /dev/null @@ -1,51 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import Type from 'gdbots/pbj/type/type'; - -export default class BooleanType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if (value !== true && value !== false) { - throw new Error('Value "' + value + '" is not a boolean.') - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - return Boolean(value); - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - return Boolean(value); - } - - /** - * {@inheritdoc} - */ - getDefault() { - return false; - } - - /** - * @see Type::isBoolean - */ - isBoolean() { - return true; - } - - /** - * {@inheritdoc} - */ - allowedInSet() { - return false; - } -} diff --git a/src/type/date-time-type.js b/src/type/date-time-type.js deleted file mode 100644 index 683d3d3..0000000 --- a/src/type/date-time-type.js +++ /dev/null @@ -1,64 +0,0 @@ -'use strict'; - -import DateUtils from 'gdbots/common/util/date-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import Type from 'gdbots/pbj/type/type'; - -export default class DateTimeType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if (!(value instanceof Date)) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "Date" but is not.'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - if (value instanceof Date) { - return value.toISOString(); //same format as ISO8601_ZULU - } - - return null; - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - if (!value || value.length === 0) { - return null; - } - - if (value instanceof Date) { - return value; - } - - return new Date(); - } - - /** - * {@inheritdoc} - */ - isScalar() { - return false; - } - - /** - * {@inheritdoc} - */ - isString() { - return true; - } - - /** - * {@inheritdoc} - */ - allowedInSet() { - return false; - } -} diff --git a/src/type/date-type.js b/src/type/date-type.js deleted file mode 100644 index 4551508..0000000 --- a/src/type/date-type.js +++ /dev/null @@ -1,63 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import Type from 'gdbots/pbj/type/type'; - -export default class DateType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if (!(value instanceof Date)) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "Date" but is not.'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - if (value instanceof Date) { - return value.toISOString().slice(0,10); - } - - return null; - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - if (!value || value.length === 0) { - return null; - } - - if (value instanceof Date) { - return value; - } - - return new Date(); - } - - /** - * {@inheritdoc} - */ - isScalar() { - return false; - } - - /** - * {@inheritdoc} - */ - isString() { - return true; - } - - /** - * {@inheritdoc} - */ - allowedInSet() { - return false; - } -} diff --git a/src/type/decimal-type.js b/src/type/decimal-type.js deleted file mode 100644 index 97ea684..0000000 --- a/src/type/decimal-type.js +++ /dev/null @@ -1,74 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import Type from 'gdbots/pbj/type/type'; - -export default class DecimalType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if (!(+value === value && (!isFinite(value) || !!(value % 1)))) { - throw new Error('Value "' + value + '" is not a float.') - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - return parseFloat(bcadd(parseFloat(value), '0', field.getScale())); - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - return parseFloat(bcadd(parseFloat(value), '0', field.getScale())); - } - - /** - * {@inheritdoc} - */ - getDefault() { - return 0.0; - } - - /** - * {@inheritdoc} - */ - isNumeric() { - return true; - } - - /** - * {@inheritdoc} - */ - getMin() { - return -1; - } - - /** - * {@inheritdoc} - */ - getMax() { - return Infinity; - } -} - -/** - * Add two arbitrary precision numbers - * - * @param string leftOperand The left operand, as a string. - * @param string rightOperand The right operand, as a string. - * @param int scale This optional parameter is used to set the number of - * digits after the decimal place in the result. If omitted, - * it will default to the scale set globally with the bcscale() - * function, or fallback to 0 if this has not been set. - * - * @return string - */ -function bcadd(leftOperand, rightOperand, scale) { - throw new Error('Not yet implemented.'); -} diff --git a/src/type/dynamic-field-type.js b/src/type/dynamic-field-type.js deleted file mode 100644 index 776ab5a..0000000 --- a/src/type/dynamic-field-type.js +++ /dev/null @@ -1,51 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import Type from 'gdbots/pbj/type/type'; - -export default class DynamicFieldType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if ('DynamicField' !== SystemUtils.getClass(value)) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "DynamicField" but is not.'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - return codec.encodeDynamicField(value, field); - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - return codec.decodeDynamicField(value, field); - } - - /** - * {@inheritdoc} - */ - isScalar() { - return false; - } - - /** - * {@inheritdoc} - */ - encodesToScalar() { - return false; - } - - /** - * {@inheritdoc} - */ - allowedInSet() { - return false; - } -} diff --git a/src/type/float-type.js b/src/type/float-type.js deleted file mode 100644 index 804d5ac..0000000 --- a/src/type/float-type.js +++ /dev/null @@ -1,58 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import Type from 'gdbots/pbj/type/type'; - -export default class FloatType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if (!(+value === value && (!isFinite(value) || !!(value % 1)))) { - throw new Error('Value "' + value + '" is not a float.') - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - return parseFloat(value); - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - return parseFloat(value); - } - - /** - * {@inheritdoc} - */ - getDefault() { - return 0.0; - } - - /** - * {@inheritdoc} - */ - isNumeric() { - return true; - } - - /** - * {@inheritdoc} - */ - getMin() { - return -1; - } - - /** - * {@inheritdoc} - */ - getMax() { - return Infinity; - } -} diff --git a/src/type/geo-point-type.js b/src/type/geo-point-type.js deleted file mode 100644 index 3d85c1a..0000000 --- a/src/type/geo-point-type.js +++ /dev/null @@ -1,53 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import Type from 'gdbots/pbj/type/type'; -import EncodeValueFailed from 'gdbots/pbj/exception/encode-value-failed'; -import DecodeValueFailed from 'gdbots/pbj/exception/decode-value-failed'; - -export default class GeoPointType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if ('GeoPoint' !== SystemUtils.getClass(value)) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "GeoPoint" but is not.'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - return codec.encodeGeoPoint(value, field); - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - return codec.decodeGeoPoint(value, field); - } - - /** - * {@inheritdoc} - */ - isScalar() { - return false; - } - - /** - * {@inheritdoc} - */ - encodesToScalar() { - return false; - } - - /** - * {@inheritdoc} - */ - allowedInSet() { - return false; - } -} diff --git a/src/type/identifier-type.js b/src/type/identifier-type.js deleted file mode 100644 index 375b47e..0000000 --- a/src/type/identifier-type.js +++ /dev/null @@ -1,85 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import DecodeValueFailed from 'gdbots/pbj/exception/decode-value-failed'; -import Type from 'gdbots/pbj/type/type'; - -export default class IdentifierType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if (!value.hasTrait('Identifier')) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "Identifier" but is not.'); - } - - if (field.hasInstance() - && !( - field.getInstance().name === SystemUtils.getClass(value) - || value.hasTrait(field.getInstance().name) - ) - ) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "' + field.getInstance().name + '" but is not.'); - } - - // intentionally using strlen to get byte length, not mb_strlen - let length = value.toString().length; - let maxBytes = this.getMaxBytes(); - let okay = length > 0 && length <= maxBytes; - - if (!okay) { - throw new Error('Field [' + field.getName() + '] must be between [1] and [' + maxBytes + '] bytes, [' + length + '] bytes given.'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - if (value.hasTrait('Identifier')) { - return value.toString(); - } - - return null; - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - if (!value || value.length === 0) { - return null; - } - - /** @var Identifier instance */ - let instance = field.getInstance(); - - try { - return instance.fromString(value); - } catch (e) { - throw new DecodeValueFailed(value, field, e); - } - } - - /** - * {@inheritdoc} - */ - isScalar() { - return false; - } - - /** - * {@inheritdoc} - */ - isString() { - return true; - } - - /** - * {@inheritdoc} - */ - getMaxBytes() { - return 100; - } -} diff --git a/src/type/int-enum-type.js b/src/type/int-enum-type.js deleted file mode 100644 index 8c54915..0000000 --- a/src/type/int-enum-type.js +++ /dev/null @@ -1,99 +0,0 @@ -'use strict'; - -import ArrayUtils from 'gdbots/common/util/array-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import Type from 'gdbots/pbj/type/type'; -import DecodeValueFailed from 'gdbots/pbj/exception/decode-value-failed'; - -export default class IntEnumType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if (!value.hasTrait('Enum')) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "Enum" but is not.'); - } - - if (field.hasInstance() - && !( - field.getInstance().name === SystemUtils.getClass(value) - || value.hasTrait(field.getInstance().name) - ) - ) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "' + field.getInstance().name + '" but is not.'); - } - - if (value === +value && isFinite(value) && !(value % 1)) { - throw new Error('Value "' + value + '" is not a integer.') - } - - if (value.getValue() < this.getMin() || value.getValue() > this.getMax()) { - throw new Error('Number "' + value.getValue() + '" was expected to be at least "' + value.getMin() + '" and at most "' + value.getMax() + '".') - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - if (value.hasTrait('Enum')) { - return parseInt(value.getValue()); - } - - return 0; - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - if (null === value) { - return null; - } - - /** @var Enum instance */ - let instance = field.getInstance(); - let enumValue = null; - - ArrayUtils.each(instance.enumValues, function(item) { - if (value === parseInt(item.getValue())) { - enumValue = item; - } - }); - - if (null === enumValue) { - throw new DecodeValueFailed(value, field, e); - } - - return enumValue; - } - - /** - * {@inheritdoc} - */ - isScalar() { - return false; - } - - /** - * {@inheritdoc} - */ - isNumeric() { - return true; - } - - /** - * {@inheritdoc} - */ - getMin() { - return 0; - } - - /** - * {@inheritdoc} - */ - getMax() { - return 65535; - } -} diff --git a/src/type/int-type.js b/src/type/int-type.js deleted file mode 100644 index 8c4fe59..0000000 --- a/src/type/int-type.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import AbstractIntType from 'gdbots/pbj/type/abstract-int-type'; - -export default class IntType extends SystemUtils.mixinClass(AbstractIntType) -{ - /** - * {@inheritdoc} - */ - getMin() { - return 0; - } - - /** - * {@inheritdoc} - */ - getMax() { - return 4294967295; - } -} diff --git a/src/type/medium-blob-type.js b/src/type/medium-blob-type.js deleted file mode 100644 index 274a14f..0000000 --- a/src/type/medium-blob-type.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import AbstractBinaryType from 'gdbots/pbj/type/abstract-binary-type'; - -export default class MediumBlobType extends SystemUtils.mixinClass(AbstractBinaryType) -{ - /** - * {@inheritdoc} - */ - getMaxBytes() { - return 16777215; - } - - /** - * {@inheritdoc} - */ - allowedInSet() { - return false; - } -} diff --git a/src/type/medium-int-type.js b/src/type/medium-int-type.js deleted file mode 100644 index 386db7c..0000000 --- a/src/type/medium-int-type.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import AbstractIntType from 'gdbots/pbj/type/abstract-int-type'; - -export default class MediumIntType extends SystemUtils.mixinClass(AbstractIntType) -{ - /** - * {@inheritdoc} - */ - getMin() { - return 0; - } - - /** - * {@inheritdoc} - */ - getMax() { - return 16777215; - } -} diff --git a/src/type/medium-text-type.js b/src/type/medium-text-type.js deleted file mode 100644 index a411b11..0000000 --- a/src/type/medium-text-type.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import AbstractStringType from 'gdbots/pbj/type/abstract-string-type'; - -export default class MediumTextType extends SystemUtils.mixinClass(AbstractStringType) -{ - /** - * {@inheritdoc} - */ - getMaxBytes() { - return 16777215; - } - - /** - * {@inheritdoc} - */ - allowedInSet() { - return false; - } -} diff --git a/src/type/message-ref-type.js b/src/type/message-ref-type.js deleted file mode 100644 index 9fba813..0000000 --- a/src/type/message-ref-type.js +++ /dev/null @@ -1,46 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import EncodeValueFailed from 'gdbots/pbj/exception/encode-value-failed'; -import DecodeValueFailed from 'gdbots/pbj/exception/decode-value-failed'; -import Type from 'gdbots/pbj/type/type'; - -export default class MessageRefType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if ('MessageRef' !== SystemUtils.getClass(value)) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "MessageRef" but is not.'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - return codec.encodeMessageRef(value, field); - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - return codec.decodeMessageRef(value, field); - } - - /** - * {@inheritdoc} - */ - isScalar() { - return false; - } - - /** - * {@inheritdoc} - */ - encodesToScalar() { - return false; - } -} diff --git a/src/type/message-type.js b/src/type/message-type.js deleted file mode 100644 index 22452d9..0000000 --- a/src/type/message-type.js +++ /dev/null @@ -1,94 +0,0 @@ -'use strict'; - -import ArrayUtils from 'gdbots/common/util/array-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import EncodeValueFailed from 'gdbots/pbj/exception/encode-value-failed'; -import DecodeValueFailed from 'gdbots/pbj/exception/decode-value-failed'; -import Type from 'gdbots/pbj/type/type'; - -export default class MessageType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if (!value.hasTrait('Message')) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "Message" but is not.'); - } - - if (field.hasInstance() - && !( - field.getInstance().name === SystemUtils.getClass(value) - || value.hasTrait(field.getInstance().name) - ) - ) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "' + field.getInstance().name + '" but is not.'); - } - - if (!field.getAnyOfInstances()) { - return; - } - - let instances = field.getAnyOfInstances(); - if (!instances || instances.length === 0) { - // means it can be "any message" - return; - } - - let found = false; - let classNames = []; - ArrayUtils.each(instances, function(instance) { - classNames.push(instance.name); - - if (value.hasTrait(instance.name) || instance.name === SystemUtils.getClass(value)) { - found = true; - } - }); - - if (!found) { - throw new Error('Field [' + field.getName() + '] must be an instance of at least one of: ' + classNames.join(',') + '.'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - return codec.encodeMessage(value, field); - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - return codec.decodeMessage(value, field); - } - - /** - * {@inheritdoc} - */ - isScalar() { - return false; - } - - /** - * {@inheritdoc} - */ - encodesToScalar() { - return false; - } - - /** - * {@inheritdoc} - */ - isMessage() { - return true; - } - - /** - * {@inheritdoc} - */ - allowedInSet() { - return false; - } -} diff --git a/src/type/microtime-type.js b/src/type/microtime-type.js deleted file mode 100644 index 204f364..0000000 --- a/src/type/microtime-type.js +++ /dev/null @@ -1,64 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import Microtime from 'gdbots/pbj/well-known/microtime'; -import Type from 'gdbots/pbj/type/type'; - -export default class MicrotimeType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if ('Microtime' !== SystemUtils.getClass(value)) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "Microtime" but is not.'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - if ('Microtime' === SystemUtils.getClass(value)) { - return value.toString(); - } - - return null; - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - if (!value || value.length === 0) { - return null; - } - - if ('Microtime' === SystemUtils.getClass(value)) { - return value; - } - - return Microtime.fromString(value); - } - - /** - * {@inheritdoc} - */ - isScalar() { - return false; - } - - /** - * {@inheritdoc} - */ - getDefault() { - return Microtime.create(); - } - - /** - * {@inheritdoc} - */ - isNumeric() { - return true; - } -} diff --git a/src/type/signed-big-int-type.js b/src/type/signed-big-int-type.js deleted file mode 100644 index f48ef1f..0000000 --- a/src/type/signed-big-int-type.js +++ /dev/null @@ -1,68 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import BigNumber from 'gdbots/pbj/well-known/big-number'; -import Type from 'gdbots/pbj/type/type'; - -export default class SignedBigIntType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if ('BigNumber' !== SystemUtils.getClass(value)) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "BigNumber" but is not.'); - } - - if (!value.greaterThanOrEqualTo('-9223372036854775808')) { - throw new Error('Field [' + field.getName() + '] cannot be less than [-9223372036854775808].'); - } - - if (!value.lessThanOrEqualTo('9223372036854775807')) { - throw new Error('Field [' + field.getName() + '] cannot be greater than [9223372036854775807].'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - if ('BigNumber' === SystemUtils.getClass(value)) { - return value.toString(); - } - - return '0'; - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - if (null === value || 'BigNumber' === SystemUtils.getClass(value)) { - return value; - } - - return new BigNumber(value); - } - - /** - * {@inheritdoc} - */ - isScalar() { - return false; - } - - /** - * {@inheritdoc} - */ - getDefault() { - return new BigNumber(0); - } - - /** - * {@inheritdoc} - */ - isNumeric() { - return true; - } -} diff --git a/src/type/signed-int-type.js b/src/type/signed-int-type.js deleted file mode 100644 index 4b3059b..0000000 --- a/src/type/signed-int-type.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import AbstractIntType from 'gdbots/pbj/type/abstract-int-type'; - -export default class SignedIntType extends SystemUtils.mixinClass(AbstractIntType) -{ - /** - * {@inheritdoc} - */ - getMin() { - return -2147483648; - } - - /** - * {@inheritdoc} - */ - getMax() { - return 2147483647; - } -} diff --git a/src/type/signed-medium-int-type.js b/src/type/signed-medium-int-type.js deleted file mode 100644 index 392bd02..0000000 --- a/src/type/signed-medium-int-type.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import AbstractIntType from 'gdbots/pbj/type/abstract-int-type'; - -export default class SignedMediumIntType extends SystemUtils.mixinClass(AbstractIntType) -{ - /** - * {@inheritdoc} - */ - getMin() { - return -8388608; - } - - /** - * {@inheritdoc} - */ - getMax() { - return 8388607; - } -} diff --git a/src/type/signed-small-int-type.js b/src/type/signed-small-int-type.js deleted file mode 100644 index cfd96f4..0000000 --- a/src/type/signed-small-int-type.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import AbstractIntType from 'gdbots/pbj/type/abstract-int-type'; - -export default class SignedSmallIntType extends SystemUtils.mixinClass(AbstractIntType) -{ - /** - * {@inheritdoc} - */ - getMin() { - return -32768; - } - - /** - * {@inheritdoc} - */ - getMax() { - return 32767; - } -} diff --git a/src/type/signed-tiny-int-type.js b/src/type/signed-tiny-int-type.js deleted file mode 100644 index 4d9c7e1..0000000 --- a/src/type/signed-tiny-int-type.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import AbstractIntType from 'gdbots/pbj/type/abstract-int-type'; - -export default class SignedTinyIntType extends SystemUtils.mixinClass(AbstractIntType) -{ - /** - * {@inheritdoc} - */ - getMin() { - return -128; - } - - /** - * {@inheritdoc} - */ - getMax() { - return 127; - } -} diff --git a/src/type/small-int-type.js b/src/type/small-int-type.js deleted file mode 100644 index 37d9167..0000000 --- a/src/type/small-int-type.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import AbstractIntType from 'gdbots/pbj/type/abstract-int-type'; - -export default class SmallIntType extends SystemUtils.mixinClass(AbstractIntType) -{ - /** - * {@inheritdoc} - */ - getMin() { - return 0; - } - - /** - * {@inheritdoc} - */ - getMax() { - return 65535; - } -} diff --git a/src/type/string-enum-type.js b/src/type/string-enum-type.js deleted file mode 100644 index cff94a6..0000000 --- a/src/type/string-enum-type.js +++ /dev/null @@ -1,97 +0,0 @@ -'use strict'; - -import ArrayUtils from 'gdbots/common/util/array-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import Type from 'gdbots/pbj/type/type'; -import DecodeValueFailed from 'gdbots/pbj/exception/decode-value-failed'; - -export default class StringEnumType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if (!value.hasTrait('Enum')) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "Enum" but is not.'); - } - - if (field.hasInstance() - && !( - field.getInstance().name === SystemUtils.getClass(value) - || value.hasTrait(field.getInstance().name) - ) - ) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "' + field.getInstance().name + '" but is not.'); - } - - if ('string' !== typeof value.getValue()) { - throw new Error('Value "' + value.getValue() + '" expected to be string.') - } - - // intentionally using strlen to get byte length, not mb_strlen - let length = value.toString().length; - let maxBytes = this.getMaxBytes(); - let okay = length > 0 && length <= maxBytes; - - if (!okay) { - throw new Error('Field [' + field.getName() + '] must be between [1] and [' + maxBytes + '] bytes, [' + length + '] bytes given.'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - if (value.hasTrait('Enum')) { - return String(value.getValue()); - } - - return null; - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - if (null === value) { - return null; - } - - /** @var Enum instance */ - let instance = field.getInstance(); - let enumValue = null; - - ArrayUtils.each(instance.enumValues, function(item) { - if (value === String(item.getValue())) { - enumValue = item; - } - }); - - if (null === enumValue) { - throw new DecodeValueFailed(value, field, e); - } - - return enumValue; - } - - /** - * {@inheritdoc} - */ - isScalar() { - return false; - } - - /** - * {@inheritdoc} - */ - isString() { - return true; - } - - /** - * {@inheritdoc} - */ - getMaxBytes() { - return 100; - } -} diff --git a/src/type/string-type.js b/src/type/string-type.js deleted file mode 100644 index ff0b139..0000000 --- a/src/type/string-type.js +++ /dev/null @@ -1,102 +0,0 @@ -'use strict'; - -import {default as DateUtils, ISO8601_ZULU, ISO8601} from 'gdbots/common/util/date-utils'; -import HashtagUtils from 'gdbots/common/util/hashtag-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import AbstractStringType from 'gdbots/pbj/type/abstract-string-type'; -import Format from 'gdbots/pbj/enum/format'; - -export default class StringType extends SystemUtils.mixinClass(AbstractStringType) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - super.guard(value, field); - - let pattern = field.getPattern(); - if (pattern && !new RegExp(pattern).test(value)) { - throw new Error('Value [' + value + '] is invalid. It must match the pattern [' + pattern + '].'); - } - - switch (field.getFormat()) { - case Format.UNKNOWN: - break; - - case Format.DATE: - if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) { - throw new Error('Field [' + field.getName() + '] must be a valid [' + field.getFormat().getValue() + '].'); - } - - break; - - case Format.DATE_TIME: - if (!DateUtils.isValidISO8601Date(value)) { - throw new Error('Field [' + field.getName() + '] must be a valid ISO8601 date-time. Format must match one of [' + ISO8601_ZULU + '], [' + ISO8601 + '] or [' + new Date().toISOString() + '].'); - } - - break; - - case Format.SLUG: - if (!/^([\w\/-]|[\w-][\w\/-]*[\w-])$/.test(value)) { - throw new Error('Field [' + field.getName() + '] must be a valid [' + field.getFormat().getValue() + '].'); - } - - break; - - case Format.EMAIL: - if (!/^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/.test(value)) { - throw new Error('Field [' + field.getName() + '] must be a valid [' + field.getFormat().getValue() + '].'); - } - - break; - - case Format.HASHTAG: - if (!HashtagUtils.isValid(value)) { - throw new Error('Field [' + field.getName() + '] must be a valid hashtag. @see HashtagUtils.isValid.'); - } - - break; - - case Format.IPV4: - if (!/^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$/.test(value)) { - throw new Error('Field [' + field.getName() + '] must be a valid [' + field.getFormat().getValue() + '].'); - } - - break; - - case Format.IPV6: - if (!/^((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7}$/.test(value)) { - throw new Error('Field [' + field.getName() + '] must be a valid [' + field.getFormat().getValue() + '].'); - } - - break; - - case Format.HOSTNAME: - case Format.URI: - case Format.URL: - if (!/(https?:\/\/(?:www\.|(?!www))[^\s\.]+\.[^\s]{2,}|www\.[^\s]+\.[^\s]{2,})/.test(value)) { - throw new Error('Field [' + field.getName() + '] must be a valid [' + field.getFormat().getValue() + '].'); - } - - break; - - case Format.UUID: - if (!/[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/.test(value)) { - throw new Error('Field [' + field.getName() + '] must be a valid [' + field.getFormat().getValue() + '].'); - } - - break; - - default: - break; - } - } - - /** - * {@inheritdoc} - */ - getMaxBytes() { - return 255; - } -} diff --git a/src/type/text-type.js b/src/type/text-type.js deleted file mode 100644 index 05dfd87..0000000 --- a/src/type/text-type.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import AbstractStringType from 'gdbots/pbj/type/abstract-string-type'; - -export default class TextType extends SystemUtils.mixinClass(AbstractStringType) -{ - /** - * {@inheritdoc} - */ - allowedInSet() { - return false; - } -} diff --git a/src/type/time-uuid-type.js b/src/type/time-uuid-type.js deleted file mode 100644 index 178005a..0000000 --- a/src/type/time-uuid-type.js +++ /dev/null @@ -1,80 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import TimeUuidIdentifier from 'gdbots/pbj/well-known/time-uuid-identifier'; -import Type from 'gdbots/pbj/type/type'; - -export default class TimeUuidType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if (!value.hasTrait('TimeUuidIdentifier') && 'TimeUuidIdentifier' !== SystemUtils.getClass(value)) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "TimeUuidIdentifier" but is not.'); - } - - if (field.hasInstance() - && !( - field.getInstance().name === SystemUtils.getClass(value) - || value.hasTrait(field.getInstance().name) - ) - ) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "' + field.getInstance().name + '" but is not.'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - if (value.hasTrait('TimeUuidIdentifier') || 'TimeUuidIdentifier' === SystemUtils.getClass(value)) { - return value.toString(); - } - - return null; - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - if (!value || value.length === 0) { - return null; - } - - /** @var TimeUuidIdentifier instance */ - let instance = field.getInstance() || TimeUuidIdentifier; - if ('object' === typeof value - && ( - value.hasTrait(instance.name) - || instance.name === SystemUtils.getClass(value) - ) - ) { - return value; - } - - return instance.fromString(value); - } - - /** - * {@inheritdoc} - */ - isScalar() { - return false; - } - - /** - * {@inheritdoc} - */ - getDefault() { - return TimeUuidIdentifier.generate(); - } - - /** - * {@inheritdoc} - */ - isString() { - return true; - } -} diff --git a/src/type/timestamp-type.js b/src/type/timestamp-type.js deleted file mode 100644 index 411c9a0..0000000 --- a/src/type/timestamp-type.js +++ /dev/null @@ -1,49 +0,0 @@ -'use strict'; - -import DateUtils from 'gdbots/common/util/date-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import Type from 'gdbots/pbj/type/type'; - -export default class TimestampType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if ('number' !== typeof value) { - throw new Error('Value must be a number.'); - } - - if (!DateUtils.isValidTimestamp(value)) { - throw new Error('Field [' + field.getName() + '] value [' + value + '] is not a valid unix timestamp.'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - return parseInt(value); - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - return parseInt(value); - } - - /** - * {@inheritdoc} - */ - getDefault() { - return new Date().getTime(); - } - - /** - * {@inheritdoc} - */ - isNumeric() { - return true; - } -} diff --git a/src/type/tiny-int-type.js b/src/type/tiny-int-type.js deleted file mode 100644 index 22f3ec0..0000000 --- a/src/type/tiny-int-type.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import AbstractIntType from 'gdbots/pbj/type/abstract-int-type'; - -export default class TinyIntType extends SystemUtils.mixinClass(AbstractIntType) -{ - /** - * {@inheritdoc} - */ - getMin() { - return 0; - } - - /** - * {@inheritdoc} - */ - getMax() { - return 255; - } -} diff --git a/src/type/trinary-type.js b/src/type/trinary-type.js deleted file mode 100644 index 7a9c9fd..0000000 --- a/src/type/trinary-type.js +++ /dev/null @@ -1,73 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import AbstractIntType from 'gdbots/pbj/type/abstract-int-type'; - -/** - * @link https://en.wikipedia.org/wiki/Three-valued_logic - * 0 = unknown - * 1 = true - * 2 = false - */ -export default class TrinaryType extends SystemUtils.mixinClass(AbstractIntType) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if ([0, 1, 2].indexOf(value) === -1) { - throw new Error('Field [' + field.getName() + '] value [' + value + '] is not a valid. Must be 0, 1, or 2.'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - let tmp = parseInt(value); - return isNaN(tmp) || !isFinite(tmp) ? 0 : tmp; - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - let tmp = parseInt(value); - return isNaN(tmp) || !isFinite(tmp) ? 0 : tmp; - } - - /** - * {@inheritdoc} - */ - getDefault() { - return 0; - } - - /** - * {@inheritdoc} - */ - isNumeric() { - return true; - } - - /** - * {@inheritdoc} - */ - getMin() { - return 0; - } - - /** - * {@inheritdoc} - */ - getMax() { - return 2; - } - - /** - * {@inheritdoc} - */ - allowedInSet() { - return false; - } -} diff --git a/src/type/type.js b/src/type/type.js deleted file mode 100644 index 567921e..0000000 --- a/src/type/type.js +++ /dev/null @@ -1,185 +0,0 @@ -'use strict'; - -import StringUtils from 'gdbots/common/util/string-utils'; -import TypeName from 'gdbots/pbj/enum/type-name'; - -/** @var array */ -let _instances = {}; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class Type -{ - /** - * Private constructor to ensure flyweight construction. - * - * @param TypeName typeName - */ - constructor(typeName) { - privateProps.set(this, { - /** @var TypeName */ - typeName: typeName - }); - } - - /** - * @return Type - */ - static create() { - let type = this.name; - if (undefined === _instances[type]) { - _instances[type] = new this(TypeName[StringUtils.toSnakeCase(type.replace('Type', '')).toUpperCase()]); - } - - return _instances[type]; - } - - /** - * @return TypeName - */ - getTypeName() { - return privateProps.get(this).typeName; - } - - /** - * Shortcut to returning the value of the TypeName - * - * @return string - */ - getTypeValue() { - return privateProps.get(this).typeName.value; - } - - /** - * @param mixed value - * @param Field field - * - * @throws \Exception - */ - guard(value, field) {} - - /** - * @param mixed value - * @param Field field - * @param Codec codec - * - * @return mixed - * - * @throws GdbotsPbjException - * @throws EncodeValueFailed - */ - encode(value, field, codec = null) {} - - /** - * @param mixed value - * @param Field field - * @param Codec codec - * - * @return mixed - * - * @throws GdbotsPbjException - * @throws DecodeValueFailed - */ - decode(value, field, codec = null) {} - - /** - * Returns true if the value gets decoded and stored during runtime as a scalar value. - * - * @return bool - */ - isScalar() { - return true; - } - - /** - * Returns true if the value gets encoded to a scalar value. This is important to - * know because a big int, date, enum, etc. is stored as an object on the message - * but when the message is encoded to an array, json, etc. it's a scalar value. - * - * @return bool - */ - encodesToScalar() { - return true; - } - - /** - * @return mixed - */ - getDefault() { - return null; - } - - /** - * @return bool - */ - isBoolean() { - return false; - } - - /** - * @return bool - */ - isBinary() { - return false; - } - - /** - * @return bool - */ - isNumeric() { - return false; - } - - /** - * @return bool - */ - isString() { - return false; - } - - /** - * @return bool - */ - isMessage() { - return false; - } - - /** - * Returns the minimum value supported by an integer type. - * - * @return int - */ - getMin() { - return -2147483648; - } - - /** - * Returns the maximum value supported by an integer type. - * - * @return int - */ - getMax() { - return 2147483647; - } - - /** - * Returns the maximum number of bytes supported by the string or binary type. - * - * @return int - */ - getMaxBytes() { - return 65535; - } - - /** - * @return bool - */ - allowedInSet() { - return true; - } -} diff --git a/src/type/uuid-type.js b/src/type/uuid-type.js deleted file mode 100644 index a45014a..0000000 --- a/src/type/uuid-type.js +++ /dev/null @@ -1,80 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import UuidIdentifier from 'gdbots/pbj/well-known/uuid-identifier'; -import Type from 'gdbots/pbj/type/type'; - -export default class UuidType extends SystemUtils.mixinClass(Type) -{ - /** - * {@inheritdoc} - */ - guard(value, field) { - if (!value.hasTrait('UuidIdentifier') && 'UuidIdentifier' !== SystemUtils.getClass(value)) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "UuidIdentifier" but is not.'); - } - - if (field.hasInstance() - && !( - field.getInstance().name === SystemUtils.getClass(value) - || value.hasTrait(field.getInstance().name) - ) - ) { - throw new Error('Class "' + value.name + '" was expected to be instanceof of "' + field.getInstance().name + '" but is not.'); - } - } - - /** - * {@inheritdoc} - */ - encode(value, field, codec = null) { - if (value.hasTrait('UuidIdentifier') || 'UuidIdentifier' === SystemUtils.getClass(value)) { - return value.toString(); - } - - return null; - } - - /** - * {@inheritdoc} - */ - decode(value, field, codec = null) { - if (!value || value.length === 0) { - return null; - } - - /** @var UuidIdentifier instance */ - let instance = field.getInstance() || UuidIdentifier; - if ('object' === typeof value - && ( - value.hasTrait(instance.name) - || instance.name === SystemUtils.getClass(value) - ) - ) { - return value; - } - - return instance.fromString(value); - } - - /** - * {@inheritdoc} - */ - isScalar() { - return false; - } - - /** - * {@inheritdoc} - */ - getDefault() { - return UuidIdentifier.generate(); - } - - /** - * {@inheritdoc} - */ - isString() { - return true; - } -} diff --git a/src/types/AbstractBinaryType.js b/src/types/AbstractBinaryType.js new file mode 100644 index 0000000..f42b8e6 --- /dev/null +++ b/src/types/AbstractBinaryType.js @@ -0,0 +1,111 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import base64 from 'base-64'; +import clamp from 'lodash/clamp'; +import isString from 'lodash/isString'; +import trim from 'lodash/trim'; +import utf8 from 'utf8'; +import Type from './Type'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import DecodeValueFailed from '../exceptions/DecodeValueFailed'; + +let useDecodeFromBase64 = true; +let useEncodeToBase64 = true; + +export default class AbstractBinaryType extends Type { + /** + * @param {boolean} [useBase64] + */ + decodeFromBase64(useBase64 = true) { + useDecodeFromBase64 = useBase64; + } + + /** + * @param {boolean} [useBase64] + */ + encodeToBase64(useBase64 = true) { + useEncodeToBase64 = useBase64; + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!isString(value)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] is not a string.`); + } + + // fixme: deal with browsers not having "Buffer" available + // we must get BYTES, not characters ಠ_ಠ + const strLength = Buffer.from(useEncodeToBase64 ? this.encode(value, field) : value).byteLength; + const minLength = field.getMinLength(); + const maxLength = clamp(field.getMaxLength(), minLength, this.getMaxBytes()); + + if (strLength >= minLength && strLength <= maxLength) { + return; + } + + throw new AssertionFailed( + `Field [${field.getName()}] :: Must be between [${minLength}] and [${maxLength}] bytes, [${strLength}] bytes given.`, + ); + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?string} + */ + encode(value, field, codec = null) { + const trimmed = trim(value); + if (trimmed === '') { + return null; + } + + return useEncodeToBase64 ? base64.encode(utf8.encode(trimmed)) : trimmed; + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?string} + * + * @throws {DecodeValueFailed} + */ + decode(value, field, codec = null) { + const trimmed = trim(value); + if (trimmed === '') { + return null; + } + + if (!useDecodeFromBase64) { + return trimmed; + } + + try { + return utf8.decode(base64.decode(trimmed)); + } catch (e) { + throw new DecodeValueFailed(value, field, `${e.name}::${e.message}`); + } + } + + /** + * @returns {boolean} + */ + isBinary() { + return true; + } + + /** + * @returns {boolean} + */ + isString() { + return true; + } +} diff --git a/src/types/AbstractIntType.js b/src/types/AbstractIntType.js new file mode 100644 index 0000000..31821a4 --- /dev/null +++ b/src/types/AbstractIntType.js @@ -0,0 +1,64 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import clamp from 'lodash/clamp'; +import isSafeInteger from 'lodash/isSafeInteger'; +import toSafeInteger from 'lodash/toSafeInteger'; +import Type from './Type'; +import AssertionFailed from '../exceptions/AssertionFailed'; + +export default class AbstractIntType extends Type { + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!isSafeInteger(value)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] is not an integer.`); + } + + const min = clamp(field.getMin(), this.getMin(), this.getMax()); + const max = clamp(field.getMax(), this.getMin(), this.getMax()); + + if (value < min || value > max) { + throw new AssertionFailed(`Field [${field.getName()}] :: Number "${value}" was expected to be at least "${min}" and at most "${max}".`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {number} + */ + encode(value, field, codec = null) { + return toSafeInteger(value); + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {number} + */ + decode(value, field, codec = null) { + return toSafeInteger(value); + } + + /** + * @returns {number} + */ + getDefault() { + return 0; + } + + /** + * @returns {boolean} + */ + isNumeric() { + return true; + } +} diff --git a/src/types/AbstractStringType.js b/src/types/AbstractStringType.js new file mode 100644 index 0000000..336d08e --- /dev/null +++ b/src/types/AbstractStringType.js @@ -0,0 +1,66 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import clamp from 'lodash/clamp'; +import isString from 'lodash/isString'; +import trim from 'lodash/trim'; +import Type from './Type'; +import AssertionFailed from '../exceptions/AssertionFailed'; + +export default class AbstractStringType extends Type { + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!isString(value)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] is not a string.`); + } + + // fixme: deal with browsers not having "Buffer" available + // we must get BYTES, not characters ಠ_ಠ + const strLength = Buffer.from(value).byteLength; + const minLength = field.getMinLength(); + const maxLength = clamp(field.getMaxLength(), minLength, this.getMaxBytes()); + + if (strLength >= minLength && strLength <= maxLength) { + return; + } + + throw new AssertionFailed( + `Field [${field.getName()}] :: Must be between [${minLength}] and [${maxLength}] bytes, [${strLength}] bytes given.`, + ); + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?string} + */ + encode(value, field, codec = null) { + const trimmed = trim(value); + return trimmed === '' ? null : trimmed; + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?string} + */ + decode(value, field, codec = null) { + const trimmed = trim(value); + return trimmed === '' ? null : trimmed; + } + + /** + * @returns {boolean} + */ + isString() { + return true; + } +} diff --git a/src/types/BigIntType.js b/src/types/BigIntType.js new file mode 100644 index 0000000..8a3d07d --- /dev/null +++ b/src/types/BigIntType.js @@ -0,0 +1,83 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import BigNumber from '../well-known/BigNumber'; +import AssertionFailed from '../exceptions/AssertionFailed'; + +export default class BigIntType extends Type { + constructor() { + super(TypeName.BIG_INT); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!(value instanceof BigNumber)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] was expected to be a BigNumber.`); + } + + if (value.isNegative()) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value "${value}" cannot be negative.`); + } + + if (!value.lessThanOrEqualTo('18446744073709551615')) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value "${value}" cannot be greater than [18446744073709551615].`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {string} + */ + encode(value, field, codec = null) { + if (value instanceof BigNumber) { + return `${value.toFixed(0)}`; + } + + return '0'; + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?BigNumber} + */ + decode(value, field, codec = null) { + if (value === null || value instanceof BigNumber) { + return value; + } + + return new BigNumber(value); + } + + /** + * @returns {boolean} + */ + isScalar() { + return false; + } + + /** + * @returns {BigNumber} + */ + getDefault() { + return new BigNumber(0); + } + + /** + * @returns {boolean} + */ + isNumeric() { + return true; + } +} diff --git a/src/types/BinaryType.js b/src/types/BinaryType.js new file mode 100644 index 0000000..1a1fb04 --- /dev/null +++ b/src/types/BinaryType.js @@ -0,0 +1,17 @@ +/* eslint-disable class-methods-use-this */ + +import AbstractBinaryType from './AbstractBinaryType'; +import TypeName from '../enums/TypeName'; + +export default class BinaryType extends AbstractBinaryType { + constructor() { + super(TypeName.BINARY); + } + + /** + * @returns {number} + */ + getMaxBytes() { + return 255; + } +} diff --git a/src/types/BlobType.js b/src/types/BlobType.js new file mode 100644 index 0000000..0a3c371 --- /dev/null +++ b/src/types/BlobType.js @@ -0,0 +1,17 @@ +/* eslint-disable class-methods-use-this */ + +import AbstractBinaryType from './AbstractBinaryType'; +import TypeName from '../enums/TypeName'; + +export default class BlobType extends AbstractBinaryType { + constructor() { + super(TypeName.BLOB); + } + + /** + * @returns {boolean} + */ + allowedInSet() { + return false; + } +} diff --git a/src/types/BooleanType.js b/src/types/BooleanType.js new file mode 100644 index 0000000..eef4ccd --- /dev/null +++ b/src/types/BooleanType.js @@ -0,0 +1,75 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import isBoolean from 'lodash/isBoolean'; +import toLower from 'lodash/toLower'; +import trim from 'lodash/trim'; +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import AssertionFailed from '../exceptions/AssertionFailed'; + +export default class BooleanType extends Type { + constructor() { + super(TypeName.BOOLEAN); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (isBoolean(value)) { + return; + } + + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] is not a boolean.`); + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {boolean} + */ + encode(value, field, codec = null) { + return !!value; + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {boolean} + */ + decode(value, field, codec = null) { + if (isBoolean(value)) { + return !!value; + } + + return ['1', 'true', 'yes', 'on', '+'].indexOf(trim(toLower(value))) !== -1; + } + + /** + * @returns {boolean} + */ + getDefault() { + return false; + } + + /** + * @returns {boolean} + */ + isBoolean() { + return true; + } + + /** + * @returns {boolean} + */ + allowedInSet() { + return false; + } +} diff --git a/src/types/DateTimeType.js b/src/types/DateTimeType.js new file mode 100644 index 0000000..a35021a --- /dev/null +++ b/src/types/DateTimeType.js @@ -0,0 +1,88 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import moment from 'moment'; +import isValidISO8601Date from '@gdbots/common/isValidISO8601Date'; +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import DecodeValueFailed from '../exceptions/DecodeValueFailed'; + +export default class DateTimeType extends Type { + constructor() { + super(TypeName.DATE_TIME); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!(value instanceof Date)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] was expected to be a Date.`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?string} + */ + encode(value, field, codec = null) { + if (value instanceof Date) { + return value.toISOString(); + } + + return null; + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?Date} + */ + decode(value, field, codec = null) { + if (value === null) { + return null; + } + + if (!(value instanceof Date) && !isValidISO8601Date(value)) { + throw new AssertionFailed( + `Field [${field.getName()}] :: Value "${value}" is not a valid ISO8601 date/time. E.g. "2017-05-25T02:54:18Z".`, + ); + } + + const m = moment(value); + if (m.isValid()) { + return m.utc().toDate(); + } + + throw new DecodeValueFailed(value, field, `Field [${field.getName()}] :: Value "${value}" is not a valid IS8601 date.`); + } + + /** + * @returns {boolean} + */ + isScalar() { + return false; + } + + /** + * @returns {boolean} + */ + isString() { + return true; + } + + /** + * @returns {boolean} + */ + allowedInSet() { + return false; + } +} diff --git a/src/types/DateType.js b/src/types/DateType.js new file mode 100644 index 0000000..31dd6eb --- /dev/null +++ b/src/types/DateType.js @@ -0,0 +1,92 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import DecodeValueFailed from '../exceptions/DecodeValueFailed'; + +export default class DateType extends Type { + constructor() { + super(TypeName.DATE); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!(value instanceof Date)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] was expected to be a Date.`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?string} + */ + encode(value, field, codec = null) { + if (value instanceof Date) { + return value.toISOString().substr(0, 10); + } + + return null; + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?Date} + */ + decode(value, field, codec = null) { + if (value === null) { + return null; + } + + let date; + if (value instanceof Date) { + // ensures no time component and utc + date = value.toISOString().substr(0, 10); + } else { + date = value.substr(0, 10); + } + + if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) { + throw new DecodeValueFailed(value, field, `Field [${field.getName()}] :: Value "${value}" is not a valid date with format "YYYY-MM-DD".`); + } + + try { + const [year, month, day] = date.split('-').map(Number); + return new Date(Date.UTC(year, month - 1, day)); + } catch (e) { + throw new DecodeValueFailed(value, field, e.message); + } + } + + /** + * @returns {boolean} + */ + isScalar() { + return false; + } + + /** + * @returns {boolean} + */ + isString() { + return true; + } + + /** + * @returns {boolean} + */ + allowedInSet() { + return false; + } +} diff --git a/src/types/DecimalType.js b/src/types/DecimalType.js new file mode 100644 index 0000000..497a973 --- /dev/null +++ b/src/types/DecimalType.js @@ -0,0 +1,76 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import isFinite from 'lodash/isFinite'; +import toFinite from 'lodash/toFinite'; +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import AssertionFailed from '../exceptions/AssertionFailed'; + +// fixme: handle precision +export default class DecimalType extends Type { + constructor() { + super(TypeName.DECIMAL); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!isFinite(value)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] is not a decimal.`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {string} + */ + encode(value, field, codec = null) { + return toFinite(value).toFixed(field.getScale()); + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {number} + */ + decode(value, field, codec = null) { + return toFinite(toFinite(value).toFixed(field.getScale())); + } + + /** + * @returns {number} + */ + getDefault() { + return 0.0; + } + + /** + * @returns {boolean} + */ + isNumeric() { + return true; + } + + /** + * @returns {number} + */ + getMin() { + return Number.MIN_VALUE; + } + + /** + * @returns {number} + */ + getMax() { + return Number.MAX_VALUE; + } +} diff --git a/src/types/DynamicFieldType.js b/src/types/DynamicFieldType.js new file mode 100644 index 0000000..a71c789 --- /dev/null +++ b/src/types/DynamicFieldType.js @@ -0,0 +1,80 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import DynamicField from '../well-known/DynamicField'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import DecodeValueFailed from '../exceptions/DecodeValueFailed'; + +export default class DynamicFieldType extends Type { + constructor() { + super(TypeName.DYNAMIC_FIELD); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!(value instanceof DynamicField)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] was expected to be a DynamicField.`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {*} + */ + encode(value, field, codec = null) { + if (value instanceof DynamicField) { + return codec.encodeDynamicField(value, field); + } + + return null; + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?DynamicField} + */ + decode(value, field, codec = null) { + if (value === null || value instanceof DynamicField) { + return value; + } + + try { + return codec.decodeDynamicField(value, field); + } catch (e) { + throw new DecodeValueFailed(value, field, e.message); + } + } + + /** + * @returns {boolean} + */ + isScalar() { + return false; + } + + /** + * @returns {boolean} + */ + encodesToScalar() { + return false; + } + + /** + * @returns {boolean} + */ + allowedInSet() { + return false; + } +} diff --git a/src/types/FloatType.js b/src/types/FloatType.js new file mode 100644 index 0000000..0693de3 --- /dev/null +++ b/src/types/FloatType.js @@ -0,0 +1,76 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import isFinite from 'lodash/isFinite'; +import toFinite from 'lodash/toFinite'; +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import AssertionFailed from '../exceptions/AssertionFailed'; + +// fixme: handle precision and scale +export default class FloatType extends Type { + constructor() { + super(TypeName.FLOAT); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!isFinite(value)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] is not a float.`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {number} + */ + encode(value, field, codec = null) { + return toFinite(value); + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {number} + */ + decode(value, field, codec = null) { + return toFinite(value); + } + + /** + * @returns {number} + */ + getDefault() { + return 0.0; + } + + /** + * @returns {boolean} + */ + isNumeric() { + return true; + } + + /** + * @returns {number} + */ + getMin() { + return Number.MIN_VALUE; + } + + /** + * @returns {number} + */ + getMax() { + return Number.MAX_VALUE; + } +} diff --git a/src/types/GeoPointType.js b/src/types/GeoPointType.js new file mode 100644 index 0000000..e90e1b7 --- /dev/null +++ b/src/types/GeoPointType.js @@ -0,0 +1,80 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import GeoPoint from '../well-known/GeoPoint'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import DecodeValueFailed from '../exceptions/DecodeValueFailed'; + +export default class GeoPointType extends Type { + constructor() { + super(TypeName.GEO_POINT); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!(value instanceof GeoPoint)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] was expected to be a GeoPoint.`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {*} + */ + encode(value, field, codec = null) { + if (value instanceof GeoPoint) { + return codec.encodeGeoPoint(value, field); + } + + return null; + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?GeoPoint} + */ + decode(value, field, codec = null) { + if (value === null || value instanceof GeoPoint) { + return value; + } + + try { + return codec.decodeGeoPoint(value, field); + } catch (e) { + throw new DecodeValueFailed(value, field, e.message); + } + } + + /** + * @returns {boolean} + */ + isScalar() { + return false; + } + + /** + * @returns {boolean} + */ + encodesToScalar() { + return false; + } + + /** + * @returns {boolean} + */ + allowedInSet() { + return false; + } +} diff --git a/src/types/IdentifierType.js b/src/types/IdentifierType.js new file mode 100644 index 0000000..8d29142 --- /dev/null +++ b/src/types/IdentifierType.js @@ -0,0 +1,90 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import Identifier from '../well-known/Identifier'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import DecodeValueFailed from '../exceptions/DecodeValueFailed'; + +export default class IdentifierType extends Type { + constructor() { + super(TypeName.IDENTIFIER); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!(value instanceof Identifier)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] was expected to be an Identifier.`); + } + + if (!(value instanceof field.getClassProto())) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value "${value}" was expected to be a "${field.getClassProto().name}".`); + } + + const str = value.toString(); + if (str.length < 1 || str.length > this.getMaxBytes()) { + throw new AssertionFailed(`Field [${field.getName()}] :: Must be between [1] and [${this.getMaxBytes()}] bytes, [${str.length}] bytes given.`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?string} + */ + encode(value, field, codec = null) { + if (value instanceof Identifier) { + return value.toString(); + } + + return null; + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?Identifier} + */ + decode(value, field, codec = null) { + const expectedProto = field.getClassProto(); + if (value === null || value instanceof expectedProto) { + return value; + } + + try { + return expectedProto.fromString(`${value}`); + } catch (e) { + throw new DecodeValueFailed(value, field, e.message); + } + } + + /** + * @returns {boolean} + */ + isScalar() { + return false; + } + + /** + * @returns {boolean} + */ + isString() { + return true; + } + + /** + * @returns {number} + */ + getMaxBytes() { + return 100; + } +} diff --git a/src/types/IntEnumType.js b/src/types/IntEnumType.js new file mode 100644 index 0000000..9fd9371 --- /dev/null +++ b/src/types/IntEnumType.js @@ -0,0 +1,104 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import Enum from '@gdbots/common/Enum'; +import isSafeInteger from 'lodash/isSafeInteger'; +import toSafeInteger from 'lodash/toSafeInteger'; +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import DecodeValueFailed from '../exceptions/DecodeValueFailed'; + +export default class IntEnumType extends Type { + constructor() { + super(TypeName.INT_ENUM); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!(value instanceof Enum)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] was expected to be an Enum.`); + } + + if (!(value instanceof field.getClassProto())) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value "${value.getEnumId()}" was expected to be "${field.getClassProto().getEnumId()}".`); + } + + const enumValue = value.getValue(); + if (!isSafeInteger(enumValue)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Enum's value "${value}" is not an integer.`); + } + + if (enumValue < this.getMin() || enumValue > this.getMax()) { + throw new AssertionFailed(`Field [${field.getName()}] :: Enum's value "${enumValue}" was expected to be at least "${this.getMin()}" and at most "${this.getMax()}".`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {number} + */ + encode(value, field, codec = null) { + if (value instanceof Enum) { + return toSafeInteger(value.getValue()); + } + + return 0; + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?Enum} + * + * @throws {DecodeValueFailed} + */ + decode(value, field, codec = null) { + if (value === null) { + return null; + } + + try { + return field.getClassProto().create(value); + } catch (e) { + throw new DecodeValueFailed(value, field, e.message); + } + } + + /** + * @returns {boolean} + */ + isScalar() { + return false; + } + + /** + * @returns {boolean} + */ + isNumeric() { + return true; + } + + /** + * @returns {number} + */ + getMin() { + return 0; + } + + /** + * @returns {number} + */ + getMax() { + return 65535; + } +} diff --git a/src/types/IntType.js b/src/types/IntType.js new file mode 100644 index 0000000..750d701 --- /dev/null +++ b/src/types/IntType.js @@ -0,0 +1,24 @@ +/* eslint-disable class-methods-use-this */ + +import AbstractIntType from './AbstractIntType'; +import TypeName from '../enums/TypeName'; + +export default class IntType extends AbstractIntType { + constructor() { + super(TypeName.INT); + } + + /** + * @returns {number} + */ + getMin() { + return 0; + } + + /** + * @returns {number} + */ + getMax() { + return 4294967295; + } +} diff --git a/src/types/MediumBlobType.js b/src/types/MediumBlobType.js new file mode 100644 index 0000000..486d12e --- /dev/null +++ b/src/types/MediumBlobType.js @@ -0,0 +1,24 @@ +/* eslint-disable class-methods-use-this */ + +import AbstractBinaryType from './AbstractBinaryType'; +import TypeName from '../enums/TypeName'; + +export default class MediumBlobType extends AbstractBinaryType { + constructor() { + super(TypeName.MEDIUM_BLOB); + } + + /** + * @returns {number} + */ + getMaxBytes() { + return 16777215; + } + + /** + * @returns {boolean} + */ + allowedInSet() { + return false; + } +} diff --git a/src/types/MediumIntType.js b/src/types/MediumIntType.js new file mode 100644 index 0000000..9815621 --- /dev/null +++ b/src/types/MediumIntType.js @@ -0,0 +1,24 @@ +/* eslint-disable class-methods-use-this */ + +import AbstractIntType from './AbstractIntType'; +import TypeName from '../enums/TypeName'; + +export default class MediumIntType extends AbstractIntType { + constructor() { + super(TypeName.MEDIUM_INT); + } + + /** + * @returns {number} + */ + getMin() { + return 0; + } + + /** + * @returns {number} + */ + getMax() { + return 16777215; + } +} diff --git a/src/types/MediumTextType.js b/src/types/MediumTextType.js new file mode 100644 index 0000000..87e5a3a --- /dev/null +++ b/src/types/MediumTextType.js @@ -0,0 +1,24 @@ +/* eslint-disable class-methods-use-this */ + +import AbstractStringType from './AbstractStringType'; +import TypeName from '../enums/TypeName'; + +export default class MediumTextType extends AbstractStringType { + constructor() { + super(TypeName.MEDIUM_TEXT); + } + + /** + * @returns {number} + */ + getMaxBytes() { + return 16777215; + } + + /** + * @returns {boolean} + */ + allowedInSet() { + return false; + } +} diff --git a/src/types/MessageRefType.js b/src/types/MessageRefType.js new file mode 100644 index 0000000..eb4a9cf --- /dev/null +++ b/src/types/MessageRefType.js @@ -0,0 +1,73 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import MessageRef from '../MessageRef'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import DecodeValueFailed from '../exceptions/DecodeValueFailed'; + +export default class MessageRefType extends Type { + constructor() { + super(TypeName.MESSAGE_REF); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!(value instanceof MessageRef)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] was expected to be a MessageRef.`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {*} + */ + encode(value, field, codec = null) { + if (value instanceof MessageRef) { + return codec.encodeMessageRef(value, field); + } + + return null; + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?MessageRef} + */ + decode(value, field, codec = null) { + if (value === null || value instanceof MessageRef) { + return value; + } + + try { + return codec.decodeMessageRef(value, field); + } catch (e) { + throw new DecodeValueFailed(value, field, e.message); + } + } + + /** + * @returns {boolean} + */ + isScalar() { + return false; + } + + /** + * @returns {boolean} + */ + encodesToScalar() { + return false; + } +} diff --git a/src/types/MessageType.js b/src/types/MessageType.js new file mode 100644 index 0000000..0688eda --- /dev/null +++ b/src/types/MessageType.js @@ -0,0 +1,99 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import Message from '../Message'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import DecodeValueFailed from '../exceptions/DecodeValueFailed'; + +export default class MessageType extends Type { + constructor() { + super(TypeName.MESSAGE); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!(value instanceof Message)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] was expected to be a Message.`); + } + + const anyOfCuries = field.getAnyOfCuries(); + if (!anyOfCuries.length) { + return; + } + + const schema = value.schema(); + if (anyOfCuries.includes(schema.getCurie().toString())) { + return; + } + + throw new AssertionFailed(`Field [${field.getName()}] :: Value "${schema.getCurie()}" must be one of: ${anyOfCuries.join(',')}.`); + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {*} + */ + encode(value, field, codec = null) { + if (value instanceof Message) { + return codec.encodeMessage(value, field); + } + + return null; + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?Message} + */ + decode(value, field, codec = null) { + if (value === null || value instanceof Message) { + return value; + } + + try { + return codec.decodeMessage(value, field); + } catch (e) { + throw new DecodeValueFailed(value, field, e.message); + } + } + + /** + * @returns {boolean} + */ + isScalar() { + return false; + } + + /** + * @returns {boolean} + */ + encodesToScalar() { + return false; + } + + /** + * @returns {boolean} + */ + isMessage() { + return true; + } + + /** + * @returns {boolean} + */ + allowedInSet() { + return false; + } +} diff --git a/src/types/MicrotimeType.js b/src/types/MicrotimeType.js new file mode 100644 index 0000000..1570d09 --- /dev/null +++ b/src/types/MicrotimeType.js @@ -0,0 +1,80 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import Microtime from '../well-known/Microtime'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import DecodeValueFailed from '../exceptions/DecodeValueFailed'; + +export default class MicrotimeType extends Type { + constructor() { + super(TypeName.MICROTIME); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!(value instanceof Microtime)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] was expected to be a Microtime.`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?string} + */ + encode(value, field, codec = null) { + if (value instanceof Microtime) { + return value.toString(); + } + + return null; + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?Microtime} + */ + decode(value, field, codec = null) { + if (value === null || value instanceof Microtime) { + return value; + } + + try { + return Microtime.fromString(`${value}`); + } catch (e) { + throw new DecodeValueFailed(value, field, e.message); + } + } + + /** + * @returns {boolean} + */ + isScalar() { + return false; + } + + /** + * @returns {Microtime} + */ + getDefault() { + return Microtime.create(); + } + + /** + * @returns {boolean} + */ + isNumeric() { + return true; + } +} diff --git a/src/types/SignedBigIntType.js b/src/types/SignedBigIntType.js new file mode 100644 index 0000000..c2534e2 --- /dev/null +++ b/src/types/SignedBigIntType.js @@ -0,0 +1,83 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import BigNumber from '../well-known/BigNumber'; +import AssertionFailed from '../exceptions/AssertionFailed'; + +export default class SignedBigIntType extends Type { + constructor() { + super(TypeName.SIGNED_BIG_INT); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!(value instanceof BigNumber)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] was expected to be a BigNumber.`); + } + + if (!value.greaterThanOrEqualTo('-9223372036854775808')) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value "${value}" cannot be less than [-9223372036854775808].`); + } + + if (!value.lessThanOrEqualTo('9223372036854775807')) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value "${value}" cannot be greater than [9223372036854775807].`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {string} + */ + encode(value, field, codec = null) { + if (value instanceof BigNumber) { + return `${value.toFixed(0)}`; + } + + return '0'; + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?BigNumber} + */ + decode(value, field, codec = null) { + if (value === null || value instanceof BigNumber) { + return value; + } + + return new BigNumber(value); + } + + /** + * @returns {boolean} + */ + isScalar() { + return false; + } + + /** + * @returns {BigNumber} + */ + getDefault() { + return new BigNumber(0); + } + + /** + * @returns {boolean} + */ + isNumeric() { + return true; + } +} diff --git a/src/types/SignedIntType.js b/src/types/SignedIntType.js new file mode 100644 index 0000000..df47aaa --- /dev/null +++ b/src/types/SignedIntType.js @@ -0,0 +1,24 @@ +/* eslint-disable class-methods-use-this */ + +import AbstractIntType from './AbstractIntType'; +import TypeName from '../enums/TypeName'; + +export default class SignedIntType extends AbstractIntType { + constructor() { + super(TypeName.SIGNED_INT); + } + + /** + * @returns {number} + */ + getMin() { + return -2147483648; + } + + /** + * @returns {number} + */ + getMax() { + return 2147483647; + } +} diff --git a/src/types/SignedMediumIntType.js b/src/types/SignedMediumIntType.js new file mode 100644 index 0000000..0e56005 --- /dev/null +++ b/src/types/SignedMediumIntType.js @@ -0,0 +1,24 @@ +/* eslint-disable class-methods-use-this */ + +import AbstractIntType from './AbstractIntType'; +import TypeName from '../enums/TypeName'; + +export default class SignedMediumIntType extends AbstractIntType { + constructor() { + super(TypeName.SIGNED_MEDIUM_INT); + } + + /** + * @returns {number} + */ + getMin() { + return -8388608; + } + + /** + * @returns {number} + */ + getMax() { + return 8388607; + } +} diff --git a/src/types/SignedSmallIntType.js b/src/types/SignedSmallIntType.js new file mode 100644 index 0000000..7bc598f --- /dev/null +++ b/src/types/SignedSmallIntType.js @@ -0,0 +1,24 @@ +/* eslint-disable class-methods-use-this */ + +import AbstractIntType from './AbstractIntType'; +import TypeName from '../enums/TypeName'; + +export default class SignedSmallIntType extends AbstractIntType { + constructor() { + super(TypeName.SIGNED_SMALL_INT); + } + + /** + * @returns {number} + */ + getMin() { + return -32768; + } + + /** + * @returns {number} + */ + getMax() { + return 32767; + } +} diff --git a/src/types/SignedTinyIntType.js b/src/types/SignedTinyIntType.js new file mode 100644 index 0000000..85dd965 --- /dev/null +++ b/src/types/SignedTinyIntType.js @@ -0,0 +1,24 @@ +/* eslint-disable class-methods-use-this */ + +import AbstractIntType from './AbstractIntType'; +import TypeName from '../enums/TypeName'; + +export default class SignedTinyIntType extends AbstractIntType { + constructor() { + super(TypeName.SIGNED_TINY_INT); + } + + /** + * @returns {number} + */ + getMin() { + return -128; + } + + /** + * @returns {number} + */ + getMax() { + return 127; + } +} diff --git a/src/types/SmallIntType.js b/src/types/SmallIntType.js new file mode 100644 index 0000000..fa1ef1f --- /dev/null +++ b/src/types/SmallIntType.js @@ -0,0 +1,24 @@ +/* eslint-disable class-methods-use-this */ + +import AbstractIntType from './AbstractIntType'; +import TypeName from '../enums/TypeName'; + +export default class SmallIntType extends AbstractIntType { + constructor() { + super(TypeName.SMALL_INT); + } + + /** + * @returns {number} + */ + getMin() { + return 0; + } + + /** + * @returns {number} + */ + getMax() { + return 65535; + } +} diff --git a/src/types/StringEnumType.js b/src/types/StringEnumType.js new file mode 100644 index 0000000..a7623c9 --- /dev/null +++ b/src/types/StringEnumType.js @@ -0,0 +1,96 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import Enum from '@gdbots/common/Enum'; +import isString from 'lodash/isString'; +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import DecodeValueFailed from '../exceptions/DecodeValueFailed'; + +export default class StringEnumType extends Type { + constructor() { + super(TypeName.STRING_ENUM); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!(value instanceof Enum)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] was expected to be an Enum.`); + } + + if (!(value instanceof field.getClassProto())) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value "${value.getEnumId()}" was expected to be "${field.getClassProto().getEnumId()}".`); + } + + const enumValue = value.getValue(); + if (!isString(enumValue)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Enum's value "${value}" is not a string.`); + } + + if (enumValue.length < 1 || enumValue.length > this.getMaxBytes()) { + throw new AssertionFailed(`Field [${field.getName()}] :: Must be between [1] and [${this.getMaxBytes()}] bytes, [${enumValue.length}] bytes given.`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?string} + */ + encode(value, field, codec = null) { + if (value instanceof Enum) { + return `${value.getValue()}`; + } + + return null; + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?Enum} + * + * @throws {DecodeValueFailed} + */ + decode(value, field, codec = null) { + if (value === null) { + return null; + } + + try { + return field.getClassProto().create(value); + } catch (e) { + throw new DecodeValueFailed(value, field, e.message); + } + } + + /** + * @returns {boolean} + */ + isScalar() { + return false; + } + + /** + * @returns {boolean} + */ + isString() { + return true; + } + + /** + * @returns {number} + */ + getMaxBytes() { + return 100; + } +} diff --git a/src/types/StringType.js b/src/types/StringType.js new file mode 100644 index 0000000..a6d38de --- /dev/null +++ b/src/types/StringType.js @@ -0,0 +1,153 @@ +/* eslint-disable class-methods-use-this, no-unused-vars, max-len, no-useless-escape */ + +import isValidEmail from '@gdbots/common/isValidEmail'; +import isValidHashtag from '@gdbots/common/isValidHashtag'; +import isValidISO8601Date from '@gdbots/common/isValidISO8601Date'; +import isValidHostname from '@gdbots/common/isValidHostname'; +import isValidIpv4 from '@gdbots/common/isValidIpv4'; +import isValidIpv6 from '@gdbots/common/isValidIpv6'; +import isValidUri from '@gdbots/common/isValidUri'; +import isValidUrl from '@gdbots/common/isValidUrl'; +import AbstractStringType from './AbstractStringType'; +import Format from '../enums/Format'; +import TypeName from '../enums/TypeName'; +import AssertionFailed from '../exceptions/AssertionFailed'; + +export default class StringType extends AbstractStringType { + constructor() { + super(TypeName.STRING); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + super.guard(value, field); + + if (field.getPattern() && !field.getPattern().test(value)) { + throw new AssertionFailed( + `Field [${field.getName()}] :: Value "${value}" does not match expression "${field.getPattern()}".`, + ); + } + + switch (field.getFormat()) { + case Format.UNKNOWN: + break; + + case Format.DATE: + if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) { + throw new AssertionFailed( + `Field [${field.getName()}] :: Value "${value}" is not a valid date with format "YYYY-MM-DD".`, + ); + } + + break; + + case Format.DATE_TIME: + if (!isValidISO8601Date(value)) { + throw new AssertionFailed( + `Field [${field.getName()}] :: Value "${value}" is not a valid ISO8601 date/time. E.g. "2017-05-25T02:54:18Z".`, + ); + } + + break; + + case Format.SLUG: + // note that this format is less restrictive than "isValidSlug" function from @gdbots/common. + // This is intentional as not everyone is as strict with slug formats. for example, youtube + // "slugs" contain both upper and lower case characters and underscores and hyphens. + if (!/^([\w\/-]|[\w-][\w\/-]*[\w-])$/.test(value)) { + throw new AssertionFailed( + `Field [${field.getName()}] :: Value "${value}" is not a valid slug.`, + ); + } + + break; + + case Format.EMAIL: + if (!isValidEmail(value)) { + throw new AssertionFailed( + `Field [${field.getName()}] :: Value "${value}" is not a valid email address.`, + ); + } + + break; + + case Format.HASHTAG: + if (!isValidHashtag(value)) { + throw new AssertionFailed( + `Field [${field.getName()}] :: Value "${value}" is not a valid hashtag.`, + ); + } + + break; + + case Format.IPV4: + if (!isValidIpv4(value)) { + throw new AssertionFailed( + `Field [${field.getName()}] :: Value "${value}" is not a valid IPv4 address.`, + ); + } + + break; + + case Format.IPV6: + if (!isValidIpv6(value)) { + throw new AssertionFailed( + `Field [${field.getName()}] :: Value "${value}" is not a valid IPv6 address.`, + ); + } + + break; + + case Format.HOSTNAME: + if (!isValidHostname(value)) { + throw new AssertionFailed( + `Field [${field.getName()}] :: Value "${value}" is not a valid HOSTNAME.`, + ); + } + + break; + + case Format.URI: + if (!isValidUri(value)) { + throw new AssertionFailed( + `Field [${field.getName()}] :: Value "${value}" is not a valid URI.`, + ); + } + + break; + + case Format.URL: + if (!isValidUrl(value)) { + throw new AssertionFailed( + `Field [${field.getName()}] :: Value "${value}" is not a valid URL.`, + ); + } + + break; + + case Format.UUID: + if (!/^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/.test(value)) { + throw new AssertionFailed( + `Field [${field.getName()}] :: Value "${value}" is not a valid UUID.`, + ); + } + + break; + + default: + break; + } + } + + /** + * @returns {number} + */ + getMaxBytes() { + return 255; + } +} diff --git a/src/types/TextType.js b/src/types/TextType.js new file mode 100644 index 0000000..e37ccbf --- /dev/null +++ b/src/types/TextType.js @@ -0,0 +1,17 @@ +/* eslint-disable class-methods-use-this */ + +import AbstractStringType from './AbstractStringType'; +import TypeName from '../enums/TypeName'; + +export default class TextType extends AbstractStringType { + constructor() { + super(TypeName.TEXT); + } + + /** + * @returns {boolean} + */ + allowedInSet() { + return false; + } +} diff --git a/src/types/TimeUuidType.js b/src/types/TimeUuidType.js new file mode 100644 index 0000000..272a5c7 --- /dev/null +++ b/src/types/TimeUuidType.js @@ -0,0 +1,85 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import TimeUuidIdentifier from '../well-known/TimeUuidIdentifier'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import DecodeValueFailed from '../exceptions/DecodeValueFailed'; + +export default class TimeUuidType extends Type { + constructor() { + super(TypeName.TIME_UUID); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!(value instanceof TimeUuidIdentifier)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] was expected to be a TimeUuidIdentifier.`); + } + + if (field.hasClassProto() && !(value instanceof field.getClassProto())) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value "${value}" was expected to be a "${field.getClassProto().name}".`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?string} + */ + encode(value, field, codec = null) { + if (value instanceof TimeUuidIdentifier) { + return value.toString(); + } + + return null; + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?TimeUuidIdentifier} + */ + decode(value, field, codec = null) { + const expectedProto = field.hasClassProto() ? field.getClassProto() : TimeUuidIdentifier; + if (value === null || value instanceof expectedProto) { + return value; + } + + try { + return expectedProto.fromString(`${value}`); + } catch (e) { + throw new DecodeValueFailed(value, field, e.message); + } + } + + /** + * @returns {boolean} + */ + isScalar() { + return false; + } + + /** + * @returns {TimeUuidIdentifier} + */ + getDefault() { + return TimeUuidIdentifier.generate(); + } + + /** + * @returns {boolean} + */ + isString() { + return true; + } +} diff --git a/src/types/TimestampType.js b/src/types/TimestampType.js new file mode 100644 index 0000000..d766881 --- /dev/null +++ b/src/types/TimestampType.js @@ -0,0 +1,62 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import isSafeInteger from 'lodash/isSafeInteger'; +import toSafeInteger from 'lodash/toSafeInteger'; +import isValidTimestamp from '@gdbots/common/isValidTimestamp'; +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import AssertionFailed from '../exceptions/AssertionFailed'; + +export default class TimestampType extends Type { + constructor() { + super(TypeName.TIMESTAMP); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!isSafeInteger(value) || !isValidTimestamp(value)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] is not a valid unix timestamp.`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {number} + */ + encode(value, field, codec = null) { + return toSafeInteger(value); + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {number} + */ + decode(value, field, codec = null) { + return toSafeInteger(value); + } + + /** + * @returns {number} + */ + getDefault() { + return Math.floor(Date.now() / 1000); + } + + /** + * @returns {boolean} + */ + isNumeric() { + return true; + } +} diff --git a/src/types/TinyIntType.js b/src/types/TinyIntType.js new file mode 100644 index 0000000..d9bdb08 --- /dev/null +++ b/src/types/TinyIntType.js @@ -0,0 +1,24 @@ +/* eslint-disable class-methods-use-this */ + +import AbstractIntType from './AbstractIntType'; +import TypeName from '../enums/TypeName'; + +export default class TinyIntType extends AbstractIntType { + constructor() { + super(TypeName.TINY_INT); + } + + /** + * @returns {number} + */ + getMin() { + return 0; + } + + /** + * @returns {number} + */ + getMax() { + return 255; + } +} diff --git a/src/types/TrinaryType.js b/src/types/TrinaryType.js new file mode 100644 index 0000000..58f7b39 --- /dev/null +++ b/src/types/TrinaryType.js @@ -0,0 +1,92 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import isSafeInteger from 'lodash/isSafeInteger'; +import toSafeInteger from 'lodash/toSafeInteger'; +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import AssertionFailed from '../exceptions/AssertionFailed'; + +/** + * @link https://en.wikipedia.org/wiki/Three-valued_logic + * 0 = unknown + * 1 = true + * 2 = false + */ +export default class TrinaryType extends Type { + constructor() { + super(TypeName.TRINARY); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!isSafeInteger(value)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] is not an integer.`); + } + + if ([0, 1, 2].indexOf(value) === -1) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value "${value}" is not an element of the valid values: [0, 1, 2]`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {number} + */ + encode(value, field, codec = null) { + return toSafeInteger(value); + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {number} + */ + decode(value, field, codec = null) { + return toSafeInteger(value); + } + + /** + * @returns {number} + */ + getDefault() { + return 0; + } + + /** + * @returns {boolean} + */ + isNumeric() { + return true; + } + + /** + * @returns {number} + */ + getMin() { + return 0; + } + + /** + * @returns {number} + */ + getMax() { + return 2; + } + + /** + * @returns {boolean} + */ + allowedInSet() { + return false; + } +} diff --git a/src/types/Type.js b/src/types/Type.js new file mode 100644 index 0000000..a86d2d6 --- /dev/null +++ b/src/types/Type.js @@ -0,0 +1,129 @@ +/* eslint-disable class-methods-use-this */ + +/** + * We store all Type instances to accomplish a loose flyweight strategy. + * Loose because we're not strictly enforcing it, but internally in this + * library we only use the factory create method to create types. + * + * @type {Map} + */ +const instances = new Map(); + +export default class Type { + /** + * @param {TypeName} typeName + */ + constructor(typeName) { + this.typeName = typeName; + Object.freeze(this); + } + + /** + * @returns {Type} + */ + static create() { + if (!instances.has(this)) { + instances.set(this, new this()); + } + + return instances.get(this); + } + + /** + * @returns {TypeName} + */ + getTypeName() { + return this.typeName; + } + + /** + * @returns {string} + */ + getTypeValue() { + return this.typeName.getValue(); + } + + /** + * @returns {boolean} + */ + isScalar() { + return true; + } + + /** + * @returns {boolean} + */ + encodesToScalar() { + return true; + } + + /** + * @returns {*} + */ + getDefault() { + return null; + } + + /** + * @returns {boolean} + */ + isBoolean() { + return false; + } + + /** + * @returns {boolean} + */ + isBinary() { + return false; + } + + /** + * @returns {boolean} + */ + isNumeric() { + return false; + } + + /** + * @returns {boolean} + */ + isString() { + return false; + } + + /** + * @returns {boolean} + */ + isMessage() { + return false; + } + + /** + * @returns {number} + */ + getMin() { + return -2147483648; + } + + /** + * @returns {number} + */ + getMax() { + return 2147483647; + } + + /** + * @returns {number} + */ + getMaxBytes() { + return 65535; + } + + /** + * @returns {boolean} + */ + allowedInSet() { + return true; + } +} diff --git a/src/types/UuidType.js b/src/types/UuidType.js new file mode 100644 index 0000000..20393a4 --- /dev/null +++ b/src/types/UuidType.js @@ -0,0 +1,85 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +import Type from './Type'; +import TypeName from '../enums/TypeName'; +import UuidIdentifier from '../well-known/UuidIdentifier'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import DecodeValueFailed from '../exceptions/DecodeValueFailed'; + +export default class UuidType extends Type { + constructor() { + super(TypeName.UUID); + } + + /** + * @param {*} value + * @param {Field} field + * + * @throws {AssertionFailed} + */ + guard(value, field) { + if (!(value instanceof UuidIdentifier)) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value [${JSON.stringify(value)}] was expected to be a UuidIdentifier.`); + } + + if (field.hasClassProto() && !(value instanceof field.getClassProto())) { + throw new AssertionFailed(`Field [${field.getName()}] :: Value "${value}" was expected to be a "${field.getClassProto().name}".`); + } + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?string} + */ + encode(value, field, codec = null) { + if (value instanceof UuidIdentifier) { + return value.toString(); + } + + return null; + } + + /** + * @param {*} value + * @param {Field} field + * @param {Codec} [codec] + * + * @returns {?UuidIdentifier} + */ + decode(value, field, codec = null) { + const expectedProto = field.hasClassProto() ? field.getClassProto() : UuidIdentifier; + if (value === null || value instanceof expectedProto) { + return value; + } + + try { + return expectedProto.fromString(`${value}`); + } catch (e) { + throw new DecodeValueFailed(value, field, e.message); + } + } + + /** + * @returns {boolean} + */ + isScalar() { + return false; + } + + /** + * @returns {UuidIdentifier} + */ + getDefault() { + return UuidIdentifier.generate(); + } + + /** + * @returns {boolean} + */ + isString() { + return true; + } +} diff --git a/src/types/index.js b/src/types/index.js new file mode 100644 index 0000000..378a775 --- /dev/null +++ b/src/types/index.js @@ -0,0 +1,71 @@ +import BigIntType from './BigIntType'; +import BinaryType from './BinaryType'; +import BlobType from './BlobType'; +import BooleanType from './BooleanType'; +import DateType from './DateType'; +import DateTimeType from './DateTimeType'; +import DecimalType from './DecimalType'; +import DynamicFieldType from './DynamicFieldType'; +import FloatType from './FloatType'; +import GeoPointType from './GeoPointType'; +import IdentifierType from './IdentifierType'; +import IntEnumType from './IntEnumType'; +import IntType from './IntType'; +import MediumBlobType from './MediumBlobType'; +import MediumIntType from './MediumIntType'; +import MediumTextType from './MediumTextType'; +import MessageRefType from './MessageRefType'; +import MessageType from './MessageType'; +import MicrotimeType from './MicrotimeType'; +import SignedBigIntType from './SignedBigIntType'; +import SignedIntType from './SignedIntType'; +import SignedMediumIntType from './SignedMediumIntType'; +import SignedSmallIntType from './SignedSmallIntType'; +import SignedTinyIntType from './SignedTinyIntType'; +import SmallIntType from './SmallIntType'; +import StringEnumType from './StringEnumType'; +import StringType from './StringType'; +import TextType from './TextType'; +import TimestampType from './TimestampType'; +import TimeUuidType from './TimeUuidType'; +import TinyIntType from './TinyIntType'; +import TrinaryType from './TrinaryType'; +import Type from './Type'; +import UuidType from './UuidType'; + +export default { + BigIntType, + BinaryType, + BlobType, + BooleanType, + DateTimeType, + DateType, + DecimalType, + DynamicFieldType, + FloatType, + GeoPointType, + IdentifierType, + IntEnumType, + IntType, + MediumBlobType, + MediumIntType, + MediumTextType, + MessageRefType, + MessageType, + MicrotimeType, + SignedBigIntType, + SignedIntType, + SignedMediumIntType, + SignedSmallIntType, + SignedTinyIntType, + SmallIntType, + StringEnumType, + StringType, + TextType, + TimestampType, + TimeUuidType, + TinyIntType, + TrinaryType, + Type, + UuidType, +}; diff --git a/src/well-known/BigNumber.js b/src/well-known/BigNumber.js new file mode 100644 index 0000000..af5dced --- /dev/null +++ b/src/well-known/BigNumber.js @@ -0,0 +1,3 @@ +import BigNumber from 'bignumber.js'; + +export default BigNumber; diff --git a/src/well-known/DatedSlugIdentifier.js b/src/well-known/DatedSlugIdentifier.js new file mode 100644 index 0000000..8ec271d --- /dev/null +++ b/src/well-known/DatedSlugIdentifier.js @@ -0,0 +1,37 @@ +import addDateToSlug from '@gdbots/common/addDateToSlug'; +import createSlug from '@gdbots/common/createSlug'; +import isValidSlug from '@gdbots/common/isValidSlug'; +import slugContainsDate from '@gdbots/common/slugContainsDate'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import Identifier from './Identifier'; + +export default class DatedSlugIdentifier extends Identifier { + /** + * @param {string} value + */ + constructor(value) { + super(value); + + if (!isValidSlug(this.value, true) || !slugContainsDate(this.value)) { + throw new AssertionFailed(`Value "${this.value}" is not a valid dated slug.`); + } + + Object.freeze(this); + } + + /** + * @param {string} str + * @param {?Date} [date] + * + * @returns {DatedSlugIdentifier} + */ + static create(str, date = null) { + const slug = createSlug(str, true); + + if (slugContainsDate(slug)) { + return new this(slug); + } + + return new this(addDateToSlug(slug, date)); + } +} diff --git a/src/well-known/DynamicField.js b/src/well-known/DynamicField.js new file mode 100644 index 0000000..0d83ca3 --- /dev/null +++ b/src/well-known/DynamicField.js @@ -0,0 +1,296 @@ +import isString from 'lodash/isString'; +import DynamicFieldKind from '../enums/DynamicFieldKind'; +import FieldRule from '../enums/FieldRule'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import Field from '../Field'; +import BooleanType from '../types/BooleanType'; +import DateType from '../types/DateType'; +import FloatType from '../types/FloatType'; +import IntType from '../types/IntType'; +import StringType from '../types/StringType'; +import TextType from '../types/TextType'; + +/** + * Dynamic fields need one field object per "kind". + * Map provides the storage for the flyweight strategy. + * + * @type {Map} + */ +const fields = new Map(); + +/** + * @param {string} kind + * + * @returns {Field} + */ +function createField(kind) { + if (!fields.has(kind)) { + let type; + switch (kind) { + case DynamicFieldKind.STRING_VAL.toString(): + type = StringType.create(); + break; + + case DynamicFieldKind.TEXT_VAL.toString(): + type = TextType.create(); + break; + + case DynamicFieldKind.INT_VAL.toString(): + type = IntType.create(); + break; + + case DynamicFieldKind.BOOL_VAL.toString(): + type = BooleanType.create(); + break; + + case DynamicFieldKind.FLOAT_VAL.toString(): + type = FloatType.create(); + break; + + case DynamicFieldKind.DATE_VAL.toString(): + type = DateType.create(); + break; + + default: + throw new AssertionFailed(`DynamicField "${kind}" is not a valid type.`); + } + + fields.set( + kind, new Field({ name: kind, type, rule: FieldRule.A_SINGLE_VALUE, required: true }), + ); + } + + return fields.get(kind); +} + +/** + * Regular expression pattern for matching a valid dynamic field name. + * @type {RegExp} + */ +export const VALID_NAME_PATTERN = /^[a-zA-Z_]{1}[a-zA-Z0-9_-]{0,126}$/; + +/** + * DynamicField is a wrapper for fields which would not be ideal as a map because + * you don't know what the field name is going to be until runtime or the number + * of fields you'll end up having will be too large. + * + * A common use case is a polling or custom form service. Eventually the number of + * fields you have is in the thousands and systems like SQL, ElasticSearch will not + * do well with that many fields. DynamicField is designed to be a "named union". + * + * For example: + * [ + * // the name of the field + * 'name' => 'your-field-name', + * // only one of the following values can be populated. + * 'bool_val' => true, + * 'date_val' => '2015-12-25', + * 'float_val' => 1.0, + * 'int_val' => 1, + * 'string_val' => 'string', + * 'text_val' => 'some text', + * ] + */ +export default class DynamicField { + /** + * @param {string} name + * @param {*} kind + * @param {*} value + * + * @throws {AssertionFailed} + */ + constructor(name, kind, value) { + if (!isString(name)) { + throw new AssertionFailed('DynamicField name must be a string.'); + } + + if (!(kind instanceof DynamicFieldKind)) { + throw new AssertionFailed('DynamicField kind was expected to be an instance of DynamicFieldKind.'); + } + + if (!VALID_NAME_PATTERN.test(name)) { + throw new AssertionFailed(`DynamicField [${name}] is invalid. It must match the pattern [${VALID_NAME_PATTERN}].`); + } + + Object.defineProperty(this, 'name', { value: name }); + Object.defineProperty(this, 'kind', { value: `${kind}` }); + + const field = createField(this.kind); + const decodedValue = field.getType().decode(value, field); + field.guardValue(decodedValue); + Object.defineProperty(this, 'value', { value: decodedValue }); + + Object.freeze(this); + } + + /** + * @param {string} json + * + * @returns {DynamicField} + * + * @throws {AssertionFailed} + */ + static fromJSON(json) { + let obj; + + try { + obj = JSON.parse(json); + } catch (e) { + throw new AssertionFailed('Invalid JSON.'); + } + + return DynamicField.fromObject(obj); + } + + /** + * @param {Object} obj + * + * @returns {DynamicField} + * + * @throws {AssertionFailed} + */ + static fromObject(obj = {}) { + if (!obj.name) { + throw new AssertionFailed('DynamicField "name" property must be set.'); + } + + const kind = Object.keys(obj).filter(key => key !== 'name').pop(); + + try { + return new DynamicField(obj.name, DynamicFieldKind.create(kind), obj[kind]); + } catch (e) { + throw new AssertionFailed(`DynamicField "${kind}" is invalid.`); + } + } + + /** + * @param {string} name + * @param {boolean} value + * + * @return {DynamicField} + */ + static createBoolVal(name, value = false) { + return new DynamicField(name, DynamicFieldKind.BOOL_VAL, value); + } + + /** + * @param {string} name + * @param {Date} value + * + * @return {DynamicField} + */ + static createDateVal(name, value) { + return new DynamicField(name, DynamicFieldKind.DATE_VAL, value); + } + + /** + * @param {string} name + * @param {number} value + * + * @return {DynamicField} + */ + static createFloatVal(name, value = 0.0) { + return new DynamicField(name, DynamicFieldKind.FLOAT_VAL, value); + } + + /** + * @param {string} name + * @param {number} value + * + * @return {DynamicField} + */ + static createIntVal(name, value = 0) { + return new DynamicField(name, DynamicFieldKind.INT_VAL, value); + } + + /** + * @param {string} name + * @param {string} value + * + * @return {DynamicField} + */ + static createStringVal(name, value) { + return new DynamicField(name, DynamicFieldKind.STRING_VAL, value); + } + + /** + * @param {string} name + * @param {string} value + * + * @return {DynamicField} + */ + static createTextVal(name, value) { + return new DynamicField(name, DynamicFieldKind.TEXT_VAL, value); + } + + /** + * @returns {string} + */ + getName() { + return this.name; + } + + /** + * @returns {string} + */ + getKind() { + return this.kind; + } + + /** + * @returns {Field} + */ + getField() { + return createField(this.kind); + } + + /** + * @returns {*} + */ + getValue() { + return this.value; + } + + /** + * @returns {string} + */ + toString() { + return JSON.stringify(this); + } + + /** + * @returns {Object} + */ + toObject() { + const field = createField(this.kind); + return { + name: this.name, + [this.kind]: field.getType().encode(this.value, field), + }; + } + + /** + * @returns {Object} + */ + toJSON() { + return this.toObject(); + } + + /** + * @returns {string} + */ + valueOf() { + return this.toString(); + } + + /** + * @param {DynamicField} other + * + * @returns {boolean} + */ + equals(other) { + return this.name === other.name + && this.kind === other.kind + && this.value === other.value; + } +} diff --git a/src/well-known/GeoPoint.js b/src/well-known/GeoPoint.js new file mode 100644 index 0000000..a3ba8dd --- /dev/null +++ b/src/well-known/GeoPoint.js @@ -0,0 +1,123 @@ +import isArray from 'lodash/isArray'; +import toFinite from 'lodash/toFinite'; +import AssertionFailed from '../exceptions/AssertionFailed'; + +/** + * Represents a GeoJson Point value. + * @link http://geojson.org/geojson-spec.html#point + */ +export default class GeoPoint { + /** + * @param {number} lat + * @param {number} lon + * + * @throws {AssertionFailed} + */ + constructor(lat, lon) { + const flat = toFinite(toFinite(lat).toFixed(8)); + const flon = toFinite(toFinite(lon).toFixed(8)); + + if (flat > 90.0 || flat < -90.0) { + throw new AssertionFailed(`Latitude "${flat}" must be within range [-90.0, 90.0].`); + } + + if (flon > 180.0 || flon < -180.0) { + throw new AssertionFailed(`Longitude "${flon}" must be within range [-180.0, 180.0].`); + } + + Object.defineProperty(this, 'lat', { value: flat }); + Object.defineProperty(this, 'lon', { value: flon }); + Object.freeze(this); + } + + /** + * @param {string} value + * + * @returns {GeoPoint} + */ + static fromString(value) { + const p = value.split(','); + return new GeoPoint(p[0], p[1]); + } + + /** + * @param {string} json + * + * @returns {GeoPoint} + */ + static fromJSON(json) { + let obj; + + try { + obj = JSON.parse(json); + } catch (e) { + throw new AssertionFailed('Invalid JSON.'); + } + + return GeoPoint.fromObject(obj); + } + + /** + * @param {Object} obj + * + * @returns {GeoPoint} + */ + static fromObject(obj = {}) { + if (obj.coordinates && isArray(obj.coordinates) && obj.coordinates.length === 2) { + return new GeoPoint(obj.coordinates[1], obj.coordinates[0]); + } + + throw new AssertionFailed('Invalid GeoJson "Point" type.'); + } + + /** + * @returns {number} + */ + getLatitude() { + return this.lat; + } + + /** + * @returns {number} + */ + getLongitude() { + return this.lon; + } + + /** + * @returns {string} + */ + toString() { + return `${this.lat},${this.lon}`; + } + + /** + * @returns {Object} + */ + toObject() { + return { type: 'Point', coordinates: [this.lon, this.lat] }; + } + + /** + * @returns {Object} + */ + toJSON() { + return this.toObject(); + } + + /** + * @returns {string} + */ + valueOf() { + return this.toString(); + } + + /** + * @param {GeoPoint} other + * + * @returns {boolean} + */ + equals(other) { + return `${this}` === `${other}`; + } +} diff --git a/src/well-known/Identifier.js b/src/well-known/Identifier.js new file mode 100644 index 0000000..e725cfa --- /dev/null +++ b/src/well-known/Identifier.js @@ -0,0 +1,62 @@ +import isString from 'lodash/isString'; +import trim from 'lodash/trim'; +import AssertionFailed from '../exceptions/AssertionFailed'; + +export default class Identifier { + /** + * @param {string} value + * + * @throws {AssertionFailed} + */ + constructor(value) { + if (!isString(value)) { + throw new AssertionFailed(`${this.constructor.name}'s value must be a string.`); + } + + const trimmed = trim(value); + if (trimmed === '') { + throw new AssertionFailed(`${this.constructor.name}'s value cannot be empty.`); + } + + Object.defineProperty(this, 'value', { value: trimmed }); + } + + /** + * @param {string} value + * + * @returns {Identifier} + */ + static fromString(value) { + return new this(value); + } + + /** + * @returns {string} + */ + toString() { + return this.value; + } + + /** + * @returns {string} + */ + toJSON() { + return this.value; + } + + /** + * @returns {string} + */ + valueOf() { + return this.value; + } + + /** + * @param {Identifier} other + * + * @returns {boolean} + */ + equals(other) { + return this.value === other.value; + } +} diff --git a/src/well-known/Microtime.js b/src/well-known/Microtime.js new file mode 100644 index 0000000..81b930d --- /dev/null +++ b/src/well-known/Microtime.js @@ -0,0 +1,126 @@ +import isString from 'lodash/isString'; +import moment from 'moment'; +import padEnd from 'lodash/padEnd'; +import AssertionFailed from '../exceptions/AssertionFailed'; + +/** + * Value object for microtime with methods to convert to and from integers. + * Note that this is a unix timestamp __WITH__ microseconds but stored + * as an integer NOT a float. + * + * In the PHP lib we have 10 digits (unix timestamp) concatenated with + * 6 microsecond digits (from php's microtime function we get that value). + * In JavaScript we don't get microsecond precision, we get milliseconds. + * fixme: generate microseconds precision to match php. + * + * @link http://php.net/manual/en/function.microtime.php + * @link https://github.com/gdbots/pbj-php/blob/master/src/well-known/Microtime.php + */ +export default class Microtime { + /** + * @param {string} value + * + * @throws {AssertionFailed} + */ + constructor(value) { + if (!isString(value)) { + throw new AssertionFailed('Microtime value must be a string.'); + } + + if (!/^[0-9]{16}$/.test(value)) { + throw new AssertionFailed('Microtime must be 16 digits.'); + } + + Object.defineProperty(this, 'value', { value }); + Object.freeze(this); + } + + /** + * @returns {Microtime} + */ + static create() { + return Microtime.fromMoment(moment()); + } + + /** + * @param {Date} date + * + * @returns {Microtime} + */ + static fromDate(date) { + return Microtime.fromMoment(moment(date)); + } + + /** + * @param {string} value + * + * @returns {Microtime} + */ + static fromString(value) { + return new Microtime(value); + } + + /** + * @private + * + * @param {moment.Moment} m + * + * @returns {Microtime} + */ + static fromMoment(m) { + return new Microtime(`${padEnd(m.valueOf(), 16, '0')}`); + } + + /** + * @returns {number} + */ + toNumber() { + return +`${this.value.substr(0, 10)}.${this.value.substr(10)}`; + } + + /** + * @private + * + * @returns {moment.Moment} + */ + toMoment() { + return moment.unix(this.toNumber()); + } + + /** + * @returns {Date} + */ + toDate() { + return this.toMoment().toDate(); + } + + /** + * @returns {string} + */ + toString() { + return this.value; + } + + /** + * @returns {string} + */ + toJSON() { + return this.value; + } + + /** + * @returns {string} + */ + valueOf() { + return this.value; + } + + /** + * @param {Microtime} other + * + * @returns {boolean} + */ + equals(other) { + return this.value === other.value; + } +} diff --git a/src/well-known/SlugIdentifier.js b/src/well-known/SlugIdentifier.js new file mode 100644 index 0000000..19e35d6 --- /dev/null +++ b/src/well-known/SlugIdentifier.js @@ -0,0 +1,28 @@ +import createSlug from '@gdbots/common/createSlug'; +import isValidSlug from '@gdbots/common/isValidSlug'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import Identifier from './Identifier'; + +export default class SlugIdentifier extends Identifier { + /** + * @param {string} value + */ + constructor(value) { + super(value); + + if (!isValidSlug(this.value)) { + throw new AssertionFailed(`Value "${this.value}" is not a valid slug.`); + } + + Object.freeze(this); + } + + /** + * @param {string} str + * + * @returns {SlugIdentifier} + */ + static create(str) { + return new this(createSlug(str)); + } +} diff --git a/src/well-known/TimeUuidIdentifier.js b/src/well-known/TimeUuidIdentifier.js new file mode 100644 index 0000000..ef830c8 --- /dev/null +++ b/src/well-known/TimeUuidIdentifier.js @@ -0,0 +1,25 @@ +import uuid from 'uuid'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import UuidIdentifier from './UuidIdentifier'; + +export default class TimeUuidIdentifier extends UuidIdentifier { + /** + * @param {string} value + */ + constructor(value) { + super(value); + + if (!/^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-1[0-9A-Fa-f]{3}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/.test(this.value)) { + throw new AssertionFailed(`Value "${this.value}" is not a valid version 1 UUID.`); + } + + Object.freeze(this); + } + + /** + * @returns {TimeUuidIdentifier} + */ + static generate() { + return new this(uuid.v1()); + } +} diff --git a/src/well-known/UuidIdentifier.js b/src/well-known/UuidIdentifier.js new file mode 100644 index 0000000..4cf971e --- /dev/null +++ b/src/well-known/UuidIdentifier.js @@ -0,0 +1,25 @@ +import uuid from 'uuid'; +import AssertionFailed from '../exceptions/AssertionFailed'; +import Identifier from './Identifier'; + +export default class UuidIdentifier extends Identifier { + /** + * @param {string} value + */ + constructor(value) { + super(value); + + if (!/^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/.test(this.value)) { + throw new AssertionFailed(`Value "${this.value}" is not a valid UUID.`); + } + + Object.freeze(this); + } + + /** + * @returns {UuidIdentifier} + */ + static generate() { + return new this(uuid.v4()); + } +} diff --git a/src/well-known/big-number.js b/src/well-known/big-number.js deleted file mode 100644 index 9d79a04..0000000 --- a/src/well-known/big-number.js +++ /dev/null @@ -1,8 +0,0 @@ -'use strict'; - -import BaseBigNumber from 'bignumber.js'; - -/** - * @link https://www.npmjs.com/package/bignumber.js - */ -export default class BigNumber extends BaseBigNumber {} diff --git a/src/well-known/dated-slug-identifier.js b/src/well-known/dated-slug-identifier.js deleted file mode 100644 index 1547b90..0000000 --- a/src/well-known/dated-slug-identifier.js +++ /dev/null @@ -1,78 +0,0 @@ -'use strict'; - -import SlugUtils from 'gdbots/common/util/slug-utils'; -import StringUtils from 'gdbots/common/util/string-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import InvalidArgumentException from 'gdbots/pbj/exception/invalid-argument-exception'; -import Identifier from 'gdbots/pbj/well-known/identifier'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class DatedSlugIdentifier extends SystemUtils.mixinClass(Identifier) -{ - /** - * @param string slug - * - * @throws \InvalidArgumentException - */ - constructor(slug) { - super(); // require before using `this` - - if ('string' !== typeof slug) { - throw new InvalidArgumentException('String expected but got [' + StringUtils.varToString(slug) + '].'); - } - - if (!SlugUtils.isValid(slug, true) || !SlugUtils.containsDate(slug)) { - throw new InvalidArgumentException('The value [' + slug + '] is not a valid dated slug.'); - } - - privateProps.set(this, { - /** @var string */ - slug: slug - }); - } - - /** - * @param string string - * @param Date date - * - * @return static - */ - static create(string, date) { - let slug = new this(SlugUtils.create(string)); - - if (!SlugUtils.containsDate(slug)) { - date = date ? date : new Date(); - - slug = SlugUtils.addDate(slug, date); - } - - return new this(slug); - } - - /** - * {@inheritdoc} - */ - static fromString(string) { - return new this(string); - } - - /** - * {@inheritdoc} - */ - toString() { - return privateProps.get(this).slug; - } - - /** - * {@inheritdoc} - */ - equals(other) { - return this.toString() == other.toString(); - } -} diff --git a/src/well-known/dynamic-field.js b/src/well-known/dynamic-field.js deleted file mode 100644 index 9904f01..0000000 --- a/src/well-known/dynamic-field.js +++ /dev/null @@ -1,280 +0,0 @@ -'use strict'; - -import FromArray from 'gdbots/common/from-array'; -import ToArray from 'gdbots/common/to-array'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import InvalidArgumentException from 'gdbots/pbj/exception/invalid-argument-exception'; -import DynamicFieldKind from 'gdbots/pbj/enum/dynamic-field-kind'; -import FieldRule from 'gdbots/pbj/enum/field-rule'; -import Field from 'gdbots/pbj/field'; -import BooleanType from 'gdbots/pbj/type/boolean-type'; -import DateType from 'gdbots/pbj/type/date-type'; -import FloatType from 'gdbots/pbj/type/float-type'; -import IntType from 'gdbots/pbj/type/int-type'; -import StringType from 'gdbots/pbj/type/string-type'; -import TextType from 'gdbots/pbj/type/text-type'; - -/** - * Regular expression pattern for matching a valid dynamic field name. - * - * @constant string - */ -export const VALID_NAME_PATTERN = /^[a-zA-Z_]{1}[a-zA-Z0-9_-]*/; - -/** - * Fields are only used to allow for type guarding/encoding/decoding. - * - * @var Field[] - */ -let _fields = []; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -/** - * DynamicField is a wrapper for fields which would not be ideal as a map because - * you don't know what the field name is going to be until runtime or the number - * of fields you'll end up having will be too large. - * - * A common use case is a polling or custom form service. Eventually the number of - * fields you have is in the thousands and systems like SQL, ElasticSearch will not - * do well with that many fields. DynamicField is designed to be a "named union". - * - * For example: - * [ - * // the name of the field - * 'name' => 'your-field-name', - * // only one of the following values can be populated. - * 'bool_val' => true, - * 'date_val' => '2015-12-25', - * 'float_val' => 1.0, - * 'int_val' => 1, - * 'string_val' => 'string', - * 'text_val' => 'some text', - * ] - */ -export default class DynamicField extends SystemUtils.mixinClass(null, FromArray, ToArray) -{ - /** - * @param string name - * @param DynamicFieldKind kind - * @param mixed value - */ - constructor(name, kind, value) { - super(); // require before using `this` - - if (1 > name.length || name.length > 127) { - throw new Error('DynamicField name length must be between 1 to 127.'); - } - if (!VALID_NAME_PATTERN.test(name)) { - throw new Error('DynamicField name [' + name + '] must match pattern [' + VALID_NAME_PATTERN + '].'); - } - - let field = createField(kind.getValue()); - - privateProps.set(this, { - /** @var string */ - name: name, - - /** @var string */ - kind: kind.getValue(), - - /** @var mixed */ - value: field.getType().decode(value, field) - }); - - field.guardValue(privateProps.get(this).value); - } - - /** - * @param string name - * @param bool value - * - * @return self - */ - static createBoolVal(name, value = false) { - return new this(name, DynamicFieldKind.BOOL_VAL, value); - } - - /** - * @param string name - * @param \DateTime value - * - * @return self - */ - static createDateVal(name, value) { - return new this(name, DynamicFieldKind.DATE_VAL, value); - } - - /** - * @param string name - * @param float value - * - * @return self - */ - static createFloatVal(name, value = 0.0) { - return new this(name, DynamicFieldKind.FLOAT_VAL, value); - } - - /** - * @param string name - * @param int value - * - * @return self - */ - static createIntVal(name, value = 0) { - return new this(name, DynamicFieldKind.INT_VAL, value); - } - - /** - * @param string name - * @param string value - * - * @return self - */ - static createStringVal(name, value) { - return new this(name, DynamicFieldKind.STRING_VAL, value); - } - - /** - * @param string name - * @param string value - * - * @return self - */ - static createTextVal(name, value) { - return new this(name, DynamicFieldKind.TEXT_VAL, value); - } - - /** - * {@inheritdoc} - */ - static fromArray(data = {}) { - if (undefined === data.name) { - throw new InvalidArgumentException('DynamicField "name" property must be set.'); - } - - let name = data.name; - - delete data.name; - - let kind = Array.keys(data)[0]; - - try { - kind = DynamicFieldKind[kind.toUpperCase()]; - } catch (e) { - throw new InvalidArgumentException('DynamicField "' + kind + '" is not a valid kind.'); - } - - return new this(name, kind, data[kind.getValue()]); - } - - /** - * {@inheritdoc} - */ - toArray() { - let field = createField(privateProps.get(this).kind); - - let data = { - 'name': privateProps.get(this).name - }; - - data[privateProps.get(this).kind] = field.getType().encode(privateProps.get(this).value, field); - - return data; - } - - /** - * @return string - */ - toString() { - return JSON.stringify(this); - } - - /** - * @return string - */ - getName() { - return privateProps.get(this).name; - } - - /** - * @return string - */ - getKind() { - return privateProps.get(this).kind; - } - - /** - * @return Field - */ - getField() { - return createField(privateProps.get(this).kind); - } - - /** - * @return mixed - */ - getValue() { - return privateProps.get(this).value; - } - - /** - * @param DynamicField other - * - * @return bool - */ - equals(other) { - return privateProps.get(this).name === privateProps.get(other).name - && privateProps.get(this).kind === privateProps.get(other).kind - && privateProps.get(this).value === privateProps.get(other).value; - } -} - -/** - * @param string kind - * - * @return Field - */ -function createField(kind) { - if (undefined === _fields[kind]) { - let type; - - switch (kind) { - case DynamicFieldKind.STRING_VAL.getValue(): - type = StringType.create(); - break; - - case DynamicFieldKind.TEXT_VAL.getValue(): - type = TextType.create(); - break; - - case DynamicFieldKind.INT_VAL.getValue(): - type = IntType.create(); - break; - - case DynamicFieldKind.BOOL_VAL.getValue(): - type = BooleanType.create(); - break; - - case DynamicFieldKind.FLOAT_VAL.getValue(): - type = FloatType.create(); - break; - - case DynamicFieldKind.DATE_VAL.getValue(): - type = DateType.create(); - break; - - default: - throw new InvalidArgumentException('DynamicField "' + kind + '" is not a valid type.'); - } - - _fields[kind] = new Field(kind, type, FieldRule.A_SINGLE_VALUE, true); - } - - return _fields[kind]; -} diff --git a/src/well-known/generates-identifier.js b/src/well-known/generates-identifier.js deleted file mode 100644 index 462f03f..0000000 --- a/src/well-known/generates-identifier.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -export default class GeneratesIdentifier -{ - /** - * @return static - */ - static generate() { - throw new Error('Interface function.'); - } -} diff --git a/src/well-known/geo-point.js b/src/well-known/geo-point.js deleted file mode 100644 index 395a85c..0000000 --- a/src/well-known/geo-point.js +++ /dev/null @@ -1,99 +0,0 @@ -'use strict'; - -import FromArray from 'gdbots/common/from-array'; -import ToArray from 'gdbots/common/to-array'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import InvalidArgumentException from 'gdbots/pbj/exception/invalid-argument-exception'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -/** - * Represents a GeoJson Point value. - * - * @link http://geojson.org/geojson-spec.html#point - */ -export default class GeoPoint extends SystemUtils.mixinClass(null, FromArray, ToArray) -{ - /** - * @param float lat - * @param float lon - * - * @throws \InvalidArgumentException - */ - constructor(lat, lon) { - super(); // require before using `this` - - privateProps.set(this, { - /** @var float */ - latitude: parseFloat(lat), - - /** @var float */ - longitude: parseFloat(lon) - }); - - if (privateProps.get(this).latitude > 90.0 || privateProps.get(this).latitude < -90.0) { - throw new InvalidArgumentException('Latitude must be within range [-90.0, 90.0]'); - } - - if (privateProps.get(this).longitude > 180.0 || privateProps.get(this).longitude < -180.0) { - throw new InvalidArgumentException('Longitude must be within range [-180.0, 180.0]'); - } - } - - /** - * @return float - */ - getLatitude() { - return privateProps.get(this).latitude; - } - - /** - * @return float - */ - getLongitude() { - return privateProps.get(this).longitude; - } - - /** - * {@inheritdoc} - */ - static fromArray(data = {}) { - if (undefined !== data.coordinates) { - return new this(data.coordinates[1], data.coordinates[0]); - } - - throw new InvalidArgumentException('Payload must be a GeoJson "Point" type.'); - } - - /** - * {@inheritdoc} - */ - toArray() { - return { - 'type': 'Point', - 'coordinates': [privateProps.get(this).longitude, privateProps.get(this).latitude] - }; - } - - /** - * @param string string A string with format lat,long - * @return self - */ - static fromString(string) { - string = string.split(','); - - return new this(string[0], string[1]); - } - - /** - * @return string - */ - toString() { - return privateProps.get(this).latitude + ',' + privateProps.get(this).longitude; - } -} diff --git a/src/well-known/identifier.js b/src/well-known/identifier.js deleted file mode 100644 index e30bfa9..0000000 --- a/src/well-known/identifier.js +++ /dev/null @@ -1,37 +0,0 @@ -'use strict'; - -export default class Identifier -{ - /** - * Creates an identifier object from a string representation - * - * @param string string - * - * @return static - * - * @throws \InvalidArgumentException - */ - static fromString(string) { - throw new Error('Interface function.'); - } - - /** - * Returns a string that can be parsed by fromString() - * - * @return string - */ - toString() { - throw new Error('Interface function.'); - } - - /** - * Compares the object to another Identifier object. Returns true if both have the same type and value. - * - * @param Identifier other - * - * @return boolean - */ - equals(other) { - throw new Error('Interface function.'); - } -} diff --git a/src/well-known/index.js b/src/well-known/index.js new file mode 100644 index 0000000..6c2c63b --- /dev/null +++ b/src/well-known/index.js @@ -0,0 +1,21 @@ +import BigNumber from './BigNumber'; +import DatedSlugIdentifier from './DatedSlugIdentifier'; +import DynamicField from './DynamicField'; +import GeoPoint from './GeoPoint'; +import Identifier from './Identifier'; +import Microtime from './Microtime'; +import SlugIdentifier from './SlugIdentifier'; +import TimeUuidIdentifier from './TimeUuidIdentifier'; +import UuidIdentifier from './UuidIdentifier'; + +export default { + BigNumber, + DatedSlugIdentifier, + DynamicField, + GeoPoint, + Identifier, + Microtime, + SlugIdentifier, + TimeUuidIdentifier, + UuidIdentifier, +}; diff --git a/src/well-known/microtime.js b/src/well-known/microtime.js deleted file mode 100644 index 7723573..0000000 --- a/src/well-known/microtime.js +++ /dev/null @@ -1,155 +0,0 @@ -'use strict'; - -import DateUtils from 'gdbots/common/util/date-utils.js'; -import StringUtils from 'gdbots/common/util/string-utils.js'; -import InvalidArgumentException from 'gdbots/pbj/exception/invalid-argument-exception'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -/** - * Value object for microtime with methods to convert to and from integers. - * - * @link http://php.net/manual/en/function.microtime.php - */ -export default class Microtime -{ - /** - * Private constructor to ensure static methods are used. - */ - constructor() { - - privateProps.set(this, { - /** - * The microtime is stored as a 16 digit integer. - * - * @var int - */ - int: 0, - - /** @var int */ - sec: 0, - - /** @var int */ - usec: 0 - }); - } - - /** - * Create a new object using the current microtime. - * - * @return self - */ - static create() { - return this.fromTimeOfDay(DateUtils.gettimeofday()); - } - - /** - * Create a new object from a float value, typically one that is returned - * from the microtime(true) call. - * - * @link http://php.net/manual/en/function.microtime.php - * - * @param float float e.g. 1422060753.9581 - * - * @return self - */ - static fromFloat(float) { - let str = StringUtils.strPad(float.replace('.', ''), 16, '0').substring(0, 16); - let m = new this(); - privateProps.get(m).int = parseInt(str); - privateProps.get(m).sec = parseInt(str.substring(0, 10)); - privateProps.get(m).usec = parseInt(str.slice(-6)); - return m; - } - - /** - * Create a new object from the result of a gettimeofday call that - * is NOT returned as a float. - * - * @link http://php.net/manual/en/function.gettimeofday.php - * - * @param array tod - * - * @return self - */ - static fromTimeOfDay(tod) { - let str = tod.sec + StringUtils.strPad(tod.usec, 6, '0', 'STR_PAD_LEFT'); - let m = new this(); - privateProps.get(m).int = parseInt(str); - privateProps.get(m).sec = parseInt(str.substring(0, 10)); - privateProps.get(m).usec = parseInt(str.slice(-6)); - return m; - } - - /** - * Create a new object from the integer (or string) version of the microtime. - * - * Total digits would be unix timestamp (10) + (3-6) microtime digits. - * Lack of precision on digits will be automatically padded with zeroes. - * - * @param string|int stringOrInteger - * - * @return self - * - * @throws \InvalidArgumentException - */ - static fromString(stringOrInteger) { - let int = String(stringOrInteger); - let len = String(int).length; - if (len < 13 || len > 16) { - throw new InvalidArgumentException('Input [' + int + '] must be between 13 and 16 digits, [' + len + '] given.'); - } - - if (len < 16) { - int = StringUtils.strPad(int, 16, '0'); - } - - let m = new this(); - privateProps.get(m).int = parseInt(int); - privateProps.get(m).sec = parseInt(int.substring(0, 10)); - privateProps.get(m).usec = parseInt(int.slice(-6)); - return m; - } - - /** - * @return string - */ - toString() { - return String(privateProps.get(this).int); - } - - /** - * @return int - */ - getSeconds() { - return privateProps.get(this).sec; - } - - /** - * @return int - */ - getMicroSeconds() { - return privateProps.get(this).usec; - } - - /** - * @return Date - */ - toDateTime() { - let d = new Date(); - d.setTime(parseFloat(privateProps.get(this).sec + '.' + StringUtils.strPad(privateProps.get(this).usec, 6, '0', 'STR_PAD_LEFT')) * 1000); - return d; - } - - /** - * @return float - */ - toFloat() { - return parseFloat(privateProps.get(this).sec + '.' + StringUtils.strPad(privateProps.get(this).usec, 6, '0', 'STR_PAD_LEFT')); - } -} diff --git a/src/well-known/slug-identifier.js b/src/well-known/slug-identifier.js deleted file mode 100644 index 45daedf..0000000 --- a/src/well-known/slug-identifier.js +++ /dev/null @@ -1,69 +0,0 @@ -'use strict'; - -import SlugUtils from 'gdbots/common/util/slug-utils'; -import StringUtils from 'gdbots/common/util/string-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import InvalidArgumentException from 'gdbots/pbj/exception/invalid-argument-exception'; -import Identifier from 'gdbots/pbj/well-known/identifier'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class SlugIdentifier extends SystemUtils.mixinClass(Identifier) -{ - /** - * @param string slug - * - * @throws \InvalidArgumentException - */ - constructor(slug) { - super(); // require before using `this` - - if ('string' !== typeof slug) { - throw new InvalidArgumentException('String expected but got [' + StringUtils.varToString(slug) + '].'); - } - - if (!SlugUtils.isValid(slug)) { - throw new InvalidArgumentException('The value [' + slug + '] is not a valid slug.'); - } - - privateProps.set(this, { - /** @var string */ - slug: slug - }); - } - - /** - * @param string string - * - * @return static - */ - static create(string) { - return new this(SlugUtils.create(string)); - } - - /** - * {@inheritdoc} - */ - static fromString(string) { - return new this(string); - } - - /** - * {@inheritdoc} - */ - toString() { - return privateProps.get(this).slug; - } - - /** - * {@inheritdoc} - */ - equals(other) { - return this.toString() == other.toString(); - } -} diff --git a/src/well-known/string-identifier.js b/src/well-known/string-identifier.js deleted file mode 100644 index 72f306d..0000000 --- a/src/well-known/string-identifier.js +++ /dev/null @@ -1,59 +0,0 @@ -'use strict'; - -import StringUtils from 'gdbots/common/util/string-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import InvalidArgumentException from 'gdbots/pbj/exception/invalid-argument-exception'; -import Identifier from 'gdbots/pbj/well-known/identifier'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class StringIdentifier extends SystemUtils.mixinClass(Identifier) -{ - /** - * @param string string - * - * @throws \InvalidArgumentException - */ - constructor(string) { - super(); // require before using `this` - - if ('string' !== typeof string) { - throw new InvalidArgumentException('String expected but got [' + StringUtils.varToString(string) + '].'); - } - - privateProps.set(this, { - /** @var string */ - string: String(string).trim() - }); - - if (!privateProps.get(this).string || privateProps.get(this).string.length === 0) { - throw new InvalidArgumentException('String cannot be empty.'); - } - } - - /** - * {@inheritdoc} - */ - static fromString(string) { - return new this(string); - } - - /** - * {@inheritdoc} - */ - toString() { - return privateProps.get(this).string; - } - - /** - * {@inheritdoc} - */ - equals(other) { - return this.toString() == other.toString(); - } -} diff --git a/src/well-known/time-uuid-identifier.js b/src/well-known/time-uuid-identifier.js deleted file mode 100644 index 59d9e21..0000000 --- a/src/well-known/time-uuid-identifier.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; - -import Uuid from 'uuid'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import InvalidArgumentException from 'gdbots/pbj/exception/invalid-argument-exception'; -import UuidIdentifier from 'gdbots/pbj/well-known/uuid-identifier'; - -export default class TimeUuidIdentifier extends SystemUtils.mixinClass(UuidIdentifier) -{ - /** - * @param string uuid - * - * @throws \InvalidArgumentException - */ - constructor(uuid) { - super(uuid); - - let version1Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; - if (!version1Regex.test(uuid)) { - throw new InvalidArgumentException('A time based (version 1) uuid is required.'); - } - } - - /** - * {@inheritdoc} - */ - static generate() { - return new this(Uuid.v1()); - } -} diff --git a/src/well-known/uuid-identifier.js b/src/well-known/uuid-identifier.js deleted file mode 100644 index 90b5256..0000000 --- a/src/well-known/uuid-identifier.js +++ /dev/null @@ -1,63 +0,0 @@ -'use strict'; - -import Uuid from 'uuid'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import Identifier from 'gdbots/pbj/well-known/identifier'; -import GeneratesIdentifier from 'gdbots/pbj/well-known/generates-identifier'; - -/** - * Holds private properties - * - * @var WeakMap - */ -let privateProps = new WeakMap(); - -export default class UuidIdentifier extends SystemUtils.mixinClass(Identifier, GeneratesIdentifier) -{ - /** - * @param string uuid - */ - constructor(uuid) { - super(); // require before using `this` - - uuid = uuid.toLowerCase(); - - let version1Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; - if (!version1Regex.test(uuid)) { - throw new Error('An invalid uuid [' + uuid + '] was provided.'); - } - - privateProps.set(this, { - /** @var string */ - uuid: uuid - }); - } - - /** - * {@inheritdoc} - */ - static generate() { - return new this(Uuid.v4()); - } - - /** - * {@inheritdoc} - */ - static fromString(string) { - return new this(Uuid.unparse(Uuid.parse(string))); - } - - /** - * {@inheritdoc} - */ - toString() { - return privateProps.get(this).uuid; - } - - /** - * {@inheritdoc} - */ - equals(other) { - return this.toString() == other.toString(); - } -} diff --git a/tests/Field.test.js b/tests/Field.test.js new file mode 100644 index 0000000..c3df2ad --- /dev/null +++ b/tests/Field.test.js @@ -0,0 +1,319 @@ +import test from 'tape'; +import Field from '../src/Field'; +import FieldRule from '../src/enums/FieldRule'; +import Format from '../src/enums/Format'; +import T from '../src/types'; +import SampleStringEnum from './fixtures/enums/SampleStringEnum'; + +test('Field tests', (t) => { + let field = new Field({ + name: 'test', + type: T.StringType.create(), + required: true, + format: Format.SLUG, + defaultValue: 'homer-simpson', + }); + + t.true(field instanceof Field, 'field MUST be an instanceOf Field'); + t.same(field.getName(), 'test'); + t.true(field.isRequired()); + t.same(field.getFormat(), Format.SLUG); + t.same(field.getType(), T.StringType.create()); + t.false(field.isAList()); + t.false(field.isAMap()); + t.false(field.isASet()); + t.true(field.isASingleValue()); + t.false(field.isOverridable()); + t.same(field.getDefault(), 'homer-simpson'); + + try { + field.test = 1; + t.fail('field instance is mutable'); + } catch (e) { + t.pass('field instance is immutable'); + } + + field = new Field({ + name: 'test', + type: T.DateType.create(), + rule: FieldRule.A_LIST, + overridable: true, + }); + t.same(field.getType(), T.DateType.create()); + t.true(field.isAList()); + t.false(field.isAMap()); + t.false(field.isASet()); + t.false(field.isASingleValue()); + t.true(field.isOverridable()); + + field = new Field({ + name: 'test', + type: T.DateType.create(), + rule: FieldRule.A_MAP, + }); + t.same(field.getType(), T.DateType.create()); + t.false(field.isAList()); + t.true(field.isAMap()); + t.false(field.isASet()); + t.false(field.isASingleValue()); + t.false(field.isOverridable()); + + field = new Field({ + name: 'test', + type: T.UuidType.create(), + rule: FieldRule.A_SET, + }); + t.same(field.getType(), T.UuidType.create()); + t.false(field.isAList()); + t.false(field.isAMap()); + t.true(field.isASet()); + t.false(field.isASingleValue()); + t.false(field.isOverridable()); + + field = new Field({ + name: 'test', + type: T.UuidType.create(), + useTypeDefault: false, + }); + t.false(field.useTypeDefault); + t.same(field.getDefault(), null); + + field = new Field({ + name: 'test', + type: T.IntType.create(), + min: 5, + max: 10, + }); + t.same(field.getMin(), 5); + t.same(field.getMax(), 10); + + field = new Field({ + name: 'test', + type: T.DecimalType.create(), + precision: 8, + scale: 4, + }); + t.same(field.getPrecision(), 8); + t.same(field.getScale(), 4); + + field = new Field({ + name: 'test', + type: T.StringType.create(), + minLength: 5, + maxLength: 10, + }); + t.same(field.getMinLength(), 5); + t.same(field.getMaxLength(), 10); + + field = new Field({ + name: 'test', + type: T.StringType.create(), + pattern: '/^a-z$/', + }); + const regex = new RegExp('^a-z$'); + t.true(field.getPattern() instanceof RegExp); + t.same(`${field.getPattern()}`, `${regex}`); + + t.end(); +}); + + +test('Field applyDefault(Enum) tests', (t) => { + let field = new Field({ + name: 'test', + type: T.StringEnumType.create(), + classProto: SampleStringEnum, + required: true, + defaultValue: SampleStringEnum.ENUM1.toString(), + }); + t.true(field.defaultValue === SampleStringEnum.ENUM1); + + field = new Field({ + name: 'test', + type: T.StringEnumType.create(), + classProto: SampleStringEnum, + required: true, + defaultValue: SampleStringEnum.ENUM1, + }); + t.true(field.defaultValue === SampleStringEnum.ENUM1); + + field = new Field({ + name: 'test', + type: T.StringEnumType.create(), + classProto: SampleStringEnum, + required: true, + defaultValue: () => SampleStringEnum.ENUM1, + }); + t.true(field.getDefault() === SampleStringEnum.ENUM1); + + t.end(); +}); + + +test('Field getDefault(dynamic) tests', (t) => { + const field = new Field({ + name: 'test', + type: T.StringType.create(), + required: true, + defaultValue: (message, f) => { + const m = message ? message.test : ''; + return `dynamic:${m}:${f.getName()}`; + }, + }); + + t.same(field.getDefault(null), 'dynamic::test'); + + const message = { test: 1 }; + t.same(field.getDefault(message), 'dynamic:1:test'); + + message.test = 2; + t.same(field.getDefault(message), 'dynamic:2:test'); + + t.end(); +}); + + +test('Field assertion tests', (t) => { + const field = new Field({ + name: 'test', + type: T.StringType.create(), + required: true, + assertion: (value) => { + if (value === 'should_fail') { + throw new Error('should_fail is not accepted.'); + } + }, + }); + + try { + field.guardValue(null); + t.fail('required field accepted null'); + } catch (e) { + t.pass(e.message); + } + + try { + field.guardValue('should_fail'); + t.fail('should_fail was accepted.'); + } catch (e) { + t.pass(e.message); + } + + try { + field.guardValue('should_not_fail'); + t.pass('should_not_fail was accepted.'); + } catch (e) { + t.pass(e.message); + } + + t.end(); +}); + + +test('Field guardDefault(A_SINGLE_VALUE) tests', (t) => { + const field = new Field({ name: 'test', type: T.StringType.create(), rule: FieldRule.A_SINGLE_VALUE }); + + try { + field.guardDefault('value'); + t.pass('accepted a valid single value'); + } catch (e) { + t.fail('did not accept a valid single value'); + } + + const invalid = [1, ['not-a-single-value']]; + invalid.forEach((v) => { + try { + field.guardDefault(v); + t.fail('accepted an invalid single value'); + } catch (e) { + t.pass(e.message); + } + }); + + t.end(); +}); + + +test('Field guardDefault(A_LIST) tests', (t) => { + const field = new Field({ name: 'test', type: T.StringType.create(), rule: FieldRule.A_LIST }); + + try { + field.guardDefault(['string', 'test']); + t.pass('accepted a valid list/array'); + } catch (e) { + t.fail('did not accept a valid list/array'); + } + + const invalid = [[1], new Map(), new Set(), 'string']; + invalid.forEach((v) => { + try { + field.guardDefault(v); + t.fail('accepted an invalid list/array'); + } catch (e) { + t.pass(e.message); + } + }); + + t.end(); +}); + + +test('Field guardDefault(A_MAP) tests', (t) => { + const field = new Field({ name: 'test', type: T.StringType.create(), rule: FieldRule.A_MAP }); + + try { + field.guardDefault((new Map()).set('string', 'test')); + t.pass('accepted a valid map'); + } catch (e) { + t.fail('did not accept a valid map'); + } + + const invalid = [ + (new Map()).set('notastring', 1), + new Set(), + ['stringnotinmap'], + 'string', + ]; + + invalid.forEach((v) => { + try { + field.guardDefault(v); + t.fail('accepted an invalid map'); + } catch (e) { + t.pass(e.message); + } + }); + + t.end(); +}); + + +test('Field guardDefault(A_SET) tests', (t) => { + const field = new Field({ name: 'test', type: T.StringType.create(), rule: FieldRule.A_SET }); + + try { + field.guardDefault((new Set()).add('val1').add('val2')); + t.pass('accepted a valid set'); + } catch (e) { + t.fail('did not accept a valid set'); + } + + const invalid = [ + (new Set()).add('val1').add(2), + new Map(), + ['not-a-set'], + 'not-a-set', + ]; + + invalid.forEach((v) => { + try { + field.guardDefault(v); + t.fail('accepted an invalid set'); + } catch (e) { + t.pass(e.message); + } + }); + + t.end(); +}); + diff --git a/tests/FieldBuilder.test.js b/tests/FieldBuilder.test.js new file mode 100644 index 0000000..4036ed6 --- /dev/null +++ b/tests/FieldBuilder.test.js @@ -0,0 +1,79 @@ +import test from 'tape'; +import Fb from '../src/FieldBuilder'; +import Field from '../src/Field'; +import Format from '../src/enums/Format'; +import T from '../src/types'; + +test('FieldBuilder tests', (t) => { + let field = Fb.create('test', T.StringType.create()) + .required() + .format(Format.SLUG) + .withDefault('homer-simpson') + .build(); + + t.true(field instanceof Field, 'field MUST be an instanceOf Field'); + t.same(field.getName(), 'test'); + t.true(field.isRequired()); + t.same(field.getFormat(), Format.SLUG); + t.same(field.getType(), T.StringType.create()); + t.false(field.isAList()); + t.false(field.isAMap()); + t.false(field.isASet()); + t.true(field.isASingleValue()); + t.false(field.isOverridable()); + t.same(field.getDefault(), 'homer-simpson'); + + try { + field.test = 1; + t.fail('field instance is mutable'); + } catch (e) { + t.pass('field instance is immutable'); + } + + field = Fb.create('test', T.DateType.create()).asAList().overridable(true).build(); + t.same(field.getType(), T.DateType.create()); + t.true(field.isAList()); + t.false(field.isAMap()); + t.false(field.isASet()); + t.false(field.isASingleValue()); + t.true(field.isOverridable()); + + field = Fb.create('test', T.DateType.create()).asAMap().build(); + t.same(field.getType(), T.DateType.create()); + t.false(field.isAList()); + t.true(field.isAMap()); + t.false(field.isASet()); + t.false(field.isASingleValue()); + t.false(field.isOverridable()); + + field = Fb.create('test', T.UuidType.create()).asASet().build(); + t.same(field.getType(), T.UuidType.create()); + t.false(field.isAList()); + t.false(field.isAMap()); + t.true(field.isASet()); + t.false(field.isASingleValue()); + t.false(field.isOverridable()); + + field = Fb.create('test', T.UuidType.create()).useTypeDefault(false).build(); + t.false(field.useTypeDefault); + t.same(field.getDefault(), null); + + field = Fb.create('test', T.IntType.create()).min(5).max(10).build(); + t.same(field.getMin(), 5); + t.same(field.getMax(), 10); + + field = Fb.create('test', T.DecimalType.create()).precision(8).scale(4).build(); + t.same(field.getPrecision(), 8); + t.same(field.getScale(), 4); + + field = Fb.create('test', T.StringType.create()).minLength(5).maxLength(10).build(); + t.same(field.getMinLength(), 5); + t.same(field.getMaxLength(), 10); + + field = Fb.create('test', T.StringType.create()).pattern('/^a-z$/').build(); + const regex = new RegExp('^a-z$'); + t.true(field.getPattern() instanceof RegExp); + t.same(`${field.getPattern()}`, `${regex}`); + + t.end(); +}); diff --git a/tests/Message.list.test.js b/tests/Message.list.test.js new file mode 100644 index 0000000..66ff363 --- /dev/null +++ b/tests/Message.list.test.js @@ -0,0 +1,137 @@ +import test from 'tape'; +import SampleMessageV1 from './fixtures/SampleMessageV1'; +import SampleOtherMessageV1 from './fixtures/SampleOtherMessageV1'; + +test('Message string_list tests', (t) => { + const msg = SampleMessageV1.create(); + + t.false(msg.has('string_list')); + t.false(msg.hasClearedField('string_list')); + + msg.addToList('string_list', ['test1', 'test2']); + t.true(msg.has('string_list')); + t.same(msg.get('string_list'), ['test1', 'test2']); + t.same(msg.get('string_list', ['default']), ['test1', 'test2']); + t.same(msg.getFromListAt('string_list', 0), 'test1'); + t.same(msg.getFromListAt('string_list', 1), 'test2'); + t.same(msg.getFromListAt('string_list', 0, 'default'), 'test1'); + t.same(msg.getFromListAt('string_list', 1, 'default'), 'test2'); + t.same(msg.getFromListAt('string_list', 2, 'default'), 'default'); + t.true(msg.isInList('string_list', 'test1')); + t.true(msg.isInList('string_list', 'test2')); + t.false(msg.isInList('string_list', 'test3')); + + msg.addToList('string_list', ['test3']); + t.same(msg.get('string_list'), ['test1', 'test2', 'test3']); + t.same(msg.getFromListAt('string_list', 2, 'default'), 'test3'); + t.true(msg.isInList('string_list', 'test1')); + t.true(msg.isInList('string_list', 'test2')); + t.true(msg.isInList('string_list', 'test3')); + + msg.removeFromListAt('string_list', 0); + t.same(msg.get('string_list'), ['test2', 'test3']); + t.same(msg.getFromListAt('string_list', 0), 'test2'); + t.same(msg.getFromListAt('string_list', 1), 'test3'); + t.false(msg.isInList('string_list', 'test1')); + t.true(msg.isInList('string_list', 'test2')); + t.true(msg.isInList('string_list', 'test3')); + + msg.addToList('string_list', ['test1']); + t.same(msg.get('string_list'), ['test2', 'test3', 'test1']); + t.same(msg.getFromListAt('string_list', 0), 'test2'); + t.same(msg.getFromListAt('string_list', 1), 'test3'); + t.same(msg.getFromListAt('string_list', 2), 'test1'); + t.true(msg.isInList('string_list', 'test1')); + t.true(msg.isInList('string_list', 'test2')); + t.true(msg.isInList('string_list', 'test3')); + + t.same(msg.getFromListAt('string_list', 0), 'test2'); + msg.removeFromListAt('string_list', 0); + t.same(msg.getFromListAt('string_list', 0), 'test3'); + msg.removeFromListAt('string_list', 0); + t.same(msg.getFromListAt('string_list', 0), 'test1'); + msg.removeFromListAt('string_list', 0); + t.true(msg.hasClearedField('string_list')); + t.same(msg.getClearedFields(), ['string_list']); + t.same(msg.get('string_list', ['what']), ['what']); + + msg.addToList('string_list', ['test1', 'test2']); + // ensure we can't modify the internal array + const myList = msg.get('string_list'); + t.false(myList === msg.get('string_list')); + t.same(myList, ['test1', 'test2']); + myList.push('test3'); + t.same(myList, ['test1', 'test2', 'test3']); + t.same(msg.get('string_list'), ['test1', 'test2']); + + t.end(); +}); + + +test('Message message_list tests', (t) => { + const msg = SampleMessageV1.create(); + const otherMsg1 = SampleOtherMessageV1.create().set('test', 'test1'); + const otherMsg2 = SampleOtherMessageV1.create().set('test', 'test2'); + const otherMsg3 = SampleOtherMessageV1.create().set('test', 'test3'); + + t.false(msg.has('message_list')); + t.false(msg.hasClearedField('message_list')); + + msg.addToList('message_list', [otherMsg1, otherMsg2]); + t.true(msg.has('message_list')); + t.same(msg.get('message_list'), [otherMsg1, otherMsg2]); + t.same(msg.get('message_list', ['default']), [otherMsg1, otherMsg2]); + t.same(msg.getFromListAt('message_list', 0), otherMsg1); + t.same(msg.getFromListAt('message_list', 1), otherMsg2); + t.same(msg.getFromListAt('message_list', 0, 'default'), otherMsg1); + t.same(msg.getFromListAt('message_list', 1, 'default'), otherMsg2); + t.same(msg.getFromListAt('message_list', 2, 'default'), 'default'); + t.true(msg.isInList('message_list', otherMsg1)); + t.true(msg.isInList('message_list', otherMsg2)); + t.false(msg.isInList('message_list', otherMsg3)); + + msg.addToList('message_list', [otherMsg3]); + t.same(msg.get('message_list'), [otherMsg1, otherMsg2, otherMsg3]); + t.same(msg.getFromListAt('message_list', 2, 'default'), otherMsg3); + t.true(msg.isInList('message_list', otherMsg1)); + t.true(msg.isInList('message_list', otherMsg2)); + t.true(msg.isInList('message_list', otherMsg3)); + + msg.removeFromListAt('message_list', 0); + t.same(msg.get('message_list'), [otherMsg2, otherMsg3]); + t.same(msg.getFromListAt('message_list', 0), otherMsg2); + t.same(msg.getFromListAt('message_list', 1), otherMsg3); + t.false(msg.isInList('message_list', otherMsg1)); + t.true(msg.isInList('message_list', otherMsg2)); + t.true(msg.isInList('message_list', otherMsg3)); + + msg.addToList('message_list', [otherMsg1]); + t.same(msg.get('message_list'), [otherMsg2, otherMsg3, otherMsg1]); + t.same(msg.getFromListAt('message_list', 0), otherMsg2); + t.same(msg.getFromListAt('message_list', 1), otherMsg3); + t.same(msg.getFromListAt('message_list', 2), otherMsg1); + t.true(msg.isInList('message_list', otherMsg1)); + t.true(msg.isInList('message_list', otherMsg2)); + t.true(msg.isInList('message_list', otherMsg3)); + + t.same(msg.getFromListAt('message_list', 0), otherMsg2); + msg.removeFromListAt('message_list', 0); + t.same(msg.getFromListAt('message_list', 0), otherMsg3); + msg.removeFromListAt('message_list', 0); + t.same(msg.getFromListAt('message_list', 0), otherMsg1); + msg.removeFromListAt('message_list', 0); + t.true(msg.hasClearedField('message_list')); + t.same(msg.getClearedFields(), ['message_list']); + t.same(msg.get('message_list', ['what']), ['what']); + + msg.addToList('message_list', [otherMsg1, otherMsg2]); + // ensure we can't modify the internal array + const myList = msg.get('message_list'); + t.false(myList === msg.get('message_list')); + t.same(myList, [otherMsg1, otherMsg2]); + myList.push(otherMsg3); + t.same(myList, [otherMsg1, otherMsg2, otherMsg3]); + t.same(msg.get('message_list'), [otherMsg1, otherMsg2]); + + t.end(); +}); diff --git a/tests/Message.map.test.js b/tests/Message.map.test.js new file mode 100644 index 0000000..521a87f --- /dev/null +++ b/tests/Message.map.test.js @@ -0,0 +1,160 @@ +import test from 'tape'; +import SampleMessageV1 from './fixtures/SampleMessageV1'; +import SampleOtherMessageV1 from './fixtures/SampleOtherMessageV1'; + +test('Message string_map tests', (t) => { + const msg = SampleMessageV1.create(); + + t.false(msg.has('string_map')); + t.false(msg.hasClearedField('string_map')); + + msg.addToMap('string_map', 'test1', 'val1'); + msg.addToMap('string_map', 'test2', 'val2'); + t.true(msg.has('string_map')); + t.same(msg.get('string_map'), { test1: 'val1', test2: 'val2' }); + t.same(msg.get('string_map', { test1: 'default' }), { test1: 'val1', test2: 'val2' }); + t.same(msg.getFromMap('string_map', 'test1'), 'val1'); + t.same(msg.getFromMap('string_map', 'test2'), 'val2'); + t.true(msg.isInMap('string_map', 'test1')); + t.true(msg.isInMap('string_map', 'test2')); + t.false(msg.isInMap('string_map', 'test3')); + + msg.addToMap('string_map', 'test3', 'val3'); + t.same(msg.get('string_map'), { test1: 'val1', test2: 'val2', test3: 'val3' }); + t.same(msg.getFromMap('string_map', 'test1'), 'val1'); + t.same(msg.getFromMap('string_map', 'test2'), 'val2'); + t.same(msg.getFromMap('string_map', 'test3'), 'val3'); + t.true(msg.isInMap('string_map', 'test1')); + t.true(msg.isInMap('string_map', 'test2')); + t.true(msg.isInMap('string_map', 'test3')); + + msg.removeFromMap('string_map', 'test2'); + t.same(msg.get('string_map'), { test1: 'val1', test3: 'val3' }); + t.true(msg.isInMap('string_map', 'test1')); + t.false(msg.isInMap('string_map', 'test2')); + t.true(msg.isInMap('string_map', 'test3')); + + msg.addToMap('string_map', 'test1', 'newval1'); + t.same(msg.getFromMap('string_map', 'test1'), 'newval1'); + t.same(msg.get('string_map'), { test1: 'newval1', test3: 'val3' }); + t.true(msg.isInMap('string_map', 'test1')); + t.false(msg.isInMap('string_map', 'test2')); + t.true(msg.isInMap('string_map', 'test3')); + + msg.addToMap('string_map', 'test2', 'newval2'); + t.same(msg.getFromMap('string_map', 'test2'), 'newval2'); + t.same(msg.getFromMap('string_map', 'invalid', 'default'), 'default'); + t.same(msg.get('string_map'), { test1: 'newval1', test3: 'val3', test2: 'newval2' }); + t.true(msg.isInMap('string_map', 'test1')); + t.true(msg.isInMap('string_map', 'test2')); + t.true(msg.isInMap('string_map', 'test3')); + + msg.removeFromMap('string_map', 'test1'); + msg.removeFromMap('string_map', 'test2'); + msg.removeFromMap('string_map', 'test3'); + t.true(msg.hasClearedField('string_map')); + t.same(msg.getClearedFields(), ['string_map']); + t.false(msg.isInMap('string_map', 'test1')); + t.false(msg.isInMap('string_map', 'test2')); + t.false(msg.isInMap('string_map', 'test3')); + + msg.addToMap('string_map', 'test1', 'val1'); + t.false(msg.hasClearedField('string_map')); + t.same(msg.getClearedFields(), []); + t.true(msg.isInMap('string_map', 'test1')); + t.false(msg.isInMap('string_map', 'test2')); + t.false(msg.isInMap('string_map', 'test3')); + + msg.clear('string_map'); + t.true(msg.hasClearedField('string_map')); + t.same(msg.getClearedFields(), ['string_map']); + t.false(msg.has('string_map')); + t.same(msg.get('string_map'), null); + t.same(msg.get('string_map', { test: 'what' }), { test: 'what' }); + t.false(msg.isInMap('string_map', 'test1')); + t.false(msg.isInMap('string_map', 'test2')); + t.false(msg.isInMap('string_map', 'test3')); + + t.end(); +}); + + +test('Message message_map tests', (t) => { + const msg = SampleMessageV1.create(); + const otherMsg1 = SampleOtherMessageV1.create().set('test', 'test1'); + const otherMsg2 = SampleOtherMessageV1.create().set('test', 'test2'); + const otherMsg3 = SampleOtherMessageV1.create().set('test', 'test3'); + const otherMsg4 = SampleOtherMessageV1.create().set('test', 'test4'); + + t.false(msg.has('message_map')); + t.false(msg.hasClearedField('message_map')); + + msg.addToMap('message_map', 'test1', otherMsg1); + msg.addToMap('message_map', 'test2', otherMsg2); + t.true(msg.has('message_map')); + t.same(msg.get('message_map'), { test1: otherMsg1, test2: otherMsg2 }); + t.same(msg.get('message_map', { test1: 'default' }), { test1: otherMsg1, test2: otherMsg2 }); + t.same(msg.getFromMap('message_map', 'test1'), otherMsg1); + t.same(msg.getFromMap('message_map', 'test2'), otherMsg2); + t.true(msg.isInMap('message_map', 'test1')); + t.true(msg.isInMap('message_map', 'test2')); + t.false(msg.isInMap('message_map', 'test3')); + + msg.addToMap('message_map', 'test3', otherMsg3); + t.same(msg.get('message_map'), { test1: otherMsg1, test2: otherMsg2, test3: otherMsg3 }); + t.same(msg.getFromMap('message_map', 'test1'), otherMsg1); + t.same(msg.getFromMap('message_map', 'test2'), otherMsg2); + t.same(msg.getFromMap('message_map', 'test3'), otherMsg3); + t.true(msg.isInMap('message_map', 'test1')); + t.true(msg.isInMap('message_map', 'test2')); + t.true(msg.isInMap('message_map', 'test3')); + + msg.removeFromMap('message_map', 'test2'); + t.same(msg.get('message_map'), { test1: otherMsg1, test3: otherMsg3 }); + t.true(msg.isInMap('message_map', 'test1')); + t.false(msg.isInMap('message_map', 'test2')); + t.true(msg.isInMap('message_map', 'test3')); + + msg.addToMap('message_map', 'test1', otherMsg4); + t.same(msg.getFromMap('message_map', 'test1'), otherMsg4); + t.same(msg.get('message_map'), { test1: otherMsg4, test3: otherMsg3 }); + t.true(msg.isInMap('message_map', 'test1')); + t.false(msg.isInMap('message_map', 'test2')); + t.true(msg.isInMap('message_map', 'test3')); + + msg.addToMap('message_map', 'test2', otherMsg4); + t.same(msg.getFromMap('message_map', 'test2'), otherMsg2); + t.same(msg.getFromMap('message_map', 'invalid', 'default'), 'default'); + t.same(msg.get('message_map'), { test1: otherMsg4, test3: otherMsg3, test2: otherMsg4 }); + t.true(msg.isInMap('message_map', 'test1')); + t.true(msg.isInMap('message_map', 'test2')); + t.true(msg.isInMap('message_map', 'test3')); + + msg.removeFromMap('message_map', 'test1'); + msg.removeFromMap('message_map', 'test2'); + msg.removeFromMap('message_map', 'test3'); + t.true(msg.hasClearedField('message_map')); + t.same(msg.getClearedFields(), ['message_map']); + t.false(msg.isInMap('message_map', 'test1')); + t.false(msg.isInMap('message_map', 'test2')); + t.false(msg.isInMap('message_map', 'test3')); + + msg.addToMap('message_map', 'test1', otherMsg1); + t.false(msg.hasClearedField('message_map')); + t.same(msg.getClearedFields(), []); + t.true(msg.isInMap('message_map', 'test1')); + t.false(msg.isInMap('message_map', 'test2')); + t.false(msg.isInMap('message_map', 'test3')); + + msg.clear('message_map'); + t.true(msg.hasClearedField('message_map')); + t.same(msg.getClearedFields(), ['message_map']); + t.false(msg.has('message_map')); + t.same(msg.get('message_map'), null); + t.same(msg.get('message_map', { test: 'what' }), { test: 'what' }); + t.false(msg.isInMap('message_map', 'test1')); + t.false(msg.isInMap('message_map', 'test2')); + t.false(msg.isInMap('message_map', 'test3')); + + t.end(); +}); diff --git a/tests/Message.set.test.js b/tests/Message.set.test.js new file mode 100644 index 0000000..c3a975f --- /dev/null +++ b/tests/Message.set.test.js @@ -0,0 +1,61 @@ +import test from 'tape'; +import SampleMessageV1 from './fixtures/SampleMessageV1'; + +test('Message string_set tests', (t) => { + const msg = SampleMessageV1.create(); + + t.false(msg.has('string_set')); + t.false(msg.hasClearedField('string_set')); + + msg.addToSet('string_set', ['test1', 'test2']); + t.true(msg.has('string_set')); + t.same(msg.get('string_set'), ['test1', 'test2']); + t.same(msg.get('string_set', ['default']), ['test1', 'test2']); + t.true(msg.isInSet('string_set', 'test1')); + t.true(msg.isInSet('string_set', 'test2')); + t.false(msg.isInSet('string_set', 'test3')); + + msg.addToSet('string_set', ['test3']); + t.same(msg.get('string_set'), ['test1', 'test2', 'test3']); + t.true(msg.isInSet('string_set', 'test1')); + t.true(msg.isInSet('string_set', 'test2')); + t.true(msg.isInSet('string_set', 'test3')); + + msg.removeFromSet('string_set', ['test2']); + t.same(msg.get('string_set'), ['test1', 'test3']); + t.true(msg.isInSet('string_set', 'test1')); + t.false(msg.isInSet('string_set', 'test2')); + t.true(msg.isInSet('string_set', 'test3')); + + msg.addToSet('string_set', ['test1']); + t.same(msg.get('string_set'), ['test1', 'test3']); + t.true(msg.isInSet('string_set', 'test1')); + t.false(msg.isInSet('string_set', 'test2')); + t.true(msg.isInSet('string_set', 'test3')); + + msg.removeFromSet('string_set', ['test1', 'test2', 'test3']); + t.true(msg.hasClearedField('string_set')); + t.same(msg.getClearedFields(), ['string_set']); + t.false(msg.isInSet('string_set', 'test1')); + t.false(msg.isInSet('string_set', 'test2')); + t.false(msg.isInSet('string_set', 'test3')); + + msg.addToSet('string_set', ['test1', 'test2']); + t.false(msg.hasClearedField('string_set')); + t.same(msg.getClearedFields(), []); + t.true(msg.isInSet('string_set', 'test1')); + t.true(msg.isInSet('string_set', 'test2')); + t.false(msg.isInSet('string_set', 'test3')); + + msg.clear('string_set'); + t.true(msg.hasClearedField('string_set')); + t.same(msg.getClearedFields(), ['string_set']); + t.false(msg.has('string_set')); + t.same(msg.get('string_set'), null); + t.same(msg.get('string_set', ['default']), ['default']); + t.false(msg.isInSet('string_set', 'test1')); + t.false(msg.isInSet('string_set', 'test2')); + t.false(msg.isInSet('string_set', 'test3')); + + t.end(); +}); diff --git a/tests/Message.single.test.js b/tests/Message.single.test.js new file mode 100644 index 0000000..53540cb --- /dev/null +++ b/tests/Message.single.test.js @@ -0,0 +1,23 @@ +import test from 'tape'; +import SampleMessageV1 from './fixtures/SampleMessageV1'; + +test('Message string_single tests', (t) => { + const msg = SampleMessageV1.create(); + + t.false(msg.has('string_single')); + t.false(msg.hasClearedField('string_single')); + + msg.set('string_single', 'test'); + t.true(msg.has('string_single')); + t.same(msg.get('string_single'), 'test'); + t.same(msg.get('string_single', 'default'), 'test'); + + msg.clear('string_single'); + t.true(msg.hasClearedField('string_single')); + t.same(msg.getClearedFields(), ['string_single']); + t.false(msg.has('string_single')); + t.same(msg.get('string_single'), null); + t.same(msg.get('string_single', 'default'), 'default'); + + t.end(); +}); diff --git a/tests/Message.test.js b/tests/Message.test.js new file mode 100644 index 0000000..979b15f --- /dev/null +++ b/tests/Message.test.js @@ -0,0 +1,154 @@ +import test from 'tape'; +import FrozenMessageIsImmutable from '../src/exceptions/FrozenMessageIsImmutable'; +import Message from '../src/Message'; +import MessageRef from '../src/MessageRef'; +import SchemaId from '../src/SchemaId'; +import SampleMessageV1 from './fixtures/SampleMessageV1'; +import SampleMessageV2 from './fixtures/SampleMessageV2'; +import SampleOtherMessageV1 from './fixtures/SampleOtherMessageV1'; + +test('Message tests', (t) => { + const msg1 = SampleMessageV1.create(); + const msg2 = SampleMessageV2.create(); + + t.true(msg1 instanceof Message, 'msg1 MUST be an instanceOf Message'); + t.true(msg2 instanceof Message, 'msg2 MUST be an instanceOf Message'); + t.true(msg1 instanceof SampleMessageV1, 'msg1 MUST be an instanceOf SampleMessageV1'); + t.true(msg2 instanceof SampleMessageV2, 'msg2 MUST be an instanceOf SampleMessageV2'); + + t.true(SampleMessageV1.schema() === msg1.schema()); + t.true(SampleMessageV2.schema() === msg2.schema()); + t.true(SampleMessageV1.schema().getId() === SchemaId.fromString('pbj:gdbots:pbj.tests::sample-message:1-0-0')); + t.true(SampleMessageV2.schema().getId() === SchemaId.fromString('pbj:gdbots:pbj.tests::sample-message:2-0-0')); + t.true(msg1.schema().getId() === SchemaId.fromString('pbj:gdbots:pbj.tests::sample-message:1-0-0')); + t.true(msg2.schema().getId() === SchemaId.fromString('pbj:gdbots:pbj.tests::sample-message:2-0-0')); + + msg1.set('string_single', '123'); + t.true(msg1.generateMessageRef().equals(MessageRef.fromString('gdbots:pbj.tests::sample-message:123'))); + t.true(msg1.generateMessageRef('tag').equals(MessageRef.fromString('gdbots:pbj.tests::sample-message:123#tag'))); + t.same(msg1.getUriTemplateVars(), { string_single: '123' }); + + t.end(); +}); + + +test('Message generateEtag tests', (t) => { + const msg = SampleMessageV1.create(); + + msg.set('string_single', '123'); + t.same(msg.generateEtag(), '691d8ff26b59e53823ab9190624e34ed'); + t.same(msg.generateEtag(['string_single']), '573915f0765194ff95833311ca5c15c1'); + + msg.set('string_single', ' ice 🍦 poop 💩 doh 😳 '); + t.same(msg.generateEtag(), '2114f330e1ce728c47aad9013f84b07c'); + + msg.set('string_single', '✓ à la mode'); + t.same(msg.generateEtag(), 'ee38a55ab894dbaa551f8870dd9cc4c4'); + + msg.set('string_single', 'foo © bar 𝌆 baz'); + t.same(msg.generateEtag(), 'd6c22aca50a8c30f57e8e40e4d7c400d'); + + msg.clear('string_single'); + t.same(msg.generateEtag(), '3362e2cd5e114f9c9bac62666fc05587'); + t.same(msg.generateEtag(['string_single']), '573915f0765194ff95833311ca5c15c1'); + t.same(msg.generateEtag(['string_single', 'message_map']), '796d3e61906902c815e1bf67685a45d8'); + + t.end(); +}); + + +test('Message freeze tests', (t) => { + let msg = SampleMessageV1.create(); + msg.set('string_single', '123'); + msg.freeze(); + + t.true(msg.isFrozen()); + + try { + msg.set('string_single', 'test'); + t.fail('frozen message is mutable'); + } catch (e) { + t.true(e instanceof FrozenMessageIsImmutable, 'Exception MUST be an instanceOf FrozenMessageIsImmutable'); + t.pass(e.message); + } + + msg = SampleMessageV1.create(); + msg.set('message_single', SampleOtherMessageV1.create().set('test', 'freeze')); + msg.freeze(); + + t.true(msg.isFrozen()); + t.true(msg.get('message_single').isFrozen()); + + try { + msg.get('message_single').set('test', 'test'); + t.fail('nested frozen message is mutable'); + } catch (e) { + t.true(e instanceof FrozenMessageIsImmutable, 'Exception MUST be an instanceOf FrozenMessageIsImmutable'); + t.pass(e.message); + } + + t.end(); +}); + + +test('Message isReplay tests', (t) => { + let msg = SampleMessageV1.create(); + msg.isReplay(true); + t.true(msg.isReplay()); + t.true(msg.isReplay()); + + try { + msg.isReplay(true); + t.fail('isReplay(true) was allowed to be set more than once.'); + } catch (e) { + t.pass(e.message); + } + + msg = SampleMessageV1.create(); + msg.isReplay(false); + t.false(msg.isReplay()); + t.false(msg.isReplay()); + + try { + msg.isReplay(false); + t.fail('isReplay(false) was allowed to be set more than once.'); + } catch (e) { + t.pass(e.message); + } + + msg = SampleMessageV1.create(); + t.false(msg.isReplay()); + t.false(msg.isReplay()); + + try { + msg.isReplay(true); + t.fail('isReplay was allowed to be reset.'); + } catch (e) { + t.pass(e.message); + } + + t.end(); +}); + + +test('Message clone tests', (t) => { + const msg = SampleMessageV1.create(); + msg.set('string_single', '123'); + msg.set('message_single', SampleOtherMessageV1.create().set('test', 'clone')); + msg.freeze(); + + t.true(msg.isFrozen()); + t.true(msg.get('message_single').isFrozen()); + + const msgClone = msg.clone(); + t.false(msg === msgClone); + t.false(msgClone.isFrozen()); + t.false(msgClone.get('message_single').isFrozen()); + t.true(msg.equals(msgClone)); + + msgClone.set('string_single', '456'); + msgClone.get('message_single').set('test', 'clone2'); + t.false(msg.equals(msgClone)); + + t.end(); +}); diff --git a/tests/MessageRef.test.js b/tests/MessageRef.test.js new file mode 100644 index 0000000..2e55a46 --- /dev/null +++ b/tests/MessageRef.test.js @@ -0,0 +1,319 @@ +import test from 'tape'; +import MessageRef from '../src/MessageRef'; +import SchemaCurie from '../src/SchemaCurie'; + +test('MessageRef tests', (t) => { + const curie = SchemaCurie.fromString('acme:blog:node:article'); + const id = '123'; + const tag = null; + const refStr = `${curie}:${id}`; + + const ref = MessageRef.fromString(refStr); + t.true(ref instanceof MessageRef, 'ref MUST be an instanceOf MessageRef'); + t.same(`${ref}`, refStr); + t.same(ref.toString(), refStr); + t.same(ref.valueOf(), refStr); + t.same(ref.toJSON(), { curie: curie.toString(), id }); + t.same(JSON.stringify(ref), `{"curie":"${curie}","id":"${id}"}`); + t.same(ref.getId(), id); + t.same(ref.getTag(), tag); + t.true(ref.hasId()); + t.false(ref.hasTag()); + t.true(ref.equals(new MessageRef(curie, id, tag))); + t.true(ref.equals(MessageRef.fromString(refStr))); + t.true(ref.getCurie() === curie); + + try { + ref.test = 1; + t.fail('ref instance is mutable'); + } catch (e) { + t.pass('ref instance is immutable'); + } + + t.end(); +}); + + +test('MessageRef with empty tag tests', (t) => { + const curie = SchemaCurie.fromString('acme:blog::article'); + const id = '123'; + const tag = ''; + const refStr = `${curie}:${id}`; + + const ref = new MessageRef(curie, id, tag); + t.true(ref instanceof MessageRef, 'ref MUST be an instanceOf MessageRef'); + t.same(`${ref}`, refStr); + t.same(ref.toString(), refStr); + t.same(ref.valueOf(), refStr); + t.same(ref.toJSON(), { curie: curie.toString(), id }); + t.same(JSON.stringify(ref), `{"curie":"${curie}","id":"${id}"}`); + t.same(ref.getId(), id); + t.same(ref.getTag(), null); + t.true(ref.hasId()); + t.false(ref.hasTag()); + t.true(ref.equals(new MessageRef(curie, id, tag))); + t.true(ref.equals(MessageRef.fromString(refStr))); + t.true(ref.getCurie() === curie); + + try { + ref.test = 1; + t.fail('ref instance is mutable'); + } catch (e) { + t.pass('ref instance is immutable'); + } + + t.end(); +}); + + +test('MessageRef with tag tests', (t) => { + const curie = SchemaCurie.fromString('acme:blog:node:article'); + const id = '123'; + const tag = 'tag'; + const refStr = `${curie}:${id}#${tag}`; + + const ref = MessageRef.fromString(refStr); + t.true(ref instanceof MessageRef, 'ref MUST be an instanceOf MessageRef'); + t.same(`${ref}`, refStr); + t.same(ref.toString(), refStr); + t.same(ref.valueOf(), refStr); + t.same(ref.toJSON(), { curie: curie.toString(), id, tag }); + t.same(JSON.stringify(ref), `{"curie":"${curie}","id":"${id}","tag":"${tag}"}`); + t.same(ref.getId(), id); + t.same(ref.getTag(), tag); + t.true(ref.hasId()); + t.true(ref.hasTag()); + t.true(ref.equals(new MessageRef(curie, id, tag))); + t.true(ref.equals(MessageRef.fromString(refStr))); + t.true(ref.getCurie() === curie); + + try { + ref.test = 1; + t.fail('ref instance is mutable'); + } catch (e) { + t.pass('ref instance is immutable'); + } + + t.end(); +}); + + +test('MessageRef fromJSON tests', (t) => { + const valid = [ + { + input: '{"curie":"acme:blog:node:article","id":"123","tag":"tag"}', + output: { + curie: 'acme:blog:node:article', + id: '123', + tag: 'tag', + }, + }, + { + input: '{"curie":"acme:blog::article","id":"123","tag":"tag"}', + output: { + curie: 'acme:blog::article', + id: '123', + tag: 'tag', + }, + }, + { + input: '{"curie":"acme:blog::article","id":"123"}', + output: { + curie: 'acme:blog::article', + id: '123', + }, + }, + { + input: '{"curie":"acme:blog:node:article","id":"2015/12/25/test","tag":"tag"}', + output: { + curie: 'acme:blog:node:article', + id: '2015/12/25/test', + tag: 'tag', + }, + }, + { + input: '{"curie":"acme:blog:node:article","id":"2015/12/25/test"}', + output: { + curie: 'acme:blog:node:article', + id: '2015/12/25/test', + }, + }, + { + input: '{"curie":"acme:blog::article","id":"2015/12/25/test","tag":"tag"}', + output: { + curie: 'acme:blog::article', + id: '2015/12/25/test', + tag: 'tag', + }, + }, + { + input: '{"curie":"acme:blog::article","id":"2015/12/25/test"}', + output: { + curie: 'acme:blog::article', + id: '2015/12/25/test', + }, + }, + { + input: '{"curie":"acme:blog::article","id":"2015/12/25/test:still:the:id"}', + output: { + curie: 'acme:blog::article', + id: '2015/12/25/test:still:the:id', + }, + }, + { + input: '{"curie":"acme:blog::article","id":"2015/12/25/test:Still_The:id","tag":"2015.Q4"}', + output: { + curie: 'acme:blog::article', + id: '2015/12/25/test:Still_The:id', + tag: '2015.q4', + }, + }, + ]; + + valid.forEach((sample) => { + try { + const ref1 = MessageRef.fromJSON(sample.input); + const ref2 = MessageRef.fromJSON(sample.input); + t.same(`${ref1}`, `${ref2}`); + t.same(ref1.toJSON(), sample.output); + t.true(ref1.getCurie() === SchemaCurie.fromString(sample.output.curie)); + t.true(ref1.getCurie().toString() === sample.output.curie); + t.same(ref1.getId(), sample.output.id); + t.same(ref1.getTag(), sample.output.tag || null); + t.same(ref1.hasTag(), !!sample.output.tag); + } catch (e) { + t.fail(e.message); + } + }); + + t.end(); +}); + + +test('MessageRef fromString tests', (t) => { + const valid = [ + { + input: 'acme:blog:node:article:123#tag', + output: { + curie: 'acme:blog:node:article', + id: '123', + tag: 'tag', + }, + }, + { + input: 'acme:blog::article:123#tag', + output: { + curie: 'acme:blog::article', + id: '123', + tag: 'tag', + }, + }, + { + input: 'acme:blog::article:123', + output: { + curie: 'acme:blog::article', + id: '123', + }, + }, + { + input: 'acme:blog:node:article:2015/12/25/test#tag', + output: { + curie: 'acme:blog:node:article', + id: '2015/12/25/test', + tag: 'tag', + }, + }, + { + input: 'acme:blog:node:article:2015/12/25/test', + output: { + curie: 'acme:blog:node:article', + id: '2015/12/25/test', + }, + }, + { + input: 'acme:blog::article:2015/12/25/test#tag', + output: { + curie: 'acme:blog::article', + id: '2015/12/25/test', + tag: 'tag', + }, + }, + { + input: 'acme:blog::article:2015/12/25/test', + output: { + curie: 'acme:blog::article', + id: '2015/12/25/test', + }, + }, + { + input: 'acme:blog::article:2015/12/25/test:still:the:id', + output: { + curie: 'acme:blog::article', + id: '2015/12/25/test:still:the:id', + }, + }, + ]; + + valid.forEach((sample) => { + try { + const ref1 = MessageRef.fromString(sample.input); + const ref2 = MessageRef.fromString(sample.input); + t.same(`${ref1}`, `${ref2}`); + t.same(ref1.toJSON(), sample.output); + t.true(ref1.getCurie() === SchemaCurie.fromString(sample.output.curie)); + t.true(ref1.getCurie().toString() === sample.output.curie); + t.same(ref1.getId(), sample.output.id); + t.same(ref1.getTag(), sample.output.tag || null); + t.same(ref1.hasTag(), !!sample.output.tag); + } catch (e) { + t.fail(e.message); + } + }); + + t.end(); +}); + + +test('MessageRef fromString(invalid) tests', (t) => { + const invalid = [ + 'test::what', + 'test::', + 'test:::', + ':test', + 'john@doe.com', + '#hashtag', + 'http://www.what.com/', + 'test.value:2015/01/01/test:what', + 'cool~topic', + 'some:thin!@##$%$%&^^&**()-=+', + 'some:test%20', + 'ACME:blog:node:article:1:2:3:4#tag', + 'ACME:blog:node:article#tag', + 'ACME:blog:node:', + 'ACME:blog::', + 'ACME:::', + 'acme:blog:node:', + 'acme:blog::', + 'acme:::', + 'acme:::#tag', + ' : ', + ' : : : #tag', + null, + false, + true, + {}, + [], + NaN, + ]; + + invalid.forEach((str) => { + try { + const ref = MessageRef.fromString(str); + t.fail(`MessageRef [${ref}] created with invalid format [${JSON.stringify(str)}].`); + } catch (e) { + t.pass(e.message); + } + }); + + t.end(); +}); diff --git a/tests/MessageResolver.test.js b/tests/MessageResolver.test.js new file mode 100644 index 0000000..89ea8a6 --- /dev/null +++ b/tests/MessageResolver.test.js @@ -0,0 +1,137 @@ +import test from 'tape'; +import MoreThanOneMessageForMixin from '../src/exceptions/MoreThanOneMessageForMixin'; +import NoMessageForCurie from '../src/exceptions/NoMessageForCurie'; +import NoMessageForMixin from '../src/exceptions/NoMessageForMixin'; +import NoMessageForQName from '../src/exceptions/NoMessageForQName'; +import NoMessageForSchemaId from '../src/exceptions/NoMessageForSchemaId'; +import MessageResolver from '../src/MessageResolver'; +import SchemaCurie from '../src/SchemaCurie'; +import SchemaId from '../src/SchemaId'; +import SchemaQName from '../src/SchemaQName'; +import SampleMessageV1 from './fixtures/SampleMessageV1'; +import SampleMessageV2 from './fixtures/SampleMessageV2'; +import SampleOtherMessageV1 from './fixtures/SampleOtherMessageV1'; +import SampleMixinV1 from './fixtures/SampleMixinV1'; +import SampleMixinV2 from './fixtures/SampleMixinV2'; +import SampleUnusedMixinV1 from './fixtures/SampleUnusedMixinV1'; + +test('MessageResolver all tests', (t) => { + const all = MessageResolver.all(); + + t.same(all.length, 3); + t.true(all.includes(SampleMessageV1)); + t.true(all.includes(SampleMessageV2)); + t.true(all.includes(SampleOtherMessageV1)); + + t.end(); +}); + + +test('MessageResolver resolveId tests', (t) => { + const message = MessageResolver.resolveId(SchemaId.fromString('pbj:gdbots:pbj.tests::sample-message:1-0-0')); + t.true(message === SampleMessageV1); + + try { + MessageResolver.resolveId(SchemaId.fromString('pbj:gdbots:pbj.tests::invalid-message:1-0-0')); + t.fail('resolved invalid SchemaId'); + } catch (e) { + t.true(e instanceof NoMessageForSchemaId, 'Exception MUST be an instanceOf NoMessageForSchemaId'); + t.pass(e.message); + } + + t.end(); +}); + + +test('MessageResolver resolveCurie tests', (t) => { + let message = MessageResolver.resolveCurie(SchemaCurie.fromString('gdbots:pbj.tests::sample-message')); + t.true(message === SampleMessageV2); + + message = MessageResolver.resolveCurie(SchemaCurie.fromString('gdbots:pbj.tests::sample-other-message')); + t.true(message === SampleOtherMessageV1); + + try { + MessageResolver.resolveCurie(SchemaCurie.fromString('gdbots:pbj.tests::invalid-message')); + t.fail('resolved invalid SchemaCurie'); + } catch (e) { + t.true(e instanceof NoMessageForCurie, 'Exception MUST be an instanceOf NoMessageForCurie'); + t.pass(e.message); + } + + t.end(); +}); + + +test('MessageResolver resolveQName tests', (t) => { + let curie = MessageResolver.resolveQName(SchemaQName.fromString('gdbots:sample-message')); + t.true(curie === SchemaCurie.fromString('gdbots:pbj.tests::sample-message')); + + curie = MessageResolver.resolveQName(SchemaQName.fromString('gdbots:sample-other-message')); + t.true(curie === SchemaCurie.fromString('gdbots:pbj.tests::sample-other-message')); + + try { + MessageResolver.resolveQName(SchemaQName.fromString('gdbots:invalid-message')); + t.fail('resolved invalid SchemaQName'); + } catch (e) { + t.true(e instanceof NoMessageForQName, 'Exception MUST be an instanceOf NoMessageForQName'); + t.pass(e.message); + } + + t.end(); +}); + + +test('MessageResolver findOneUsingMixin tests', (t) => { + const mixin = SampleMixinV2.create(); + const schema = MessageResolver.findOneUsingMixin(mixin); + + t.same(schema.getId(), SchemaId.fromString('pbj:gdbots:pbj.tests::sample-message:2-0-0')); + t.true(schema.createMessage() instanceof SampleMessageV2); + + try { + MessageResolver.findOneUsingMixin(SampleUnusedMixinV1.create()); + t.fail('findOneUsingMixin found schema for invalid mixin'); + } catch (e) { + t.true(e instanceof NoMessageForMixin, 'Exception MUST be an instanceOf NoMessageForMixin'); + t.pass(e.message); + } + + try { + MessageResolver.findOneUsingMixin(SampleMixinV1.create()); + t.fail('findOneUsingMixin found one schema for mixin used more than once'); + } catch (e) { + t.true(e instanceof MoreThanOneMessageForMixin, 'Exception MUST be an instanceOf MoreThanOneMessageForMixin'); + t.pass(e.message); + } + + t.end(); +}); + + +test('MessageResolver findAllUsingMixin tests', (t) => { + let mixin = SampleMixinV1.create(); + let schemas = MessageResolver.findAllUsingMixin(mixin); + + t.same(2, schemas.length); + t.same(schemas[0].getId(), SchemaId.fromString('pbj:gdbots:pbj.tests::sample-message:1-0-0')); + t.same(schemas[1].getId(), SchemaId.fromString('pbj:gdbots:pbj.tests::sample-other-message:1-0-0')); + t.true(schemas[0].createMessage() instanceof SampleMessageV1); + t.true(schemas[1].createMessage() instanceof SampleOtherMessageV1); + + mixin = SampleMixinV2.create(); + schemas = MessageResolver.findAllUsingMixin(mixin); + + t.same(1, schemas.length); + t.same(schemas[0].getId(), SchemaId.fromString('pbj:gdbots:pbj.tests::sample-message:2-0-0')); + t.true(schemas[0].createMessage() instanceof SampleMessageV2); + + try { + MessageResolver.findAllUsingMixin(SampleUnusedMixinV1.create()); + t.fail('findOneUsingMixin found schema for invalid mixin'); + } catch (e) { + t.true(e instanceof NoMessageForMixin, 'Exception MUST be an instanceOf NoMessageForMixin'); + t.pass(e.message); + } + + t.end(); +}); diff --git a/tests/Mixin.test.js b/tests/Mixin.test.js new file mode 100644 index 0000000..adb211f --- /dev/null +++ b/tests/Mixin.test.js @@ -0,0 +1,23 @@ +import test from 'tape'; +import Mixin from '../src/Mixin'; +import SchemaId from '../src/SchemaId'; +import SampleMixinV1 from './fixtures/SampleMixinV1'; + +test('Mixin tests', (t) => { + const mixin = SampleMixinV1.create(); + + t.true(mixin instanceof Mixin, 'mixin MUST be an instanceOf Mixin'); + t.true(mixin instanceof SampleMixinV1, 'mixin MUST be an instanceOf SampleMixinV1'); + t.true(mixin === SampleMixinV1.create(), 'SampleMixinV1.create() must return the same instance'); + t.true(mixin.getId() === SchemaId.fromString('pbj:gdbots:pbj.tests::sample-mixin:1-0-0')); + t.same(2, mixin.getFields().length); + + try { + mixin.test = 1; + t.fail('mixin instance is mutable'); + } catch (e) { + t.pass('mixin instance is immutable'); + } + + t.end(); +}); diff --git a/tests/Schema.test.js b/tests/Schema.test.js new file mode 100644 index 0000000..2dcd9cd --- /dev/null +++ b/tests/Schema.test.js @@ -0,0 +1,173 @@ +import test from 'tape'; +import FieldAlreadyDefined from '../src/exceptions/FieldAlreadyDefined'; +import FieldNotDefined from '../src/exceptions/FieldNotDefined'; +import FieldOverrideNotCompatible from '../src/exceptions/FieldOverrideNotCompatible'; +import MixinAlreadyAdded from '../src/exceptions/MixinAlreadyAdded'; +import MixinNotDefined from '../src/exceptions/MixinNotDefined'; +import Fb from '../src/FieldBuilder'; +import Schema from '../src/Schema'; +import SchemaId from '../src/SchemaId'; +import T from '../src/types'; +// import TypeName from '../src/enums/TypeName'; +import SampleMessageV1 from './fixtures/SampleMessageV1'; +import SampleMixinV1 from './fixtures/SampleMixinV1'; +import SampleMixinV2 from './fixtures/SampleMixinV2'; + +test('Schema tests', (t) => { + const schema = SampleMessageV1.schema(); + const mixinId = SampleMixinV1.create().getId(); + + t.true(schema instanceof Schema, 'schema MUST be an instanceOf Schema'); + t.true(schema.getId() === SchemaId.fromString('pbj:gdbots:pbj.tests::sample-message:1-0-0')); + t.same(`${schema}`, schema.getId().toString()); + t.same(`${schema.getCurie()}`, 'gdbots:pbj.tests::sample-message'); + t.same(`${schema.getCurieMajor()}`, 'gdbots:pbj.tests::sample-message:v1'); + t.same(`${schema.getQName()}`, 'gdbots:sample-message'); + t.same(schema.getFields().length, 10, 'schema should have 10 fields'); + t.same(schema.getClassName(), 'SampleMessageV1'); + t.same(schema.getHandlerMethodName(), 'sampleMessage'); + t.same(schema.getHandlerMethodName(true), 'sampleMessageV1'); + t.true(schema.hasMixin(mixinId.getCurieMajor())); + t.true(schema.getMixin(mixinId.getCurieMajor()), SampleMixinV1.create()); + t.true(schema.hasMixin(mixinId.getCurie().toString())); + t.true(schema.getMixin(mixinId.getCurie().toString()), SampleMixinV1.create()); + t.same(schema.getMixins(), [SampleMixinV1.create()]); + t.same(schema.getMixinIds(), [mixinId.getCurieMajor()]); + t.same(schema.getMixinCuries(), [mixinId.getCurie().toString()]); + t.same(schema.getRequiredFields()[0].getName(), '_schema'); + + // TypeName.getKeys() + ['string'].forEach((typeName) => { + ['single', 'set', 'list', 'map'].forEach((rule) => { + const fieldName = `${typeName.toLowerCase()}_${rule}`; + + if (rule === 'set' && !schema.hasField(fieldName)) { + return; + } + + t.true(schema.hasField(fieldName), `schema MUST have field [${fieldName}]`); + t.same(schema.getField(fieldName).getType().getTypeName().getName(), typeName.toUpperCase()); + }); + }); + + try { + schema.getField('invalid_field'); + t.fail('schema.getField("invalid_field") should have thrown FieldNotDefined'); + } catch (e) { + t.true(e instanceof FieldNotDefined, 'Exception MUST be an instanceOf FieldNotDefined'); + t.pass(e.message); + } + + try { + schema.test = 1; + t.fail('schema instance is mutable'); + } catch (e) { + t.pass('schema instance is immutable'); + } + + t.end(); +}); + + +test('Schema overridable tests', (t) => { + let schema = new Schema('pbj:gdbots:pbj.tests::sample-message:1-0-0', SampleMessageV1, + [Fb.create('mixin_string', T.StringType.create()).withDefault('homer').build()], + [SampleMixinV1.create()], + ); + + const fields = schema.getFields(); + t.same(fields[0].getName(), '_schema'); + t.same(fields[1].getName(), 'mixin_string'); + t.same(fields[2].getName(), 'mixin_int'); + t.same(schema.getField('mixin_string').getDefault(), 'homer'); + + try { + schema = new Schema('pbj:gdbots:pbj.tests::sample-message:1-0-0', SampleMessageV1, + [Fb.create('mixin_string', T.IntType.create()).build()], + [SampleMixinV1.create()], + ); + t.fail('schema allowed invalid override (type mismatch)'); + } catch (e) { + t.true(e instanceof FieldOverrideNotCompatible, 'Exception MUST be an instanceOf FieldOverrideNotCompatible'); + t.pass(e.message); + } + + try { + schema = new Schema('pbj:gdbots:pbj.tests::sample-message:1-0-0', SampleMessageV1, + [Fb.create('mixin_string', T.StringType.create()).required().build()], + [SampleMixinV1.create()], + ); + t.fail('schema allowed invalid override (original optional, override required)'); + } catch (e) { + t.true(e instanceof FieldOverrideNotCompatible, 'Exception MUST be an instanceOf FieldOverrideNotCompatible'); + t.pass(e.message); + } + + try { + schema = new Schema('pbj:gdbots:pbj.tests::sample-message:1-0-0', SampleMessageV1, + [Fb.create('mixin_string', T.StringType.create()).asAMap().build()], + [SampleMixinV1.create()], + ); + t.fail('schema allowed invalid override (original single, override map)'); + } catch (e) { + t.true(e instanceof FieldOverrideNotCompatible, 'Exception MUST be an instanceOf FieldOverrideNotCompatible'); + t.pass(e.message); + } + + try { + schema = new Schema('pbj:gdbots:pbj.tests::sample-message:1-0-0', SampleMessageV1, + [Fb.create('mixin_int', T.IntType.create()).build()], + [SampleMixinV1.create()], + ); + t.fail('schema allowed invalid override (not overridable)'); + } catch (e) { + t.true(e instanceof FieldAlreadyDefined, 'Exception MUST be an instanceOf FieldAlreadyDefined'); + t.pass(e.message); + } + + t.end(); +}); + + +test('Schema mixin tests', (t) => { + let schema = new Schema('pbj:gdbots:pbj.tests::sample-message:1-0-0', SampleMessageV1, [], + [SampleMixinV1.create()], + ); + + try { + schema.getMixin('invalid_mixin'); + t.fail('schema.getMixin("invalid_mixin") should have thrown MixinNotDefined'); + } catch (e) { + t.true(e instanceof MixinNotDefined, 'Exception MUST be an instanceOf MixinNotDefined'); + t.pass(e.message); + } + + try { + schema = new Schema('pbj:gdbots:pbj.tests::sample-message:1-0-0', SampleMessageV1, [], + [ + SampleMixinV1.create(), + SampleMixinV1.create(), + ], + ); + t.fail('schema allowed same mixin twice'); + } catch (e) { + t.true(e instanceof MixinAlreadyAdded, 'Exception MUST be an instanceOf MixinAlreadyAdded'); + t.pass(e.message); + } + + try { + schema = new Schema('pbj:gdbots:pbj.tests::sample-message:1-0-0', SampleMessageV1, [], + [ + SampleMixinV1.create(), + SampleMixinV2.create(), + ], + ); + t.fail('schema allowed same mixin (by curie) twice'); + } catch (e) { + t.true(e instanceof MixinAlreadyAdded, 'Exception MUST be an instanceOf MixinAlreadyAdded'); + t.pass(e.message); + } + + t.end(); +}); + diff --git a/tests/SchemaCurie.test.js b/tests/SchemaCurie.test.js new file mode 100644 index 0000000..f54b488 --- /dev/null +++ b/tests/SchemaCurie.test.js @@ -0,0 +1,91 @@ +import test from 'tape'; +import SchemaCurie from '../src/SchemaCurie'; +import SchemaQName from '../src/SchemaQName'; + +test('SchemaCurie tests', (t) => { + const valid = [ + 'acme:blog:node:article', + 'acme:blog::article', + 'acme:blog.v1::article', + 'acme:blog.v1:node:article', + 'acme-widgets:web.v1::article', + 'acme-widgets:web.v1:node:article', + ]; + valid.forEach((str) => { + try { + const [vendor, pkg, category, message] = str.split(':'); + const curie1 = new SchemaCurie(vendor, pkg, category, message); + const curie2 = SchemaCurie.fromString(str); + const qname = SchemaQName.fromString(`${vendor}:${message}`); + t.same(`${curie1}`, `${curie2}`); + t.true(curie1 instanceof SchemaCurie, 'curie1 MUST be an instanceOf SchemaCurie'); + t.true(curie2 instanceof SchemaCurie, 'curie2 MUST be an instanceOf SchemaCurie'); + t.same(curie1.toString(), str); + t.same(curie1.valueOf(), str); + t.same(curie1.toJSON(), str); + t.same(`${curie1}`, str); + t.same(JSON.stringify(curie1), `"${str}"`); + t.same(curie1.getVendor(), vendor); + t.same(curie1.getPackage(), pkg); + t.same(curie1.getCategory(), category || null); + t.same(curie1.getMessage(), message); + t.true(curie1.equals(curie2)); + t.true(curie2.equals(curie1)); + t.true(curie1 === curie2); + t.true(curie1.getQName() === qname); + t.true(curie2.getQName() === qname); + + try { + curie1.test = 1; + t.fail('curie1 instance is mutable'); + } catch (e) { + t.pass('curie1 instance is immutable'); + } + } catch (e) { + t.fail(e.message); + } + }); + + const invalid = [ + `acme:blog:node:article${'x'.repeat(124)}`, + 'test::what', + 'test::', + 'test:::', + ':test', + 'john@doe.com', + '#hashtag', + 'http://www.what.com/', + 'test.value:2015/01/01/test:what', + 'cool~topic', + 'some:thin!@##$%$%&^^&**()-=+', + 'some:test%20', + 'ACME:blog:node:article:1:2:3:4#tag', + 'ACME:blog:node:article#tag', + 'ACME:blog:node:', + 'ACME:blog::', + 'ACME:::', + 'acme:blog:node:', + 'acme:blog::', + 'acme:::', + 'acme:::', + ' : ', + ' : : : ', + ':', + null, + false, + true, + {}, + [], + NaN, + ]; + invalid.forEach((str) => { + try { + const curie = SchemaCurie.fromString(str); + t.fail(`SchemaCurie [${curie}] created with invalid format [${JSON.stringify(str)}].`); + } catch (e) { + t.pass(e.message); + } + }); + + t.end(); +}); diff --git a/tests/SchemaId.test.js b/tests/SchemaId.test.js new file mode 100644 index 0000000..fde0237 --- /dev/null +++ b/tests/SchemaId.test.js @@ -0,0 +1,98 @@ +import test from 'tape'; +import SchemaId from '../src/SchemaId'; +import SchemaCurie from '../src/SchemaCurie'; +import SchemaQName from '../src/SchemaQName'; + +test('SchemaId tests', (t) => { + const valid = [ + 'pbj:acme:blog:node:article:1-2-3', + 'pbj:acme:blog::article:1-2-3', + 'pbj:acme:blog.v1::article:1-2-3', + 'pbj:acme:blog.v1:node:article:1-2-3', + 'pbj:acme-widgets:web.v1::article:1-2-3', + 'pbj:acme-widgets:web.v1:node:article:1-2-3', + ]; + valid.forEach((str) => { + try { + const [vendor, pkg, category, message, version] = str.substr(4).split(':'); + const id1 = new SchemaId(vendor, pkg, category, message, version); + const id2 = SchemaId.fromString(str); + const curie = SchemaCurie.fromString(`${vendor}:${pkg}:${category}:${message}`); + const qname = SchemaQName.fromString(`${vendor}:${message}`); + + t.same(`${id1}`, `${id2}`); + t.true(id1 instanceof SchemaId, 'id1 MUST be an instanceOf SchemaId'); + t.true(id2 instanceof SchemaId, 'id2 MUST be an instanceOf SchemaId'); + t.same(id1.toString(), str); + t.same(id1.valueOf(), str); + t.same(id1.toJSON(), str); + t.same(`${id1}`, str); + t.same(JSON.stringify(id1), `"${str}"`); + + t.same(id1.getVendor(), vendor); + t.same(id1.getPackage(), pkg); + t.same(id1.getCategory(), category || null); + t.same(id1.getMessage(), message); + t.true(id1.getCurie() === curie); + t.true(id1.getQName() === qname); + t.same(id1.getVersion().toString(), version); + t.same(id1.getCurieMajor(), `${curie}:v${id1.getVersion().getMajor()}`); + + t.true(id1.equals(id2)); + t.true(id2.equals(id1)); + t.true(id1 === id2); + + try { + id1.test = 1; + t.fail('id1 instance is mutable'); + } catch (e) { + t.pass('id1 instance is immutable'); + } + } catch (e) { + t.fail(e.message); + } + }); + + const invalid = [ + `pbj:acme:blog:node:article${'x'.repeat(124)}:1-2-3`, + 'test::what', + 'test::', + 'test:::', + ':test', + 'john@doe.com', + '#hashtag', + 'http://www.what.com/', + 'test.value:2015/01/01/test:what', + 'cool~topic', + 'some:thin!@##$%$%&^^&**()-=+', + 'some:test%20', + 'ACME:blog:node:article:1:2:3:4#tag', + 'ACME:blog:node:article#tag', + 'ACME:blog:node:', + 'ACME:blog::', + 'ACME:::', + 'pbj:acme:blog:node:', + 'pbj:acme:blog::', + 'pbj:acme:::', + 'pbj:acme:::', + ' : ', + ' : : : ', + ':', + null, + false, + true, + {}, + [], + NaN, + ]; + invalid.forEach((str) => { + try { + const id = SchemaId.fromString(str); + t.fail(`SchemaId [${id}] created with invalid format [${JSON.stringify(str)}].`); + } catch (e) { + t.pass(e.message); + } + }); + + t.end(); +}); diff --git a/tests/SchemaQName.test.js b/tests/SchemaQName.test.js new file mode 100644 index 0000000..143e9d6 --- /dev/null +++ b/tests/SchemaQName.test.js @@ -0,0 +1,101 @@ +import test from 'tape'; +import SchemaCurie from '../src/SchemaCurie'; +import SchemaId from '../src/SchemaId'; +import SchemaQName from '../src/SchemaQName'; +import InvalidSchemaQName from '../src/exceptions/InvalidSchemaQName'; + +test('SchemaQName tests', (t) => { + const valid = ['acme:article', 'youtube:video', 'acme-widgets:widget-thing']; + valid.forEach((qname) => { + try { + const [vendor, message] = qname.split(':'); + const schemaQName = SchemaQName.fromString(qname); + const schemaQName2 = new SchemaQName(vendor, message); + t.same(`${schemaQName}`, `${schemaQName2}`); + t.true(schemaQName instanceof SchemaQName, 'schemaQName MUST be an instanceOf SchemaQName'); + t.same(schemaQName.toString(), qname); + t.same(schemaQName.valueOf(), qname); + t.same(schemaQName.toJSON(), qname); + t.same(`${schemaQName}`, qname); + t.same(JSON.stringify(schemaQName), `"${qname}"`); + t.same(schemaQName.getVendor(), vendor); + t.same(schemaQName.getMessage(), message); + + try { + schemaQName.test = 1; + t.fail('schemaQName instance is mutable'); + } catch (e) { + t.pass('schemaQName instance is immutable'); + } + } catch (e) { + t.fail(e.message); + } + }); + + const invalid = [ + 'Not A qname', + 'acme.widgets:widget', + ' acme:widget ', + ':', + ' : ', + ' ', + 1, + 0, + '', + null, + false, + true, + {}, + [], + NaN, + ]; + invalid.forEach((qname) => { + try { + const schemaQName = SchemaQName.fromString(qname); + t.fail(`SchemaQName [${schemaQName}] created with invalid value [${JSON.stringify(qname)}].`); + } catch (e) { + t.true(e instanceof InvalidSchemaQName, 'Exception MUST be an instanceOf InvalidSchemaQName'); + t.pass(e.message); + } + }); + + t.end(); +}); + + +test('SchemaQName instance tests', (t) => { + const instance1 = SchemaQName.fromString('acme:article'); + const instance2 = SchemaQName.fromString('acme:article'); + t.same(instance1, instance2); + + try { + instance1.test = 1; + t.fail('SchemaQName instance1 is mutable'); + } catch (e) { + t.pass('SchemaQName instance1 is immutable'); + } + + try { + instance2.test = 1; + t.fail('SchemaQName instance2 is mutable'); + } catch (e) { + t.pass('SchemaQName instance2 is immutable'); + } + + t.end(); +}); + + +test('SchemaQName fromCurie tests', (t) => { + const curie = SchemaCurie.fromString('acme:blog:node:article'); + const qname = SchemaQName.fromCurie(curie); + t.same(qname.toString(), 'acme:article'); + t.end(); +}); + + +test('SchemaQName fromId tests', (t) => { + const qname = SchemaQName.fromId(SchemaId.fromString('pbj:acme:blog:node:article:1-2-3')); + t.same(qname.toString(), 'acme:article'); + t.end(); +}); diff --git a/tests/SchemaVersion.test.js b/tests/SchemaVersion.test.js new file mode 100644 index 0000000..f6e0ebd --- /dev/null +++ b/tests/SchemaVersion.test.js @@ -0,0 +1,48 @@ +import test from 'tape'; +import SchemaVersion from '../src/SchemaVersion'; +import InvalidSchemaVersion from '../src/exceptions/InvalidSchemaVersion'; + +test('SchemaVersion tests', (t) => { + const valid = ['1-0-0', '1-1-1', '2-20-0', '300-4000-5000']; + valid.forEach((version) => { + try { + const [major, minor, patch] = version.split('-').map(Number); + const schemaVersion = SchemaVersion.fromString(version); + const schemaVersion2 = new SchemaVersion(major, minor, patch); + t.same(`${schemaVersion}`, `${schemaVersion2}`); + t.true(schemaVersion instanceof SchemaVersion, 'schemaVersion MUST be an instanceOf SchemaVersion'); + t.same(schemaVersion.toString(), version); + t.same(schemaVersion.valueOf(), version); + t.same(schemaVersion.toJSON(), version); + t.same(`${schemaVersion}`, version); + t.same(JSON.stringify(schemaVersion), `"${version}"`); + t.same(schemaVersion.getMajor(), major); + t.same(schemaVersion.getMinor(), minor); + t.same(schemaVersion.getPatch(), patch); + + try { + schemaVersion.test = 1; + t.fail('schemaVersion instance is mutable'); + } catch (e) { + t.pass('schemaVersion instance is immutable'); + } + } catch (e) { + t.fail(e.message); + } + }); + + const invalid = [ + 'Not A version', '1-0-0.1', '1.0.0', '1-1-1-dev', ' 1-0-0 ', 1, 0, '', null, false, true, {}, [], NaN, + ]; + invalid.forEach((version) => { + try { + const schemaVersion = SchemaVersion.fromString(version); + t.fail(`SchemaVersion [${schemaVersion}] created with invalid version [${JSON.stringify(version)}].`); + } catch (e) { + t.true(e instanceof InvalidSchemaVersion, 'Exception MUST be an instanceOf InvalidSchemaVersion'); + t.pass(e.message); + } + }); + + t.end(); +}); diff --git a/tests/add-types-test.js b/tests/add-types-test.js deleted file mode 100644 index 4a89ea4..0000000 --- a/tests/add-types-test.js +++ /dev/null @@ -1,239 +0,0 @@ -'use strict'; - -import IntEnum from './fixtures/enum/int-enum'; -import Priority from './fixtures/enum/priority'; -import Provider from './fixtures/enum/provider'; -import StringEnum from './fixtures/enum/string-enum'; -import MapsMessage from './fixtures/maps-message'; -import EmailMessage from './fixtures/email-message'; -import NestedMessage from './fixtures/nested-message'; -import ArrayUtils from 'gdbots/common/util/array-utils'; -import StringUtils from 'gdbots/common/util/string-utils'; -import BigNumber from 'gdbots/pbj/well-known/big-number'; -import GeoPoint from 'gdbots/pbj/well-known/geo-point'; -import DynamicField from 'gdbots/pbj/well-known/dynamic-field'; -import Microtime from 'gdbots/pbj/well-known/microtime'; -import TimeUuidIdentifier from 'gdbots/pbj/well-known/time-uuid-identifier'; -import UuidIdentifier from 'gdbots/pbj/well-known/uuid-identifier'; -import MessageRef from 'gdbots/pbj/message-ref'; - -describe('add-types-test', function() { - it('should add invalid types', function(done) { - let message = MapsMessage.create(); - - ArrayUtils.each(getInvalidTypeValues(), function(v, k) { - let thrown = false; - - try { - if (Array.isArray(v)) { - message.addToMap(k, 'test1', v[0]); - message.addToMap(k, 'test2', v[1]); - } else { - message.addToMap(k, 'test1', v); - } - } catch (e) { - thrown = true; - } - - if (!thrown) { - if (Array.isArray(v)) { - console.log('[' + k + '] accepted an invalid [' + StringUtils.varToString(v[0]) + '] value.'); - console.log('[' + k + '] accepted an invalid [' + StringUtils.varToString(v[1]) + '] value.'); - } else { - console.log('[' + k + '] accepted an invalid [' + StringUtils.varToString(v) + '] value.'); - } - } - }); - - done(); - }); - - it('should add invalid type to map', function(done) { - let shouldWork = MapsMessage.create(); - let shouldFail = Object.assign({}, shouldWork); - - /* - * some int types won't fail because they're all ints of course, just different ranges. - * e.g. an Int is also all other unsigned ints (except BigInt but that's a class so we're fine) - */ - let allInts = [ - 'TinyInt', - 'SmallInt', - 'MediumInt', - 'Int', - 'SignedTinyInt', - 'SignedSmallInt', - 'SignedMediumInt', - 'SignedInt', - 'Timestamp' - ]; - - let allStrings = ['Binary', 'Blob', 'MediumBlob', 'MediumText', 'String', 'Text']; - - ArrayUtils.each(shouldWork.constructor.getAllTypes(), function(type) { - ArrayUtils.each(getTypeValues(), function(v, k) { - let thrown = false; - - if (type == k) { - if (Array.isArray(v)) { - shouldWork.addToMap(type, 'test1', v[0]); - shouldWork.addToMap(type, 'test2', v[1]); - } else { - shouldWork.addToMap(type, 'test1', v); - } - - return; - } - - try { - if (Array.isArray(v)) { - shouldFail.addToMap(type, 'test1', v[0]); - shouldFail.addToMap(type, 'test2', v[1]); - } else { - shouldFail.addToMap(type, 'test1', v); - } - - switch (type) { - case 'Binary': - case 'Blob': - case 'MediumBlob': - case 'MediumText': - case 'String': - case 'Text': - case 'Timestamp': - if (allStrings.indexOf(k) >= 0) { - return; - } - break; - - case 'Decimal': - if (k === 'Float') { - return; - } - break; - - case 'Date': - if (k === 'DateTime') { - return; - } - break; - - case 'DateTime': - if (k === 'Date') { - return; - } - break; - - case 'Float': - if (k === 'Decimal') { - return; - } - break; - - case 'Identifier': - if (['TimeUuid', 'Uuid'].indexOf(k) >= 0) { - return; - } - break; - - case 'Uuid': - if (['Identifier', 'TimeUuid'].indexOf(k) >= 0) { - return; - } - break; - - default: - // do nothing - } - - if (type.name === 'IntType' && allInts.indexOf(k) >= 0) { - return; - } - } catch (e) { - thrown = true; - } - - if (!thrown) { - if (Array.isArray(v)) { - console.log('[' + type + '] accepted an invalid/mismatched [' + StringUtils.varToString(v[0]) + '] value.'); - console.log('[' + type + '] accepted an invalid/mismatched [' + StringUtils.varToString(v[1]) + '] value.'); - } else { - console.log('[' + type + '] accepted an invalid/mismatched [' + StringUtils.varToString(v) + '] value.'); - } - } - }); - }); - - done(); - }); -}); - -function getTypeValues() { - return { - 'BigInt': [new BigNumber(0), new BigNumber('18446744073709551615')], - 'Binary': 'aG9tZXIgc2ltcHNvbg==', - 'Blob': 'aG9tZXIgc2ltcHNvbg==', - 'Boolean': [false, true], - 'Date': new Date(), - 'DateTime': new Date(), - 'Decimal': 3.14, - 'DynamicField': DynamicField.createIntVal('int_val', 1), - 'Float': 13213.032468, - 'GeoPoint': new GeoPoint(0.5, 102.0), - 'IntEnum': IntEnum.UNKNOWN.getValue(), - 'Int': [0, 4294967295], - 'MediumInt': [0, 16777215], - 'MediumBlob': 'aG9tZXIgc2ltcHNvbg==', - 'MediumText': 'medium text', - 'Message': NestedMessage.create(), - 'MessageRef': new MessageRef(NestedMessage.schema().getCurie(), UuidIdentifier.generate()), - 'Microtime': Microtime.create(), - 'SignedBigInt': [new BigNumber('-9223372036854775808'), new BigNumber('9223372036854775807')], - 'SignedMediumInt': [-8388608, 8388607], - 'SignedSmallInt': [-32768, 32767], - 'SignedTinyInt': [-128, 127], - 'SmallInt': [0, 65535], - 'StringEnum': StringEnum.UNKNOWN.getValue(), - 'String': 'string', - 'Text': 'text', - 'TimeUuid': TimeUuidIdentifier.generate(), - 'Timestamp': Math.floor(new Date().getTime() / 1000), - 'TinyInt': [0, 255], - 'Uuid': UuidIdentifier.generate(), - }; -} - -function getInvalidTypeValues() { - return { - 'BigInt': [new BigNumber(-1), new BigNumber('18446744073709551616')], - 'Binary': false, - 'Blob': false, - 'Boolean': 'not_a_bool', - 'Date': 'not_a_date', - 'DateTime': 'not_a_date', - 'Decimal': 1, - 'DynamicField': 'not_a_dynamic_field', - 'Float': 1, - 'GeoPoint': 'not_a_geo_point', - 'IntEnum': Priority.NORMAL.getValue(), // not the correct enum - 'Int': [-1, 4294967296], - 'MediumInt': [-1, 16777216], - 'MediumBlob': false, - 'MediumText': false, - 'Message': EmailMessage.create(), // not the correct message - 'MessageRef': 'not_a_message_ref', - 'Microtime': new Date().getTime(), - 'SignedBigInt': [new BigNumber('-9223372036854775809'), new BigNumber('9223372036854775808')], - 'SignedMediumInt': [-8388609, 8388608], - 'SignedSmallInt': [-32769, 32768], - 'SignedTinyInt': [-129, 128], - 'SmallInt': [-1, 65536], - 'StringEnum': Provider.AOL.getValue(), // not the correct enum - 'String': false, - 'Text': false, - 'TimeUuid': 'not_a_time_uuid', - 'Timestamp': 'not_a_timestamp', - 'TinyInt': [-1, 256], - 'Uuid': 'not_a_uuid', - }; -} diff --git a/tests/babel-register.js b/tests/babel-register.js new file mode 100644 index 0000000..06cc9b0 --- /dev/null +++ b/tests/babel-register.js @@ -0,0 +1,3 @@ +require('babel-register')({ + ignore: /node_modules\/(?!@gdbots|lodash-es)/, +}); diff --git a/tests/bootstrap.js b/tests/bootstrap.js deleted file mode 100644 index 025dfeb..0000000 --- a/tests/bootstrap.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -// disable babel cache. -process.env.BABEL_DISABLE_CACHE = 1; - -require('babel-register')({ - ignore: /node_modules(?![\/]@gdbots)/, - - plugins: [ - ['module-alias', [ - { src: './src', expose: 'gdbots/pbj' }, - { src: 'npm:@gdbots/common/src', expose: 'gdbots/common' } - ]] - ] -}); - -require('chai').should(); -global.expect = require('chai'); diff --git a/tests/fixtures/SampleMessageV1.js b/tests/fixtures/SampleMessageV1.js new file mode 100644 index 0000000..235a466 --- /dev/null +++ b/tests/fixtures/SampleMessageV1.js @@ -0,0 +1,53 @@ +/* eslint-disable class-methods-use-this */ +import Fb from '../../src/FieldBuilder'; +import Message from '../../src/Message'; +import MessageResolver from '../../src/MessageResolver'; +import Schema from '../../src/Schema'; +import T from '../../src/types'; +import SampleMixinV1 from './SampleMixinV1'; +import SampleTraitV1 from './SampleTraitV1'; + +export default class SampleMessageV1 extends Message { + /** + * @private + * + * @returns {Schema} + */ + static defineSchema() { + return new Schema('pbj:gdbots:pbj.tests::sample-message:1-0-0', SampleMessageV1, + [ + Fb.create('string_single', T.StringType.create()) + .build(), + Fb.create('string_set', T.StringType.create()) + .asASet() + .build(), + Fb.create('string_list', T.StringType.create()) + .asAList() + .build(), + Fb.create('string_map', T.StringType.create()) + .asAMap() + .build(), + + Fb.create('message_single', T.MessageType.create()) + .anyOfCuries(['gdbots:pbj.tests::sample-other-message']) + .build(), + Fb.create('message_list', T.MessageType.create()) + .asAList() + .anyOfCuries(['gdbots:pbj.tests::sample-other-message']) + .build(), + Fb.create('message_map', T.MessageType.create()) + .asAMap() + .anyOfCuries(['gdbots:pbj.tests::sample-other-message']) + .build(), + ], + [ + SampleMixinV1.create(), + ], + ); + } +} + +SampleTraitV1(SampleMessageV1); +MessageResolver.register('gdbots:pbj.tests::sample-message:v1', SampleMessageV1); +Object.freeze(SampleMessageV1); +Object.freeze(SampleMessageV1.prototype); diff --git a/tests/fixtures/SampleMessageV2.js b/tests/fixtures/SampleMessageV2.js new file mode 100644 index 0000000..eb97516 --- /dev/null +++ b/tests/fixtures/SampleMessageV2.js @@ -0,0 +1,41 @@ +/* eslint-disable class-methods-use-this */ +import Fb from '../../src/FieldBuilder'; +import Message from '../../src/Message'; +import MessageResolver from '../../src/MessageResolver'; +import Schema from '../../src/Schema'; +import T from '../../src/types'; +import SampleMixinV2 from './SampleMixinV2'; +import SampleTraitV2 from './SampleTraitV2'; + +export default class SampleMessageV2 extends Message { + /** + * @private + * + * @returns {Schema} + */ + static defineSchema() { + return new Schema('pbj:gdbots:pbj.tests::sample-message:2-0-0', SampleMessageV2, + [ + Fb.create('string_single', T.StringType.create()) + .build(), + Fb.create('string_set', T.StringType.create()) + .asASet() + .build(), + Fb.create('string_list', T.StringType.create()) + .asAList() + .build(), + Fb.create('string_map', T.StringType.create()) + .asAMap() + .build(), + ], + [ + SampleMixinV2.create(), + ], + ); + } +} + +SampleTraitV2(SampleMessageV2); +MessageResolver.register('gdbots:pbj.tests::sample-message', SampleMessageV2); +Object.freeze(SampleMessageV2); +Object.freeze(SampleMessageV2.prototype); diff --git a/tests/fixtures/SampleMixinV1.js b/tests/fixtures/SampleMixinV1.js new file mode 100644 index 0000000..0a75641 --- /dev/null +++ b/tests/fixtures/SampleMixinV1.js @@ -0,0 +1,28 @@ +/* eslint-disable class-methods-use-this */ +import Fb from '../../src/FieldBuilder'; +import Mixin from '../../src/Mixin'; +import SchemaId from '../../src/SchemaId'; +import T from '../../src/types'; + +export default class SampleMixinV1 extends Mixin { + /** + * @returns {SchemaId} + */ + getId() { + return SchemaId.fromString('pbj:gdbots:pbj.tests::sample-mixin:1-0-0'); + } + + /** + * @returns {Field[]} + */ + getFields() { + return [ + Fb.create('mixin_string', T.StringType.create()) + .overridable(true) + .build(), + Fb.create('mixin_int', T.IntType.create()) + .required() + .build(), + ]; + } +} diff --git a/tests/fixtures/SampleMixinV2.js b/tests/fixtures/SampleMixinV2.js new file mode 100644 index 0000000..22af59d --- /dev/null +++ b/tests/fixtures/SampleMixinV2.js @@ -0,0 +1,30 @@ +/* eslint-disable class-methods-use-this */ +import Fb from '../../src/FieldBuilder'; +import Mixin from '../../src/Mixin'; +import SchemaId from '../../src/SchemaId'; +import T from '../../src/types'; + +export default class SampleMixinV2 extends Mixin { + /** + * @returns {SchemaId} + */ + getId() { + return SchemaId.fromString('pbj:gdbots:pbj.tests::sample-mixin:2-0-0'); + } + + /** + * @returns {Field[]} + */ + getFields() { + return [ + Fb.create('mixin_string', T.StringType.create()) + .overridable(true) + .build(), + Fb.create('mixin_int', T.IntType.create()) + .required() + .build(), + Fb.create('mixin_date', T.DateType.create()) + .build(), + ]; + } +} diff --git a/tests/fixtures/SampleOtherMessageV1.js b/tests/fixtures/SampleOtherMessageV1.js new file mode 100644 index 0000000..3411fbb --- /dev/null +++ b/tests/fixtures/SampleOtherMessageV1.js @@ -0,0 +1,32 @@ +/* eslint-disable class-methods-use-this */ +import Fb from '../../src/FieldBuilder'; +import Message from '../../src/Message'; +import MessageResolver from '../../src/MessageResolver'; +import Schema from '../../src/Schema'; +import T from '../../src/types'; +import SampleMixinV1 from './SampleMixinV1'; +import SampleTraitV1 from './SampleTraitV1'; + +export default class SampleOtherMessageV1 extends Message { + /** + * @private + * + * @returns {Schema} + */ + static defineSchema() { + return new Schema('pbj:gdbots:pbj.tests::sample-other-message:1-0-0', SampleOtherMessageV1, + [ + Fb.create('test', T.StringType.create()) + .build(), + ], + [ + SampleMixinV1.create(), + ], + ); + } +} + +SampleTraitV1(SampleOtherMessageV1); +MessageResolver.register('gdbots:pbj.tests::sample-other-message', SampleOtherMessageV1); +Object.freeze(SampleOtherMessageV1); +Object.freeze(SampleOtherMessageV1.prototype); diff --git a/tests/fixtures/SampleTraitV1.js b/tests/fixtures/SampleTraitV1.js new file mode 100644 index 0000000..65fe8ba --- /dev/null +++ b/tests/fixtures/SampleTraitV1.js @@ -0,0 +1,24 @@ +/* eslint-disable class-methods-use-this, no-param-reassign */ +import MessageRef from '../../src/MessageRef'; + +export default function SampleTraitV1(m) { + return Object.assign(m.prototype, { + /** + * @param {?string} tag + * + * @returns {MessageRef} + */ + generateMessageRef(tag = null) { + return new MessageRef(this.schema().getCurie(), this.get('string_single'), tag); + }, + + /** + * @returns {Object} + */ + getUriTemplateVars() { + return { + string_single: this.get('string_single'), + }; + }, + }); +} diff --git a/tests/fixtures/SampleTraitV2.js b/tests/fixtures/SampleTraitV2.js new file mode 100644 index 0000000..13aa7d3 --- /dev/null +++ b/tests/fixtures/SampleTraitV2.js @@ -0,0 +1,24 @@ +/* eslint-disable class-methods-use-this, no-param-reassign */ +import MessageRef from '../../src/MessageRef'; + +export default function SampleTraitV2(m) { + return Object.assign(m.prototype, { + /** + * @param {?string} tag + * + * @returns {MessageRef} + */ + generateMessageRef(tag = null) { + return new MessageRef(this.schema().getCurie(), this.get('string_single'), tag); + }, + + /** + * @returns {Object} + */ + getUriTemplateVars() { + return { + string_single: this.get('string_single'), + }; + }, + }); +} diff --git a/tests/fixtures/SampleUnusedMixinV1.js b/tests/fixtures/SampleUnusedMixinV1.js new file mode 100644 index 0000000..e1b722d --- /dev/null +++ b/tests/fixtures/SampleUnusedMixinV1.js @@ -0,0 +1,12 @@ +/* eslint-disable class-methods-use-this */ +import Mixin from '../../src/Mixin'; +import SchemaId from '../../src/SchemaId'; + +export default class SampleUnusedMixinV1 extends Mixin { + /** + * @returns {SchemaId} + */ + getId() { + return SchemaId.fromString('pbj:gdbots:pbj.tests::sample-unused-mixin:1-0-0'); + } +} diff --git a/tests/fixtures/email-message.js b/tests/fixtures/email-message.js deleted file mode 100644 index db44f8c..0000000 --- a/tests/fixtures/email-message.js +++ /dev/null @@ -1,106 +0,0 @@ -'use strict'; - -import Priority from './enum/priority'; -import Provider from './enum/provider'; -import NestedMessage from './nested-message'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import Format from 'gdbots/pbj/enum/format'; -import BooleanType from 'gdbots/pbj/type/boolean-type'; -import DateTimeType from 'gdbots/pbj/type/date-time-type'; -import DynamicFieldType from 'gdbots/pbj/type/dynamic-field-type'; -import IntEnumType from 'gdbots/pbj/type/int-enum-type'; -import MessageType from 'gdbots/pbj/type/message-type'; -import MicrotimeType from 'gdbots/pbj/type/microtime-type'; -import StringType from 'gdbots/pbj/type/string-type'; -import StringEnumType from 'gdbots/pbj/type/string-enum-type'; -import TimeUuidType from 'gdbots/pbj/type/time-uuid-type'; -import Fb from 'gdbots/pbj/field-builder'; -import MessageRef from 'gdbots/pbj/message-ref'; -import MessageResolver from 'gdbots/pbj/message-resolver'; -import Message from 'gdbots/pbj/message'; -import Schema from 'gdbots/pbj/schema'; - -export default class EmailMessage extends SystemUtils.mixinClass(Message) -{ - /** - * @return Schema - */ - static defineSchema() { - let schema = new Schema('pbj:gdbots:tests.pbj:fixtures:email-message:1-0-0', this.name, - [ - Fb.create('id', TimeUuidType.create()) - .required() - .build(), - Fb.create('from_name', StringType.create()) - .build(), - Fb.create('from_email', StringType.create()) - .required() - .format('email') - .build(), - Fb.create('subject', StringType.create()) - .withDefault(function (message = null) { - if (!message) { - return null; - } - return message.get('labels', []).join(',') + ' test'; - }) - .build(), - Fb.create('body', StringType.create()).build(), - Fb.create('priority', IntEnumType.create()) - .required() - .instance(Priority) - .withDefault(Priority.NORMAL) - .build(), - Fb.create('sent', BooleanType.create()).build(), - Fb.create('date_sent', DateTimeType.create()).build(), - Fb.create('microtime_sent', MicrotimeType.create()).build(), - Fb.create('provider', StringEnumType.create()) - .instance(Provider) - .withDefault(Provider.GMAIL) - .build(), - Fb.create('labels', StringType.create()) - .format(Format.HASHTAG.getValue()) - .asASet() - .build(), - Fb.create('nested', MessageType.create()) - .instance(NestedMessage) - .build(), - Fb.create('enum_in_set', StringEnumType.create()) - .instance(Provider) - .asASet() - .build(), - Fb.create('enum_in_list', StringEnumType.create()) - .instance(Provider) - .asAList() - .build(), - Fb.create('any_of_message', MessageType.create()) - .instance(Message) - .asAList() - .build(), - Fb.create('dynamic_fields', DynamicFieldType.create()) - .asAList() - .build(), - ] - ); - - MessageResolver.registerSchema(this, schema); - - return schema; - } - - /** - * {@inheritdoc} - */ - generateMessageRef(tag = null) { - return new MessageRef(this.constructor.schema().getCurie(), this.get('id'), tag); - } - - /** - * {@inheritdoc} - */ - getUriTemplateVars() { - return { - 'id': this.get('id').toString() - }; - } -} diff --git a/tests/fixtures/enum/int-enum.js b/tests/fixtures/enum/int-enum.js deleted file mode 100644 index d8dcbd7..0000000 --- a/tests/fixtures/enum/int-enum.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -import Enum from 'gdbots/common/enum'; -import SystemUtils from 'gdbots/common/util/system-utils'; - -/** - * @method static IntEnum UNKNOWN() - * @method static IntEnum A_INT() - */ -export default class IntEnum extends SystemUtils.mixinClass(Enum) {} - -IntEnum.initEnum({ - UNKNOWN: 0, - A_INT: 1 -}); diff --git a/tests/fixtures/enum/priority.js b/tests/fixtures/enum/priority.js deleted file mode 100644 index d1d63e0..0000000 --- a/tests/fixtures/enum/priority.js +++ /dev/null @@ -1,17 +0,0 @@ -'use strict'; - -import Enum from 'gdbots/common/enum'; -import SystemUtils from 'gdbots/common/util/system-utils'; - -/** - * @method static Priority NORMAL() - * @method static Priority HIGH() - * @method static Priority LOW() - */ -export default class Priority extends SystemUtils.mixinClass(Enum) {} - -Priority.initEnum({ - NORMAL: 1, - HIGH: 2, - LOW: 3 -}); diff --git a/tests/fixtures/enum/provider.js b/tests/fixtures/enum/provider.js deleted file mode 100644 index 40904c0..0000000 --- a/tests/fixtures/enum/provider.js +++ /dev/null @@ -1,17 +0,0 @@ -'use strict'; - -import Enum from 'gdbots/common/enum'; -import SystemUtils from 'gdbots/common/util/system-utils'; - -/** - * @method static Provider AOL() - * @method static Provider GMAIL() - * @method static Provider HOTMAIL() - */ -export default class Provider extends SystemUtils.mixinClass(Enum) {} - -Provider.initEnum({ - AOL: 'aol', - GMAIL: 'gmail', - HOTMAIL: 'hotmail' -}); diff --git a/tests/fixtures/enum/string-enum.js b/tests/fixtures/enum/string-enum.js deleted file mode 100644 index 30a6dfd..0000000 --- a/tests/fixtures/enum/string-enum.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -import Enum from 'gdbots/common/enum'; -import SystemUtils from 'gdbots/common/util/system-utils'; - -/** - * @method static StringEnum UNKNOWN() - * @method static StringEnum A_STRING() - */ -export default class StringEnum extends SystemUtils.mixinClass(Enum) {} - -StringEnum.initEnum({ - UNKNOWN: 'unknown', - A_STRING: 'string' -}); diff --git a/tests/fixtures/enums/SampleIntEnum.js b/tests/fixtures/enums/SampleIntEnum.js new file mode 100644 index 0000000..e63a885 --- /dev/null +++ b/tests/fixtures/enums/SampleIntEnum.js @@ -0,0 +1,10 @@ +import Enum from '@gdbots/common/Enum'; + +export default class SampleIntEnum extends Enum { +} + +SampleIntEnum.configure({ + UNKNOWN: 0, + ENUM1: 1, + ENUM2: 2, +}, 'gdbots:pbj.tests:sample-int-enum'); diff --git a/tests/fixtures/enums/SampleStringEnum.js b/tests/fixtures/enums/SampleStringEnum.js new file mode 100644 index 0000000..7321e98 --- /dev/null +++ b/tests/fixtures/enums/SampleStringEnum.js @@ -0,0 +1,10 @@ +import Enum from '@gdbots/common/Enum'; + +export default class SampleStringEnum extends Enum { +} + +SampleStringEnum.configure({ + UNKNOWN: 'unknown', + ENUM1: 'val1', + ENUM2: 'val2', +}, 'gdbots:pbj.tests:sample-string-enum'); diff --git a/tests/fixtures/maps-message.js b/tests/fixtures/maps-message.js deleted file mode 100644 index f348a62..0000000 --- a/tests/fixtures/maps-message.js +++ /dev/null @@ -1,136 +0,0 @@ -'use strict'; - -import NestedMessage from './nested-message'; -import StringEnum from './enum/string-enum'; -import IntEnum from './enum/int-enum'; -import StringUtils from 'gdbots/common/util/string-utils'; -import SystemUtils from 'gdbots/common/util/system-utils'; -import TimeUuidIdentifier from 'gdbots/pbj/well-known/time-uuid-identifier'; -import Fb from 'gdbots/pbj/field-builder'; -import MessageRef from 'gdbots/pbj/message-ref'; -import MessageResolver from 'gdbots/pbj/message-resolver'; -import Message from 'gdbots/pbj/message'; -import Schema from 'gdbots/pbj/schema'; -import BigIntType from 'gdbots/pbj/type/big-int-type'; -import BinaryType from 'gdbots/pbj/type/binary-type'; -import BlobType from 'gdbots/pbj/type/blob-type'; -import BooleanType from 'gdbots/pbj/type/boolean-type'; -import DateTimeType from 'gdbots/pbj/type/date-time-type'; -import DateType from 'gdbots/pbj/type/date-type'; -import DecimalType from 'gdbots/pbj/type/decimal-type'; -import FloatType from 'gdbots/pbj/type/float-type'; -import GeoPointType from 'gdbots/pbj/type/geo-point-type'; -import IdentifierType from 'gdbots/pbj/type/identifier-type'; -import IntEnumType from 'gdbots/pbj/type/int-enum-type'; -import IntType from 'gdbots/pbj/type/int-type'; -import MediumBlobType from 'gdbots/pbj/type/medium-blob-type'; -import MediumIntType from 'gdbots/pbj/type/medium-int-type'; -import MediumTextType from 'gdbots/pbj/type/medium-text-type'; -import MessageRefType from 'gdbots/pbj/type/message-ref-type'; -import MessageType from 'gdbots/pbj/type/message-type'; -import MicrotimeType from 'gdbots/pbj/type/microtime-type'; -import SignedBigIntType from 'gdbots/pbj/type/signed-big-int-type'; -import SignedIntType from 'gdbots/pbj/type/signed-int-type'; -import SignedMediumIntType from 'gdbots/pbj/type/signed-medium-int-type'; -import SignedSmallIntType from 'gdbots/pbj/type/signed-small-int-type'; -import SignedTinyIntType from 'gdbots/pbj/type/signed-tiny-int-type'; -import SmallIntType from 'gdbots/pbj/type/small-int-type'; -import StringEnumType from 'gdbots/pbj/type/string-enum-type'; -import StringType from 'gdbots/pbj/type/string-type'; -import TextType from 'gdbots/pbj/type/text-type'; -import TimeUuidType from 'gdbots/pbj/type/time-uuid-type'; -import TimestampType from 'gdbots/pbj/type/timestamp-type'; -import TinyIntType from 'gdbots/pbj/type/tiny-int-type'; -import UuidType from 'gdbots/pbj/type/uuid-type'; - -export default class MapsMessage extends SystemUtils.mixinClass(Message) -{ - /** - * @return array - */ - static getAllTypes() { - return [ - BigIntType, BinaryType, BlobType, BooleanType, DateTimeType, DateType, DecimalType, - FloatType, GeoPointType, IdentifierType, IntEnumType, IntType, MediumBlobType, MediumIntType, - MediumTextType, MessageRefType, MessageType, MicrotimeType, SignedBigIntType, SignedIntType, - SignedMediumIntType, SignedSmallIntType, SignedTinyIntType, SmallIntType, StringEnumType, - StringType, TextType, TimeUuidType, TimestampType, TinyIntType, UuidType - ]; - } - - /** - * @return Schema - */ - static defineSchema() { - let fields = []; - - /** @var Type type */ - for (let type of this.getAllTypes()) { - let typeName = StringUtils.toSnakeCase(type.name.substring(0, type.name.length-4)).toLowerCase(); - let field = null; - - switch (typeName) { - case 'identifier': - field = Fb.create(typeName, type.create()) - .asAMap() - .instance(TimeUuidIdentifier) - .build(); - - break; - - case 'int_enum': - field = Fb.create(typeName, type.create()) - .asAMap() - .instance(IntEnum) - .build(); - - break; - - case 'string_enum': - field = Fb.create(typeName, type.create()) - .asAMap() - .instance(StringEnum) - .build(); - - break; - - case 'message': - field = Fb.create(typeName, type.create()) - .asAMap() - .instance(NestedMessage) - .build(); - - break; - - default: - field = Fb.create(typeName, type.create()) - .asAMap() - .build(); - } - - if (field) { - fields.push(field); - } - } - - let schema = new Schema('pbj:gdbots:tests.pbj:fixtures:maps-message:1-0-0', this.name, fields); - - MessageResolver.registerSchema(this, schema); - - return schema; - } - - /** - * {@inheritdoc} - */ - generateMessageRef(tag = null) { - return new MessageRef(this.constructor.schema().getCurie(), null, tag); - } - - /** - * {@inheritdoc} - */ - getUriTemplateVars() { - return {}; - } -} diff --git a/tests/fixtures/nested-message.js b/tests/fixtures/nested-message.js deleted file mode 100644 index 852c441..0000000 --- a/tests/fixtures/nested-message.js +++ /dev/null @@ -1,47 +0,0 @@ -'use strict'; - -import SystemUtils from 'gdbots/common/util/system-utils'; -import GeoPointType from 'gdbots/pbj/type/geo-point-type'; -import IntType from 'gdbots/pbj/type/int-type'; -import MessageRefType from 'gdbots/pbj/type/message-ref-type'; -import StringType from 'gdbots/pbj/type/string-type'; -import Fb from 'gdbots/pbj/field-builder'; -import MessageRef from 'gdbots/pbj/message-ref'; -import MessageResolver from 'gdbots/pbj/message-resolver'; -import Message from 'gdbots/pbj/message'; -import Schema from 'gdbots/pbj/schema'; - -export default class NestedMessage extends SystemUtils.mixinClass(Message) -{ - /** - * @return Schema - */ - static defineSchema() { - let schema = new Schema('pbj:gdbots:tests.pbj:fixtures:nested-message:1-0-0', this.name, - [ - Fb.create('test1', StringType.create()).build(), - Fb.create('test2', IntType.create()).asASet().build(), - Fb.create('location', GeoPointType.create()).build(), - Fb.create('refs', MessageRefType.create()).asASet().build() - ] - ); - - MessageResolver.registerSchema(this, schema); - - return schema; - } - - /** - * {@inheritdoc} - */ - generateMessageRef(tag = null) { - return new MessageRef(this.constructor.schema().getCurie(), null, tag); - } - - /** - * {@inheritdoc} - */ - getUriTemplateVars() { - return {}; - } -} diff --git a/tests/fixtures/well-known/SampleDatedSlugIdentifier.js b/tests/fixtures/well-known/SampleDatedSlugIdentifier.js new file mode 100644 index 0000000..6516af3 --- /dev/null +++ b/tests/fixtures/well-known/SampleDatedSlugIdentifier.js @@ -0,0 +1,4 @@ +import DatedSlugIdentifier from '../../../src/well-known/DatedSlugIdentifier'; + +export default class SampleDatedSlugIdentifier extends DatedSlugIdentifier { +} diff --git a/tests/fixtures/well-known/SampleSlugIdentifier.js b/tests/fixtures/well-known/SampleSlugIdentifier.js new file mode 100644 index 0000000..d6bb0d5 --- /dev/null +++ b/tests/fixtures/well-known/SampleSlugIdentifier.js @@ -0,0 +1,4 @@ +import SlugIdentifier from '../../../src/well-known/SlugIdentifier'; + +export default class SampleSlugIdentifier extends SlugIdentifier { +} diff --git a/tests/fixtures/well-known/SampleTimeUuidIdentifier.js b/tests/fixtures/well-known/SampleTimeUuidIdentifier.js new file mode 100644 index 0000000..8619a38 --- /dev/null +++ b/tests/fixtures/well-known/SampleTimeUuidIdentifier.js @@ -0,0 +1,4 @@ +import TimeUuidIdentifier from '../../../src/well-known/TimeUuidIdentifier'; + +export default class SampleTimeUuidIdentifier extends TimeUuidIdentifier { +} diff --git a/tests/fixtures/well-known/SampleUuidIdentifier.js b/tests/fixtures/well-known/SampleUuidIdentifier.js new file mode 100644 index 0000000..bb46132 --- /dev/null +++ b/tests/fixtures/well-known/SampleUuidIdentifier.js @@ -0,0 +1,4 @@ +import UuidIdentifier from '../../../src/well-known/UuidIdentifier'; + +export default class SampleUuidIdentifier extends UuidIdentifier { +} diff --git a/tests/index.test.js b/tests/index.test.js new file mode 100644 index 0000000..de361b2 --- /dev/null +++ b/tests/index.test.js @@ -0,0 +1,5 @@ +import test from 'tape'; + +test('index tests', (t) => { + t.end(); +}); diff --git a/tests/maps-test.js b/tests/maps-test.js deleted file mode 100644 index f16445e..0000000 --- a/tests/maps-test.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -import MapsMessage from './fixtures/maps-message'; - -describe('maps-test', function() { - it('should validate string map', function(done) { - let message = MapsMessage.create() - .addToMap('string', 'test1', '123') - .addToMap('string', 'test2', '456'); - - message.get('string').should.eql({ - test1: '123', - test2: '456' - }); - - message.removeFromMap('string', 'test2'); - - message.get('string').should.eql({ - test1: '123' - }); - - message - .addToMap('string', 'test2', '456') - .addToMap('string', 'test3', '789'); - - message.get('string').should.eql({ - test1: '123', - test2: '456', - test3: '789' - }); - - done(); - }); -}); diff --git a/tests/message-test.js b/tests/message-test.js deleted file mode 100644 index d6a9fd0..0000000 --- a/tests/message-test.js +++ /dev/null @@ -1,339 +0,0 @@ -'use strict'; - -import Priority from './fixtures/enum/priority'; -import Provider from './fixtures/enum/provider'; -import MapsMessage from './fixtures/maps-message'; -import EmailMessage from './fixtures/email-message'; -import NestedMessage from './fixtures/nested-message'; -import Enum from 'gdbots/common/enum'; -import FrozenMessageIsImmutable from 'gdbots/pbj/exception/frozen-message-is-immutable'; -import JsonSerializer from 'gdbots/pbj/serializer/json-serializer'; - -/** @var Serializer */ -let serializer = null; - -/** @var EmailMessage */ -let emailMessageFixture = null; - -describe('maps-test', function() { - it('create message from array', function(done) { - let message = createEmailMessage(); - message.set('priority', Priority.HIGH); - - message.get('priority').should.eql(Priority.HIGH); - Priority.HIGH.should.eql(message.get('priority')); - - let json = getSerializer().serialize(message); - message = getSerializer().deserialize(json); - - message.get('priority').should.eql(Priority.HIGH); - Priority.HIGH.should.eql(message.get('priority')); - - message.get('nested').get('location').getLatitude().should.eql(0.5); - - done(); - }); - - it('unique items in set', function(done) { - let message = EmailMessage.create() - .addToSet('labels', ['CHICKEN', 'Chicken', 'chicken', 'DONUTS', 'Donuts', 'donuts']); - - message.get('labels').length.should.eql(2); - message.get('labels').should.eql(['chicken', 'donuts']); - - done(); - }); - - it('is in set', function(done) { - let message = EmailMessage.create() - .addToSet('labels', ['abc']) - .addToSet( - 'enum_in_set', - [ - Provider.AOL, - Provider.GMAIL, - ] - ); - - message.isInSet('labels', 'abc').should.true; - message.isInSet('labels', 'idontexist').should.false; - message.isInSet('enum_in_set', Provider.AOL).should.true; - message.isInSet('enum_in_set', Provider.HOTMAIL).should.false; - - done(); - }); - - it('enum in set', function(done) { - let message = EmailMessage.create() - .addToSet( - 'enum_in_set', - [ - Provider.AOL, - Provider.AOL, - Provider.GMAIL, - Provider.GMAIL, - ] - ); - - message.get('enum_in_set').length.should.eql(2); - message.get('enum_in_set').should.eql([Provider.AOL, Provider.GMAIL]); - - done(); - }); - - it('is in list', function(done) { - let message = createEmailMessage(); - - let messageInList = message.get('any_of_message')[0]; - let messageNotInList = cloneMessage(messageInList); - messageNotInList.addToMap('string', 'key', 'val'); - - message.isInList('any_of_message', messageInList).should.true; - message.isInList('any_of_message', messageNotInList).should.false; - message.isInList('any_of_message', 'notinlist').should.false; - message.isInList('any_of_message', NestedMessage.create()).should.false; - message.isInList('enum_in_list', 'aol').should.false; - message.isInList('enum_in_list', Provider.AOL).should.true; - message.isInList('enum_in_list', 'notinlist').should.false; - message.isInList('enum_in_list', Provider.HOTMAIL).should.false; - - done(); - }); - - it('enum in list', function(done) { - let message = EmailMessage.create() - .addToList( - 'enum_in_list', - [ - Provider.AOL, - Provider.AOL, - Provider.GMAIL, - Provider.GMAIL, - ] - ); - - message.get('enum_in_list').length.should.eql(4); - message.get('enum_in_list').should.eql([Provider.AOL, Provider.AOL, Provider.GMAIL, Provider.GMAIL]); - - done(); - }); - - it('is in map', function(done) { - let message = MapsMessage.create(); - message.addToMap('string', 'string1', 'val1'); - - message.isInMap('string', 'string1').should.true; - message.isInMap('string', 'notinmap').should.false; - message.isInMap('microtime', 'notinmap').should.false; - - message.clear('string'); - - message.isInMap('string', 'string1').should.false; - - done(); - }); - - it('nested message', function(done) { - let message = createEmailMessage(); - let nestedMessage = NestedMessage.create() - .set('test1', 'val1') - .addToSet('test2', [1, 2]); - - message.set('nested', nestedMessage); - - nestedMessage.get('test2').should.eql([1, 2]); - message.get('nested').should.eql(nestedMessage); - - done(); - }); - - it('any of message in list', function(done) { - let message = EmailMessage.create() - .addToList( - 'any_of_message', - [ - MapsMessage.create().addToMap('string', 'test:field:name', 'value1'), - NestedMessage.create().set('test1', 'value1') - ] - ); - - message.get('any_of_message').length.should.eql(2); - - done(); - }); - - it('freeze', function(done) { - let message = createEmailMessage(); - let nestedMessage = NestedMessage.create(); - message.set('nested', nestedMessage); - - message.freeze(); - - message.isFrozen().should.true; - nestedMessage.isFrozen().should.true; - - done(); - }); - - it('frozen message is immutable', function(done) { - let message = createEmailMessage(); - let nestedMessage = NestedMessage.create(); - message.set('nested', nestedMessage); - - try { - - message.freeze(); - - message.set('from_name', 'homer'); - nestedMessage.set('test1', 'test1'); - } catch (e) { - e.should.eql(new FrozenMessageIsImmutable()); - } - - done(); - }); - - it('clone', function(done) { - let message = createEmailMessage(); - let nestedMessage = NestedMessage.create(); - message.set('nested', nestedMessage); - - nestedMessage.set('test1', 'original'); - - let message2 = cloneMessage(message); - message2.set('from_name', 'marge').get('nested').set('test1', 'clone'); - - (message == message2).should.false; - (message.get('date_sent') == message2.get('date_sent')).should.false; - (message.get('microtime_sent') == message2.get('microtime_sent')).should.false; - (message.get('nested') == message2.get('nested')).should.false; - (message.get('nested').get('test1') == message2.get('nested').get('test1')).should.false; - - done(); - }); - - it('clone is mutable after original is frozen', function(done) { - let message = createEmailMessage(); - let nestedMessage = NestedMessage.create(); - message.set('nested', nestedMessage); - - nestedMessage.set('test1', 'original'); - - message.freeze(); - - let message2 = cloneMessage(message); - message2.set('from_name', 'marge').get('nested').set('test1', 'clone'); - - try { - message.set('from_name', 'homer').get('nested').set('test1', 'original'); - - console.error('Original message should still be immutable.'); - } catch (e) { - e.should.eql(new FrozenMessageIsImmutable()); - } - - done(); - }); -}); - -/** - * @return Message message - */ -function cloneMessage(message) { - return getSerializer().deserialize( - getSerializer().serialize(message) - ); -} - -/** - * @return Serializer - */ -function getSerializer() { - if (null === serializer) { - serializer = new JsonSerializer(); - } - return serializer; -} - -/** - * @return EmailMessage - */ -function createEmailMessage() { - if (null === emailMessageFixture) { - emailMessageFixture = getSerializer().deserialize(jsonEmailMessage()); - } - - let message = cloneMessage(emailMessageFixture); - - message.set('date_sent', new Date('2014-12-25T12:12:00.123456Z')); - - return message; -} - -/** - * @return string - */ -function jsonEmailMessage() { - return JSON.stringify({ - "_schema": "pbj:gdbots:tests.pbj:fixtures:email-message:1-0-0", - "id": "0dcee564-aa71-11e4-a811-3c15c2c60168", - "from_name": "homer ", - "from_email": "homer@thesimpsons.com", - "priority": 2, - "sent": false, - "date_sent": "2014-12-25T12:12:00.123456+00:00", - "microtime_sent": "1422122017734617", - "provider": "gmail", - "labels": [ - "donuts", - "mmmm", - "chicken" - ], - "nested": { - "_schema": "pbj:gdbots:tests.pbj:fixtures:nested-message:1-0-0", - "test1": "val1", - "test2": [ - 1, - 2 - ], - "location": { - "type": "Point", - "coordinates": [102.0,0.5] - }, - "refs": [ - { - "curie": "gdbots:tests.pbj:fixtures:email-message", - "id": "0dcee564-aa71-11e4-a811-3c15c2c60168", - "tag": "parent" - }, - { - "curie": "gdbots:tests.pbj:fixtures:email-message", - "id": "0dcee564-aa71-11e4-a811-3c15c2c60168", - "tag": "parent" - } - ] - }, - "enum_in_set": [ - "aol", - "gmail" - ], - "enum_in_list": [ - "aol", - "aol", - "gmail", - "gmail" - ], - "any_of_message": [ - { - "_schema": "pbj:gdbots:tests.pbj:fixtures:maps-message:1-0-0", - "String": { - "test:field:name": "value1" - } - }, - { - "_schema": "pbj:gdbots:tests.pbj:fixtures:nested-message:1-0-0", - "test1": "value1" - } - ] - }); -} diff --git a/tests/serializers/JsonSerializer.test.js b/tests/serializers/JsonSerializer.test.js new file mode 100644 index 0000000..deb3e8c --- /dev/null +++ b/tests/serializers/JsonSerializer.test.js @@ -0,0 +1,16 @@ +import test from 'tape'; +import JsonSerializer from '../../src/serializers/JsonSerializer'; +import SampleMessageV1 from './../fixtures/SampleMessageV1'; + +test('JsonSerializer tests', (t) => { + const message = SampleMessageV1.create() + .set('string_single', 'test') + .addToSet('string_set', ['set1', 'set2']) + .addToList('string_list', ['list1', 'list2']) + .addToMap('string_map', 'key1', 'val1') + .addToMap('string_map', 'key2', 'val2'); + + t.same(JsonSerializer.serialize(message), '{"_schema":"pbj:gdbots:pbj.tests::sample-message:1-0-0","mixin_int":0,"string_single":"test","string_set":["set1","set2"],"string_list":["list1","list2"],"string_map":{"key1":"val1","key2":"val2"}}'); + + t.end(); +}); diff --git a/tests/serializers/ObjectSerializer.test.js b/tests/serializers/ObjectSerializer.test.js new file mode 100644 index 0000000..4348fb3 --- /dev/null +++ b/tests/serializers/ObjectSerializer.test.js @@ -0,0 +1,121 @@ +import test from 'tape'; +import Fb from '../../src/FieldBuilder'; +import DynamicField from '../../src/well-known/DynamicField'; +import GeoPoint from '../../src/well-known/GeoPoint'; +import MessageRef from '../../src/MessageRef'; +import T from '../../src/types'; +import ObjectSerializer from '../../src/serializers/ObjectSerializer'; +import SampleMessageV1 from '../fixtures/SampleMessageV1'; +import SampleOtherMessageV1 from '../fixtures/SampleOtherMessageV1'; + +test('ObjectSerializer serialize tests', (t) => { + const message = SampleMessageV1.create() + .set('string_single', 'test') + .addToSet('string_set', ['set1', 'set2']) + .addToList('string_list', ['list1', 'list2']) + .addToMap('string_map', 'key1', 'val1') + .addToMap('string_map', 'key2', 'val2') + .set('message_single', SampleOtherMessageV1.create().set('test', 'single')) + .addToList('message_list', [SampleOtherMessageV1.create().set('test', 'list')]) + .addToMap('message_map', 'test', SampleOtherMessageV1.create().set('test', 'map')); + + const obj = { + _schema: 'pbj:gdbots:pbj.tests::sample-message:1-0-0', + mixin_int: 0, + string_single: 'test', + string_set: ['set1', 'set2'], + string_list: ['list1', 'list2'], + string_map: { key1: 'val1', key2: 'val2' }, + message_single: { + _schema: 'pbj:gdbots:pbj.tests::sample-other-message:1-0-0', + mixin_int: 0, + test: 'single', + }, + message_list: [ + { + _schema: 'pbj:gdbots:pbj.tests::sample-other-message:1-0-0', + mixin_int: 0, + test: 'list', + }, + ], + message_map: { + test: { + _schema: 'pbj:gdbots:pbj.tests::sample-other-message:1-0-0', + mixin_int: 0, + test: 'map', + }, + }, + }; + + t.same(ObjectSerializer.serialize(message), obj); + + t.end(); +}); + + +test('ObjectSerializer deserialize tests', (t) => { + const message = SampleMessageV1.create() + .addToList('string_list', ['list1', 'list2']) + .set('string_single', 'test') + .addToSet('string_set', ['set1', 'set2']) + .addToMap('string_map', 'key1', 'val1') + .addToMap('string_map', 'key2', 'val2') + .set('message_single', SampleOtherMessageV1.create().set('test', 'single')) + .addToList('message_list', [SampleOtherMessageV1.create().set('test', 'list')]) + .addToMap('message_map', 'test', SampleOtherMessageV1.create().set('test', 'map')); + + t.true(message.equals(ObjectSerializer.deserialize(ObjectSerializer.serialize(message)))); + + t.end(); +}); + + +test('ObjectSerializer encode/decode Message tests', (t) => { + const message = SampleMessageV1.create(); + const field = message.schema().getField('string_single'); + const obj = { + _schema: 'pbj:gdbots:pbj.tests::sample-message:1-0-0', + mixin_int: 0, + }; + + t.same(ObjectSerializer.encodeMessage(message, field), obj); + t.true(message.equals(ObjectSerializer.decodeMessage(obj, field))); + + t.end(); +}); + + +test('ObjectSerializer encode/decode MessageRef tests', (t) => { + const obj = { curie: 'acme:blog:node:article', id: '123', tag: 'tag' }; + const ref = MessageRef.fromObject(obj); + const field = Fb.create('test', T.MessageRefType.create()).build(); + + t.same(ObjectSerializer.encodeMessageRef(ref, field), obj); + t.true(ref.equals(ObjectSerializer.decodeMessageRef(obj, field))); + + t.end(); +}); + + +test('ObjectSerializer encode/decode GeoPoint tests', (t) => { + const obj = { type: 'Point', coordinates: [102, 0.5] }; + const geoPoint = GeoPoint.fromObject(obj); + const field = Fb.create('test', T.GeoPointType.create()).build(); + + t.same(ObjectSerializer.encodeGeoPoint(geoPoint, field), obj); + t.true(geoPoint.equals(ObjectSerializer.decodeGeoPoint(obj, field))); + + t.end(); +}); + + +test('ObjectSerializer encode/decode DynamicField tests', (t) => { + const obj = { name: 'test', bool_val: true }; + const df = DynamicField.fromObject(obj); + const field = Fb.create('test', T.DynamicFieldType.create()).build(); + + t.same(ObjectSerializer.encodeDynamicField(df, field), obj); + t.true(df.equals(ObjectSerializer.decodeDynamicField(obj, field))); + + t.end(); +}); diff --git a/tests/type/trinary-type-test.js b/tests/type/trinary-type-test.js deleted file mode 100644 index cc661f4..0000000 --- a/tests/type/trinary-type-test.js +++ /dev/null @@ -1,73 +0,0 @@ -'use strict'; - -import TrinaryType from 'gdbots/pbj/type/trinary-type'; -import FieldBuilder from 'gdbots/pbj/field-builder'; - -describe('trinary-type-test', function() { - it('validate encoding', function(done) { - let field = FieldBuilder.create('trinary_unknown', TrinaryType.create()).build(); - let type = field.getType(); - - type.encode(0, field).should.eql(0); - type.encode(1, field).should.eql(1); - type.encode(2, field).should.eql(2); - - done(); - }); - - it('validate decoding', function(done) { - let field = FieldBuilder.create('trinary_unknown', TrinaryType.create()).build(); - let type = field.getType(); - - type.decode(null, field).should.eql(0); - type.decode(0, field).should.eql(0); - type.decode(1, field).should.eql(1); - type.decode(2, field).should.eql(2); - - type.decode('0', field).should.eql(0); - type.decode('1', field).should.eql(1); - type.decode('2', field).should.eql(2); - - done(); - }); - - it('validate values', function(done) { - let field = FieldBuilder.create('trinary_unknown', TrinaryType.create()).build(); - let type = field.getType(); - - type.guard(0, field); - type.guard(1, field); - type.guard(2, field); - - done(); - }); - - it('invalid values validation', function(done) { - let field = FieldBuilder.create('trinary_unknown', TrinaryType.create()).build(); - let type = field.getType(); - let thrown = false; - - let invalid = [ - 'a', - [], - 3, - -1, - false, - true, - ]; - - invalid.forEach(function(val) { - try { - type.guard(val, field); - } catch (e) { - thrown = true; - } - - if (false === thrown) { - console.log('TrinaryType field accepted invalid value [' + val + '].'); - } - }); - - done(); - }); -}); diff --git a/tests/type/type-test.js b/tests/type/type-test.js deleted file mode 100644 index 4b249d1..0000000 --- a/tests/type/type-test.js +++ /dev/null @@ -1,60 +0,0 @@ -'use strict'; - -import NestedMessage from '../fixtures/nested-message'; -import GeoPoint from 'gdbots/pbj/well-known/geo-point'; -import ArraySerializer from 'gdbots/pbj/serializer/array-serializer'; -import BinaryType from 'gdbots/pbj/type/binary-type'; -import BlobType from 'gdbots/pbj/type/blob-type'; -import MediumBlobType from 'gdbots/pbj/type/medium-blob-type'; -import MediumTextType from 'gdbots/pbj/type/medium-text-type'; -import StringType from 'gdbots/pbj/type/string-type'; -import TextType from 'gdbots/pbj/type/text-type'; -import Fb from 'gdbots/pbj/field-builder'; - -describe('type-test', function() { - it('should validate geo-point type', function(done) { - let point = GeoPoint.fromArray({ - type: 'Point', - coordinates: [102.0, 0.5] - }); - - let message = NestedMessage.create(); - message.set('location', point); - - message.get('location').getLatitude().should.eql(0.5); - message.get('location').getLongitude().should.eql(102.0); - message.toArray().location.should.eql(point.toArray()); - - let json = new ArraySerializer().serialize(message); - let newMessage = message.constructor.fromArray(json); - - newMessage.toArray().should.eql(message.toArray()); - - done(); - }); - - it('should thrown an exception when guard max-bytes invalid', function(done) { - let types = [ - BinaryType, BlobType, MediumBlobType, - MediumTextType, StringType, TextType - ]; - - types.forEach(function(TypeName) { - let field = Fb.create(TypeName.name, TypeName.create()).build(); - let text = 'a'.repeat(field.getType().getMaxBytes() + 1); - let thrown = false; - - try { - field.getType().guard(text, field); - } catch (e) { - thrown = true; - } - - if (false === thrown) { - console.log('[' + TypeName.name + '] accepted more than [' + field.getType().getMaxBytes() + '] bytes.'); - } - }); - - done(); - }); -}); diff --git a/tests/types/BigIntType.test.js b/tests/types/BigIntType.test.js new file mode 100644 index 0000000..bd92f1d --- /dev/null +++ b/tests/types/BigIntType.test.js @@ -0,0 +1,99 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import BigIntType from '../../src/types/BigIntType'; +import BigNumber from '../../src/well-known/BigNumber'; +import helpers from './helpers'; + +test('BigIntType property tests', (t) => { + const bigIntType = BigIntType.create(); + t.true(bigIntType instanceof Type); + t.true(bigIntType instanceof BigIntType); + t.same(bigIntType, BigIntType.create()); + t.true(bigIntType === BigIntType.create()); + t.same(bigIntType.getTypeName(), TypeName.BIG_INT); + t.same(bigIntType.getTypeValue(), TypeName.BIG_INT.valueOf()); + t.same(bigIntType.isScalar(), false); + t.same(bigIntType.encodesToScalar(), true); + t.same(bigIntType.getDefault(), new BigNumber(0)); + t.same(bigIntType.isBoolean(), false); + t.same(bigIntType.isBinary(), false); + t.same(bigIntType.isNumeric(), true); + t.same(bigIntType.isString(), false); + t.same(bigIntType.isMessage(), false); + t.same(bigIntType.allowedInSet(), true); + + try { + bigIntType.test = 1; + t.fail('BigIntType instance is mutable'); + } catch (e) { + t.pass('BigIntType instance is immutable'); + } + + t.end(); +}); + + +test('BigIntType guard tests', (t) => { + const field = new Field({ name: 'test', type: BigIntType.create() }); + const valid = [ + new BigNumber(0), + new BigNumber('18446744073709551615'), + ]; + const invalid = [ + -1, + new BigNumber('-1'), + new BigNumber('18446744073709551616'), + '0', + '1', + '2', + null, + [], + {}, + '', + NaN, + undefined, + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('BigIntType encode tests', (t) => { + const field = new Field({ name: 'test', type: BigIntType.create() }); + const samples = [ + { input: new BigNumber('18446744073709551615'), output: '18446744073709551615' }, + { input: new BigNumber('18446744073709551610.555'), output: '18446744073709551611' }, + { input: new BigNumber(1), output: '1' }, + { input: new BigNumber(1.44444), output: '1' }, + { input: 0, output: '0' }, + { input: 1, output: '0' }, + { input: 2, output: '0' }, + { input: false, output: '0' }, + { input: '', output: '0' }, + { input: null, output: '0' }, + { input: undefined, output: '0' }, + { input: NaN, output: '0' }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('BigIntType decode tests', (t) => { + const field = new Field({ name: 'test', type: BigIntType.create() }); + const samples = [ + { input: '18446744073709551615', output: new BigNumber('18446744073709551615') }, + { input: '1', output: new BigNumber('1') }, + { input: '0', output: new BigNumber('0') }, + { input: '0', output: new BigNumber(0) }, + { input: new BigNumber(1), output: new BigNumber(1) }, + { input: null, output: null }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/BinaryType.test.js b/tests/types/BinaryType.test.js new file mode 100644 index 0000000..09a1786 --- /dev/null +++ b/tests/types/BinaryType.test.js @@ -0,0 +1,101 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import BinaryType from '../../src/types/BinaryType'; +import helpers from './helpers'; + +test('BinaryType property tests', (t) => { + const binaryType = BinaryType.create(); + t.true(binaryType instanceof Type); + t.true(binaryType instanceof BinaryType); + t.same(binaryType, BinaryType.create()); + t.true(binaryType === BinaryType.create()); + t.same(binaryType.getTypeName(), TypeName.BINARY); + t.same(binaryType.getTypeValue(), TypeName.BINARY.valueOf()); + t.same(binaryType.isScalar(), true); + t.same(binaryType.encodesToScalar(), true); + t.same(binaryType.getDefault(), null); + t.same(binaryType.isBoolean(), false); + t.same(binaryType.isBinary(), true); + t.same(binaryType.isNumeric(), false); + t.same(binaryType.isString(), true); + t.same(binaryType.isMessage(), false); + t.same(binaryType.allowedInSet(), true); + t.same(binaryType.getMaxBytes(), 255); + + try { + binaryType.test = 1; + t.fail('binaryType instance is mutable'); + } catch (e) { + t.pass('binaryType instance is immutable'); + } + + t.end(); +}); + + +test('BinaryType guard tests', (t) => { + const field = new Field({ name: 'test', type: BinaryType.create() }); + const valid = [ + 'test', + 'KOKVr8Kw4pahwrAp4pWv77i1IOKUu+KUgeKUuw==', + 'IGljZSDwn42mIHBvb3Ag8J+SqSBkb2gg8J+YsyA=', + '4LKgX+CyoA==', + ]; + const invalid = [-1, 1, true, false, null, [], {}, NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('BinaryType guard (min/max length) tests', (t) => { + const binaryType = BinaryType.create(); + binaryType.decodeFromBase64(false); + binaryType.encodeToBase64(false); + + const field = new Field({ name: 'test', type: binaryType, minLength: 5, maxLength: 10 }); + const valid = ['01234', '0123456789', '012345', '012345678']; + const invalid = ['0123', '01234567890']; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + + binaryType.decodeFromBase64(true); + binaryType.encodeToBase64(true); + t.end(); +}); + + +test('BinaryType encode tests', (t) => { + const field = new Field({ name: 'test', type: BinaryType.create() }); + const samples = [ + { input: 'test', output: 'dGVzdA==' }, + { input: 'homer simpson', output: 'aG9tZXIgc2ltcHNvbg==' }, + { input: '✓ à la mode', output: '4pyTIMOgIGxhIG1vZGU=' }, + { input: ' ice 🍦 poop 💩 doh 😳 ', output: 'aWNlIPCfjaYgcG9vcCDwn5KpIGRvaCDwn5iz' }, + { input: '(╯°□°)╯︵ ┻━┻', output: 'KOKVr8Kw4pahwrAp4pWv77i1IOKUu+KUgeKUuw==' }, + { input: 'ಠ_ಠ', output: '4LKgX+CyoA==' }, + { input: 'foo © bar 𝌆 baz', output: 'Zm9vIMKpIGJhciDwnYyGIGJheg==' }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('BinaryType decode tests', (t) => { + const field = new Field({ name: 'test', type: BinaryType.create() }); + const samples = [ + { input: 'dGVzdA==', output: 'test' }, + { input: 'aG9tZXIgc2ltcHNvbg==', output: 'homer simpson' }, + { input: '4pyTIMOgIGxhIG1vZGU=', output: '✓ à la mode' }, + { input: 'aWNlIPCfjaYgcG9vcCDwn5KpIGRvaCDwn5iz', output: 'ice 🍦 poop 💩 doh 😳' }, + { input: 'KOKVr8Kw4pahwrAp4pWv77i1IOKUu+KUgeKUuw==', output: '(╯°□°)╯︵ ┻━┻' }, + { input: '4LKgX+CyoA==', output: 'ಠ_ಠ' }, + { input: 'Zm9vIMKpIGJhciDwnYyGIGJheg==', output: 'foo © bar 𝌆 baz' }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/BlobType.test.js b/tests/types/BlobType.test.js new file mode 100644 index 0000000..c13a409 --- /dev/null +++ b/tests/types/BlobType.test.js @@ -0,0 +1,101 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import BlobType from '../../src/types/BlobType'; +import helpers from './helpers'; + +test('BlobType property tests', (t) => { + const blobType = BlobType.create(); + t.true(blobType instanceof Type); + t.true(blobType instanceof BlobType); + t.same(blobType, BlobType.create()); + t.true(blobType === BlobType.create()); + t.same(blobType.getTypeName(), TypeName.BLOB); + t.same(blobType.getTypeValue(), TypeName.BLOB.valueOf()); + t.same(blobType.isScalar(), true); + t.same(blobType.encodesToScalar(), true); + t.same(blobType.getDefault(), null); + t.same(blobType.isBoolean(), false); + t.same(blobType.isBinary(), true); + t.same(blobType.isNumeric(), false); + t.same(blobType.isString(), true); + t.same(blobType.isMessage(), false); + t.same(blobType.allowedInSet(), false); + t.same(blobType.getMaxBytes(), 65535); + + try { + blobType.test = 1; + t.fail('blobType instance is mutable'); + } catch (e) { + t.pass('blobType instance is immutable'); + } + + t.end(); +}); + + +test('BlobType guard tests', (t) => { + const field = new Field({ name: 'test', type: BlobType.create() }); + const valid = [ + 'test', + 'KOKVr8Kw4pahwrAp4pWv77i1IOKUu+KUgeKUuw==', + 'IGljZSDwn42mIHBvb3Ag8J+SqSBkb2gg8J+YsyA=', + '4LKgX+CyoA==', + ]; + const invalid = [-1, 1, true, false, null, [], {}, NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('BlobType guard (min/max length) tests', (t) => { + const blobType = BlobType.create(); + blobType.decodeFromBase64(false); + blobType.encodeToBase64(false); + + const field = new Field({ name: 'test', type: blobType, minLength: 5, maxLength: 10 }); + const valid = ['01234', '0123456789', '012345', '012345678']; + const invalid = ['0123', '01234567890']; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + + blobType.decodeFromBase64(true); + blobType.encodeToBase64(true); + t.end(); +}); + + +test('BlobType encode tests', (t) => { + const field = new Field({ name: 'test', type: BlobType.create() }); + const samples = [ + { input: 'test', output: 'dGVzdA==' }, + { input: 'homer simpson', output: 'aG9tZXIgc2ltcHNvbg==' }, + { input: '✓ à la mode', output: '4pyTIMOgIGxhIG1vZGU=' }, + { input: ' ice 🍦 poop 💩 doh 😳 ', output: 'aWNlIPCfjaYgcG9vcCDwn5KpIGRvaCDwn5iz' }, + { input: '(╯°□°)╯︵ ┻━┻', output: 'KOKVr8Kw4pahwrAp4pWv77i1IOKUu+KUgeKUuw==' }, + { input: 'ಠ_ಠ', output: '4LKgX+CyoA==' }, + { input: 'foo © bar 𝌆 baz', output: 'Zm9vIMKpIGJhciDwnYyGIGJheg==' }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('BlobType decode tests', (t) => { + const field = new Field({ name: 'test', type: BlobType.create() }); + const samples = [ + { input: 'dGVzdA==', output: 'test' }, + { input: 'aG9tZXIgc2ltcHNvbg==', output: 'homer simpson' }, + { input: '4pyTIMOgIGxhIG1vZGU=', output: '✓ à la mode' }, + { input: 'aWNlIPCfjaYgcG9vcCDwn5KpIGRvaCDwn5iz', output: 'ice 🍦 poop 💩 doh 😳' }, + { input: 'KOKVr8Kw4pahwrAp4pWv77i1IOKUu+KUgeKUuw==', output: '(╯°□°)╯︵ ┻━┻' }, + { input: '4LKgX+CyoA==', output: 'ಠ_ಠ' }, + { input: 'Zm9vIMKpIGJhciDwnYyGIGJheg==', output: 'foo © bar 𝌆 baz' }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/BooleanType.test.js b/tests/types/BooleanType.test.js new file mode 100644 index 0000000..1974f19 --- /dev/null +++ b/tests/types/BooleanType.test.js @@ -0,0 +1,104 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import BooleanType from '../../src/types/BooleanType'; +import helpers from './helpers'; + +test('BooleanType property tests', (t) => { + const booleanType = BooleanType.create(); + t.true(booleanType instanceof Type); + t.true(booleanType instanceof BooleanType); + t.same(booleanType, BooleanType.create()); + t.true(booleanType === BooleanType.create()); + t.same(booleanType.getTypeName(), TypeName.BOOLEAN); + t.same(booleanType.getTypeValue(), TypeName.BOOLEAN.valueOf()); + t.same(booleanType.isScalar(), true); + t.same(booleanType.encodesToScalar(), true); + t.same(booleanType.getDefault(), false); + t.same(booleanType.isBoolean(), true); + t.same(booleanType.isBinary(), false); + t.same(booleanType.isNumeric(), false); + t.same(booleanType.isString(), false); + t.same(booleanType.isMessage(), false); + t.same(booleanType.allowedInSet(), false); + + try { + booleanType.test = 1; + t.fail('booleanType instance is mutable'); + } catch (e) { + t.pass('booleanType instance is immutable'); + } + + t.end(); +}); + + +test('BooleanType guard tests', (t) => { + const field = new Field({ name: 'test', type: BooleanType.create() }); + const valid = [true, false]; + const invalid = ['true', 'false', 1, 0, 'on', 'off', 'yes', 'no', '+', '-', null, [], {}, -1, '', NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('BooleanType encode tests', (t) => { + const field = new Field({ name: 'test', type: BooleanType.create() }); + const samples = [ + { input: false, output: false }, + { input: '', output: false }, + { input: null, output: false }, + { input: undefined, output: false }, + { input: 0, output: false }, + { input: NaN, output: false }, + { input: true, output: true }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('BooleanType decode tests', (t) => { + const field = new Field({ name: 'test', type: BooleanType.create() }); + const samples = [ + { input: false, output: false }, + { input: 'false', output: false }, + { input: 'FALSE', output: false }, + { input: 'False', output: false }, + { input: 'FaLSe', output: false }, + { input: '0', output: false }, + { input: '-1', output: false }, + { input: 'no', output: false }, + { input: 'null', output: false }, + { input: '', output: false }, + { input: 0, output: false }, + { input: -1, output: false }, + { input: null, output: false }, + { input: undefined, output: false }, + { input: {}, output: false }, + { input: [], output: false }, + { input: NaN, output: false }, + + { input: true, output: true }, + { input: 'true', output: true }, + { input: 'TRUE', output: true }, + { input: 'True', output: true }, + { input: 'tRuE', output: true }, + { input: '1', output: true }, + { input: 'yes', output: true }, + { input: 'YES', output: true }, + { input: 'Yes', output: true }, + { input: 'yEs', output: true }, + { input: '+', output: true }, + { input: 'on', output: true }, + { input: 'ON', output: true }, + { input: 'On', output: true }, + { input: 1, output: true }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/DateTimeType.test.js b/tests/types/DateTimeType.test.js new file mode 100644 index 0000000..17ea49f --- /dev/null +++ b/tests/types/DateTimeType.test.js @@ -0,0 +1,126 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import DateTimeType from '../../src/types/DateTimeType'; +import helpers from './helpers'; + +test('DateTimeType property tests', (t) => { + const dateTimeType = DateTimeType.create(); + t.true(dateTimeType instanceof Type); + t.true(dateTimeType instanceof DateTimeType); + t.same(dateTimeType, DateTimeType.create()); + t.true(dateTimeType === DateTimeType.create()); + t.same(dateTimeType.getTypeName(), TypeName.DATE_TIME); + t.same(dateTimeType.getTypeValue(), TypeName.DATE_TIME.valueOf()); + t.same(dateTimeType.isScalar(), false); + t.same(dateTimeType.encodesToScalar(), true); + t.same(dateTimeType.getDefault(), null); + t.same(dateTimeType.isBoolean(), false); + t.same(dateTimeType.isBinary(), false); + t.same(dateTimeType.isNumeric(), false); + t.same(dateTimeType.isString(), true); + t.same(dateTimeType.isMessage(), false); + t.same(dateTimeType.allowedInSet(), false); + + try { + dateTimeType.test = 1; + t.fail('DateTimeType instance is mutable'); + } catch (e) { + t.pass('DateTimeType instance is immutable'); + } + + t.end(); +}); + + +test('DateTimeType guard tests', (t) => { + const field = new Field({ name: 'test', type: DateTimeType.create() }); + const valid = [ + new Date('2015-12-25T07:30:45.123Z'), + new Date('2015-12-25T07:30:45.123+08:00'), + ]; + const invalid = [ + '2015-12-25', + null, + [], + {}, + '', + NaN, + undefined, + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('DateTimeType encode tests', (t) => { + const field = new Field({ name: 'test', type: DateTimeType.create() }); + const samples = [ + { + input: new Date('2015-12-25T07:30:45.123Z'), + output: '2015-12-25T07:30:45.123Z', + }, + { + input: new Date('2015-12-25T07:30:45.123+08:00'), + output: '2015-12-24T23:30:45.123Z', + }, + { input: 0, output: null }, + { input: 1, output: null }, + { input: 2, output: null }, + { input: false, output: null }, + { input: '', output: null }, + { input: null, output: null }, + { input: undefined, output: null }, + { input: NaN, output: null }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('DateTimeType decode tests', (t) => { + const field = new Field({ name: 'test', type: DateTimeType.create() }); + const date = new Date(Date.UTC(2015, 11, 25, 12, 30, 45, 123)); + const samples = [ + { + input: '2015-12-25T07:30:45.123Z', + output: new Date('2015-12-25T07:30:45.123Z'), + }, + { + input: '2015-12-25T07:30:45.123+08:00', + output: new Date('2015-12-24T23:30:45.123Z'), + }, + { + input: '2015-12-25T07:30:45.123+0800', + output: new Date('2015-12-24T23:30:45.123Z'), + }, + { input: date, output: date }, + { input: null, output: null }, + ]; + + function format(d) { + return d instanceof Date ? d.toISOString() : d; + } + + samples.forEach((obj) => { + try { + const actual = field.getType().decode(obj.input, field); + t.same(format(actual), format(obj.output)); + } catch (e) { + t.fail(e.message); + } + }); + + t.end(); +}); + + +test('DateTimeType decode(invalid) tests', (t) => { + const field = new Field({ name: 'test', type: DateTimeType.create() }); + const samples = ['nope', '12/25/2015', false, [], {}, '', NaN, undefined]; + helpers.decodeInvalidSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/DateType.test.js b/tests/types/DateType.test.js new file mode 100644 index 0000000..a3fbcc2 --- /dev/null +++ b/tests/types/DateType.test.js @@ -0,0 +1,111 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import DateType from '../../src/types/DateType'; +import helpers from './helpers'; + +test('DateType property tests', (t) => { + const dateType = DateType.create(); + t.true(dateType instanceof Type); + t.true(dateType instanceof DateType); + t.same(dateType, DateType.create()); + t.true(dateType === DateType.create()); + t.same(dateType.getTypeName(), TypeName.DATE); + t.same(dateType.getTypeValue(), TypeName.DATE.valueOf()); + t.same(dateType.isScalar(), false); + t.same(dateType.encodesToScalar(), true); + t.same(dateType.getDefault(), null); + t.same(dateType.isBoolean(), false); + t.same(dateType.isBinary(), false); + t.same(dateType.isNumeric(), false); + t.same(dateType.isString(), true); + t.same(dateType.isMessage(), false); + t.same(dateType.allowedInSet(), false); + + try { + dateType.test = 1; + t.fail('DateType instance is mutable'); + } catch (e) { + t.pass('DateType instance is immutable'); + } + + t.end(); +}); + + +test('DateType guard tests', (t) => { + const field = new Field({ name: 'test', type: DateType.create() }); + const valid = [ + new Date(2015, 11, 25), + new Date('2015-12-25'), + ]; + const invalid = [ + '2015-12-25', + null, + [], + {}, + '', + NaN, + undefined, + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('DateType encode tests', (t) => { + const field = new Field({ name: 'test', type: DateType.create() }); + const samples = [ + { input: new Date(2015, 11, 25), output: '2015-12-25' }, + { input: 0, output: null }, + { input: 1, output: null }, + { input: 2, output: null }, + { input: false, output: null }, + { input: '', output: null }, + { input: null, output: null }, + { input: undefined, output: null }, + { input: NaN, output: null }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('DateType decode tests', (t) => { + const field = new Field({ name: 'test', type: DateType.create() }); + const date = new Date(Date.UTC(2015, 11, 25)); + const samples = [ + { input: '2015-12-25T07:30:45.123Z', output: date }, + { input: '2015-12-25T07:30:45.123+08:00', output: date }, + { input: '2015-12-25T07:30:45.123+0800', output: date }, + { input: '2015-12-25', output: date }, + { input: date, output: date }, + { input: null, output: null }, + ]; + + function format(d) { + return d instanceof Date ? d.toISOString().substr(0, 10) : d; + } + + samples.forEach((obj) => { + try { + const actual = field.getType().decode(obj.input, field); + t.same(format(actual), format(obj.output)); + } catch (e) { + t.fail(e.message); + } + }); + + t.end(); +}); + + +test('DateType decode(invalid) tests', (t) => { + const field = new Field({ name: 'test', type: DateType.create() }); + const samples = ['nope', '12/25/2015', false, [], {}, '', NaN, undefined]; + helpers.decodeInvalidSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/DecimalType.test.js b/tests/types/DecimalType.test.js new file mode 100644 index 0000000..e6fe6bb --- /dev/null +++ b/tests/types/DecimalType.test.js @@ -0,0 +1,118 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import DecimalType from '../../src/types/DecimalType'; +import helpers from './helpers'; + +test('DecimalType property tests', (t) => { + const decimalType = DecimalType.create(); + t.true(decimalType instanceof Type); + t.true(decimalType instanceof DecimalType); + t.same(decimalType, DecimalType.create()); + t.true(decimalType === DecimalType.create()); + t.same(decimalType.getTypeName(), TypeName.DECIMAL); + t.same(decimalType.getTypeValue(), TypeName.DECIMAL.valueOf()); + t.same(decimalType.isScalar(), true); + t.same(decimalType.encodesToScalar(), true); + t.same(decimalType.getDefault(), 0.0); + t.same(decimalType.isBoolean(), false); + t.same(decimalType.isBinary(), false); + t.same(decimalType.isNumeric(), true); + t.same(decimalType.isString(), false); + t.same(decimalType.isMessage(), false); + t.same(decimalType.allowedInSet(), true); + t.same(decimalType.getMin(), Number.MIN_VALUE); + t.same(decimalType.getMax(), Number.MAX_VALUE); + + try { + decimalType.test = 1; + t.fail('decimalType instance is mutable'); + } catch (e) { + t.pass('decimalType instance is immutable'); + } + + t.end(); +}); + + +test('DecimalType guard tests', (t) => { + const field = new Field({ name: 'test', type: DecimalType.create() }); + const valid = [0.0, 3.14, -3.14, Number.MIN_VALUE, Number.MAX_VALUE]; + const invalid = ['0', '0.0', '3.14', '-3.14', null, [], {}, '', NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('DecimalType encode tests', (t) => { + const field = new Field({ name: 'test', type: DecimalType.create() }); + const samples = [ + { input: 0.1, output: '0.10' }, + { input: 3.14159265358979, output: '3.14' }, + { input: -3.14159265358979, output: '-3.14' }, + { input: false, output: '0.00' }, + { input: '', output: '0.00' }, + { input: null, output: '0.00' }, + { input: undefined, output: '0.00' }, + { input: NaN, output: '0.00' }, + { input: '3.14159265358979', output: '3.14' }, + { input: '-3.14159265358979', output: '-3.14' }, + ]; + + helpers.encodeSamples(field, samples, t); + t.comment('test samples with scale of 6'); + + const fieldWith6Scale = new Field({ name: 'test_6_scale', type: DecimalType.create(), precision: 10, scale: 6 }); + const samplesWith6Scale = [ + { input: 0.1, output: '0.100000' }, + { input: 1.1, output: '1.100000' }, + { input: 3.14159265358979, output: '3.141593' }, + { input: -3.14159265358979, output: '-3.141593' }, + { input: '3.14159265358979', output: '3.141593' }, + { input: '-3.14159265358979', output: '-3.141593' }, + ]; + + helpers.encodeSamples(fieldWith6Scale, samplesWith6Scale, t); + + t.end(); +}); + + +test('DecimalType decode tests', (t) => { + const field = new Field({ name: 'test', type: DecimalType.create() }); + const samples = [ + { input: 0, output: 0 }, + { input: 3.14159265358979, output: 3.14 }, + { input: -3.14159265358979, output: -3.14 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: NaN, output: 0 }, + { input: '3.14159265358979', output: 3.14 }, + { input: '-3.14159265358979', output: -3.14 }, + ]; + + helpers.decodeSamples(field, samples, t); + + const fieldWith6Scale = new Field({ name: 'test_6_scale', type: DecimalType.create(), precision: 10, scale: 6 }); + const samplesWith6Scale = [ + { input: '0.100000', output: 0.1 }, + { input: '1.100000', output: 1.1 }, + { input: 3.14159265358979, output: 3.141593 }, + { input: -3.14159265358979, output: -3.141593 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: NaN, output: 0 }, + { input: '3.14159265358979', output: 3.141593 }, + { input: '-3.14159265358979', output: -3.141593 }, + ]; + + helpers.decodeSamples(fieldWith6Scale, samplesWith6Scale, t); + + t.end(); +}); diff --git a/tests/types/DynamicFieldType.test.js b/tests/types/DynamicFieldType.test.js new file mode 100644 index 0000000..f186a5e --- /dev/null +++ b/tests/types/DynamicFieldType.test.js @@ -0,0 +1,124 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import DynamicFieldType from '../../src/types/DynamicFieldType'; +import DynamicField from '../../src/well-known/DynamicField'; +import helpers from './helpers'; + +test('DynamicFieldType property tests', (t) => { + const dynamicFieldType = DynamicFieldType.create(); + t.true(dynamicFieldType instanceof Type); + t.true(dynamicFieldType instanceof DynamicFieldType); + t.same(dynamicFieldType, DynamicFieldType.create()); + t.true(dynamicFieldType === DynamicFieldType.create()); + t.same(dynamicFieldType.getTypeName(), TypeName.DYNAMIC_FIELD); + t.same(dynamicFieldType.getTypeValue(), TypeName.DYNAMIC_FIELD.valueOf()); + t.same(dynamicFieldType.isScalar(), false); + t.same(dynamicFieldType.encodesToScalar(), false); + t.same(dynamicFieldType.getDefault(), null); + t.same(dynamicFieldType.isBoolean(), false); + t.same(dynamicFieldType.isBinary(), false); + t.same(dynamicFieldType.isNumeric(), false); + t.same(dynamicFieldType.isString(), false); + t.same(dynamicFieldType.isMessage(), false); + t.same(dynamicFieldType.allowedInSet(), false); + + try { + dynamicFieldType.test = 1; + t.fail('DynamicFieldType instance is mutable'); + } catch (e) { + t.pass('DynamicFieldType instance is immutable'); + } + + t.end(); +}); + + +test('DynamicFieldType guard tests', (t) => { + const field = new Field({ name: 'test', type: DynamicFieldType.create() }); + const valid = [ + DynamicField.createStringVal('test', 'taco'), + DynamicField.createIntVal('test', 9000), + ]; + const invalid = [ + 'test', + null, + [], + {}, + '', + NaN, + undefined, + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('DynamicFieldType encode tests', (t) => { + const field = new Field({ name: 'test', type: DynamicFieldType.create() }); + const codec = { encodeDynamicField: value => value.toJSON() }; + const samples = [ + { + input: DynamicField.createStringVal('test', 'taco'), + output: { name: 'test', string_val: 'taco' }, + }, + { + input: DynamicField.createBoolVal('test', true), + output: { name: 'test', bool_val: true }, + }, + { input: 0, output: null }, + { input: 1, output: null }, + { input: 2, output: null }, + { input: false, output: null }, + { input: '', output: null }, + { input: null, output: null }, + { input: undefined, output: null }, + { input: NaN, output: null }, + ]; + + helpers.encodeSamples(field, samples, t, codec); + t.end(); +}); + + +test('DynamicFieldType decode tests', (t) => { + const field = new Field({ name: 'test', type: DynamicFieldType.create() }); + const codec = { decodeDynamicField: value => DynamicField.fromObject(value) }; + const df = DynamicField.createStringVal('test1', 'taco'); + const samples = [ + { + input: { name: 'test', string_val: 'taco' }, + output: DynamicField.createStringVal('test', 'taco'), + }, + { + input: { name: 'test', bool_val: true }, + output: DynamicField.createBoolVal('test', true), + }, + { input: df, output: df }, + { input: null, output: null }, + ]; + + helpers.decodeSamples(field, samples, t, codec); + t.end(); +}); + + +test('DynamicFieldType decode(invalid) tests', (t) => { + const field = new Field({ name: 'test', type: DynamicFieldType.create() }); + const codec = { decodeDynamicField: value => DynamicField.fromObject(value) }; + const samples = [ + 'nope', + { name: 'test', nothing: true }, + { name: 'test' }, + false, + [], + {}, + '', + NaN, + undefined, + ]; + helpers.decodeInvalidSamples(field, samples, t, codec); + t.end(); +}); diff --git a/tests/types/FloatType.test.js b/tests/types/FloatType.test.js new file mode 100644 index 0000000..f457244 --- /dev/null +++ b/tests/types/FloatType.test.js @@ -0,0 +1,90 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import FloatType from '../../src/types/FloatType'; +import helpers from './helpers'; + +test('FloatType property tests', (t) => { + const floatType = FloatType.create(); + t.true(floatType instanceof Type); + t.true(floatType instanceof FloatType); + t.same(floatType, FloatType.create()); + t.true(floatType === FloatType.create()); + t.same(floatType.getTypeName(), TypeName.FLOAT); + t.same(floatType.getTypeValue(), TypeName.FLOAT.valueOf()); + t.same(floatType.isScalar(), true); + t.same(floatType.encodesToScalar(), true); + t.same(floatType.getDefault(), 0.0); + t.same(floatType.isBoolean(), false); + t.same(floatType.isBinary(), false); + t.same(floatType.isNumeric(), true); + t.same(floatType.isString(), false); + t.same(floatType.isMessage(), false); + t.same(floatType.allowedInSet(), true); + t.same(floatType.getMin(), Number.MIN_VALUE); + t.same(floatType.getMax(), Number.MAX_VALUE); + + try { + floatType.test = 1; + t.fail('floatType instance is mutable'); + } catch (e) { + t.pass('floatType instance is immutable'); + } + + t.end(); +}); + + +test('FloatType guard tests', (t) => { + const field = new Field({ name: 'test', type: FloatType.create() }); + const valid = [ + 0.0, 3.14159265358979323846, -3.14159265358979323846, Number.MIN_VALUE, Number.MAX_VALUE, + ]; + const invalid = [ + '0', '0.0', '3.14159265358979323846', '-3.14159265358979323846', null, [], {}, '', NaN, undefined, + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('FloatType encode tests', (t) => { + const field = new Field({ name: 'test', type: FloatType.create() }); + const samples = [ + { input: 0.0, output: 0.0 }, + { input: 3.14159265358979, output: 3.14159265358979 }, + { input: -3.14159265358979, output: -3.14159265358979 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: NaN, output: 0 }, + { input: '3.14159265358979', output: 3.14159265358979 }, + { input: '-3.14159265358979', output: -3.14159265358979 }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('FloatType decode tests', (t) => { + const field = new Field({ name: 'test', type: FloatType.create() }); + const samples = [ + { input: 0.0, output: 0.0 }, + { input: 3.14159265358979, output: 3.14159265358979 }, + { input: -3.14159265358979, output: -3.14159265358979 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: NaN, output: 0 }, + { input: '3.14159265358979', output: 3.14159265358979 }, + { input: '-3.14159265358979', output: -3.14159265358979 }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/GeoPointType.test.js b/tests/types/GeoPointType.test.js new file mode 100644 index 0000000..fe0a8d9 --- /dev/null +++ b/tests/types/GeoPointType.test.js @@ -0,0 +1,115 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import GeoPointType from '../../src/types/GeoPointType'; +import GeoPoint from '../../src/well-known/GeoPoint'; +import helpers from './helpers'; + +test('GeoPointType property tests', (t) => { + const geoPointType = GeoPointType.create(); + t.true(geoPointType instanceof Type); + t.true(geoPointType instanceof GeoPointType); + t.same(geoPointType, GeoPointType.create()); + t.true(geoPointType === GeoPointType.create()); + t.same(geoPointType.getTypeName(), TypeName.GEO_POINT); + t.same(geoPointType.getTypeValue(), TypeName.GEO_POINT.valueOf()); + t.same(geoPointType.isScalar(), false); + t.same(geoPointType.encodesToScalar(), false); + t.same(geoPointType.getDefault(), null); + t.same(geoPointType.isBoolean(), false); + t.same(geoPointType.isBinary(), false); + t.same(geoPointType.isNumeric(), false); + t.same(geoPointType.isString(), false); + t.same(geoPointType.isMessage(), false); + t.same(geoPointType.allowedInSet(), false); + + try { + geoPointType.test = 1; + t.fail('GeoPointType instance is mutable'); + } catch (e) { + t.pass('GeoPointType instance is immutable'); + } + + t.end(); +}); + + +test('GeoPointType guard tests', (t) => { + const field = new Field({ name: 'test', type: GeoPointType.create() }); + const valid = [ + GeoPoint.fromString('34.1789335,-118.347594'), + new GeoPoint(34.1789335, -118.347594), + ]; + const invalid = [ + '34.1789335,-118.347594', + null, + [], + {}, + '', + NaN, + undefined, + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('GeoPointType encode tests', (t) => { + const field = new Field({ name: 'test', type: GeoPointType.create() }); + const codec = { encodeGeoPoint: value => value.toJSON() }; + const samples = [ + { + input: GeoPoint.fromString('34.1789335,-118.347594'), + output: { type: 'Point', coordinates: [-118.347594, 34.1789335] }, + }, + { input: 0, output: null }, + { input: 1, output: null }, + { input: 2, output: null }, + { input: false, output: null }, + { input: '', output: null }, + { input: null, output: null }, + { input: undefined, output: null }, + { input: NaN, output: null }, + ]; + + helpers.encodeSamples(field, samples, t, codec); + t.end(); +}); + + +test('GeoPointType decode tests', (t) => { + const field = new Field({ name: 'test', type: GeoPointType.create() }); + const codec = { decodeGeoPoint: value => GeoPoint.fromObject(value) }; + const gp = GeoPoint.fromString('34.1789335,-118.347594'); + const samples = [ + { + input: { type: 'Point', coordinates: [-118.347594, 34.1789335] }, + output: gp, + }, + { input: gp, output: gp }, + { input: null, output: null }, + ]; + + helpers.decodeSamples(field, samples, t, codec); + t.end(); +}); + + +test('GeoPointType decode(invalid) tests', (t) => { + const field = new Field({ name: 'test', type: GeoPointType.create() }); + const codec = { decodeGeoPoint: value => GeoPoint.fromObject(value) }; + const samples = [ + 'nope', + { type: 'Point', coordinates: [-181, 91] }, + false, + [], + {}, + '', + NaN, + undefined, + ]; + helpers.decodeInvalidSamples(field, samples, t, codec); + t.end(); +}); diff --git a/tests/types/IdentifierType.test.js b/tests/types/IdentifierType.test.js new file mode 100644 index 0000000..b7cbf84 --- /dev/null +++ b/tests/types/IdentifierType.test.js @@ -0,0 +1,109 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import IdentifierType from '../../src/types/IdentifierType'; +import UuidIdentifier from '../../src/well-known/UuidIdentifier'; +import SampleUuidIdentifier from '../fixtures/well-known/SampleUuidIdentifier'; +import helpers from './helpers'; + +test('IdentifierType property tests', (t) => { + const identifierType = IdentifierType.create(); + t.true(identifierType instanceof Type); + t.true(identifierType instanceof IdentifierType); + t.same(identifierType, IdentifierType.create()); + t.true(identifierType === IdentifierType.create()); + t.same(identifierType.getTypeName(), TypeName.IDENTIFIER); + t.same(identifierType.getTypeValue(), TypeName.IDENTIFIER.valueOf()); + t.same(identifierType.isScalar(), false); + t.same(identifierType.encodesToScalar(), true); + t.same(identifierType.getDefault(), null); + t.same(identifierType.isBoolean(), false); + t.same(identifierType.isBinary(), false); + t.same(identifierType.isNumeric(), false); + t.same(identifierType.isString(), true); + t.same(identifierType.isMessage(), false); + t.same(identifierType.allowedInSet(), true); + + try { + identifierType.test = 1; + t.fail('IdentifierType instance is mutable'); + } catch (e) { + t.pass('IdentifierType instance is immutable'); + } + + t.end(); +}); + + +test('IdentifierType guard tests', (t) => { + const field = new Field({ name: 'test', type: IdentifierType.create(), classProto: SampleUuidIdentifier }); + const valid = [ + SampleUuidIdentifier.generate(), + SampleUuidIdentifier.fromString('4b268351-2445-4d98-a777-b461330d5c7f'), + new SampleUuidIdentifier('4b268351-2445-4d98-a777-b461330d5c7a'), + ]; + const invalid = [ + UuidIdentifier.generate(), + '4b268351-2445-4d98-a777-b461330d5c7f', + null, + [], + {}, + '', + NaN, + undefined, + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('IdentifierType encode tests', (t) => { + const field = new Field({ name: 'test', type: IdentifierType.create(), classProto: SampleUuidIdentifier }); + const id = SampleUuidIdentifier.generate(); + const samples = [ + { + input: SampleUuidIdentifier.fromString('4b268351-2445-4d98-a777-b461330d5c7f'), + output: '4b268351-2445-4d98-a777-b461330d5c7f', + }, + { input: id, output: id.toString() }, + { input: 0, output: null }, + { input: 1, output: null }, + { input: 2, output: null }, + { input: false, output: null }, + { input: '', output: null }, + { input: null, output: null }, + { input: undefined, output: null }, + { input: NaN, output: null }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('IdentifierType decode tests', (t) => { + const field = new Field({ name: 'test', type: IdentifierType.create(), classProto: SampleUuidIdentifier }); + const id = SampleUuidIdentifier.generate(); + const samples = [ + { + input: '4b268351-2445-4d98-a777-b461330d5c7f', + output: SampleUuidIdentifier.fromString('4b268351-2445-4d98-a777-b461330d5c7f'), + }, + { input: id.toString(), output: id }, + { input: id, output: id }, + { input: null, output: null }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); + + +test('IdentifierType decode(invalid) tests', (t) => { + const field = new Field({ name: 'test', type: IdentifierType.create(), classProto: SampleUuidIdentifier }); + const samples = ['nope', '4b268351-2445-4d98-a777-b461330d5c7fX', false, [], {}, '', NaN, undefined]; + helpers.decodeInvalidSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/IntEnumType.test.js b/tests/types/IntEnumType.test.js new file mode 100644 index 0000000..a797687 --- /dev/null +++ b/tests/types/IntEnumType.test.js @@ -0,0 +1,92 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import IntEnumType from '../../src/types/IntEnumType'; +import SampleIntEnum from '../fixtures/enums/SampleIntEnum'; +import SampleStringEnum from '../fixtures/enums/SampleStringEnum'; +import helpers from './helpers'; + +test('IntEnumType property tests', (t) => { + const intEnumType = IntEnumType.create(); + t.true(intEnumType instanceof Type); + t.true(intEnumType instanceof IntEnumType); + t.same(intEnumType, IntEnumType.create()); + t.true(intEnumType === IntEnumType.create()); + t.same(intEnumType.getTypeName(), TypeName.INT_ENUM); + t.same(intEnumType.getTypeValue(), TypeName.INT_ENUM.valueOf()); + t.same(intEnumType.isScalar(), false); + t.same(intEnumType.encodesToScalar(), true); + t.same(intEnumType.getDefault(), null); + t.same(intEnumType.isBoolean(), false); + t.same(intEnumType.isBinary(), false); + t.same(intEnumType.isNumeric(), true); + t.same(intEnumType.isString(), false); + t.same(intEnumType.isMessage(), false); + t.same(intEnumType.allowedInSet(), true); + t.same(intEnumType.getMin(), 0); + t.same(intEnumType.getMax(), 65535); + + try { + intEnumType.test = 1; + t.fail('IntEnumType instance is mutable'); + } catch (e) { + t.pass('IntEnumType instance is immutable'); + } + + t.end(); +}); + + +test('IntEnumType guard tests', (t) => { + const field = new Field({ name: 'test', type: IntEnumType.create(), classProto: SampleIntEnum }); + const valid = [SampleIntEnum.UNKNOWN, SampleIntEnum.ENUM1, SampleIntEnum.ENUM2]; + const invalid = [0, 1, 2, '0', '1', '2', null, [], {}, '', NaN, undefined, SampleStringEnum.UNKNOWN]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('IntEnumType encode tests', (t) => { + const field = new Field({ name: 'test', type: IntEnumType.create(), classProto: SampleIntEnum }); + const samples = [ + { input: SampleIntEnum.UNKNOWN, output: 0 }, + { input: SampleIntEnum.ENUM1, output: 1 }, + { input: SampleIntEnum.ENUM2, output: 2 }, + { input: 0, output: 0 }, + { input: 1, output: 0 }, + { input: 2, output: 0 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: NaN, output: 0 }, + { input: SampleStringEnum.UNKNOWN, output: 0 }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('IntEnumType decode tests', (t) => { + const field = new Field({ name: 'test', type: IntEnumType.create(), classProto: SampleIntEnum }); + const samples = [ + { input: 0, output: SampleIntEnum.UNKNOWN }, + { input: 1, output: SampleIntEnum.ENUM1 }, + { input: 2, output: SampleIntEnum.ENUM2 }, + { input: null, output: null }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); + + +test('IntEnumType decode(invalid) tests', (t) => { + const field = new Field({ name: 'test', type: IntEnumType.create(), classProto: SampleIntEnum }); + const samples = [3, false, [], {}, '', NaN, undefined, SampleStringEnum.UNKNOWN]; + helpers.decodeInvalidSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/IntType.test.js b/tests/types/IntType.test.js new file mode 100644 index 0000000..8173f7e --- /dev/null +++ b/tests/types/IntType.test.js @@ -0,0 +1,98 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import IntType from '../../src/types/IntType'; +import helpers from './helpers'; + +test('IntType property tests', (t) => { + const intType = IntType.create(); + t.true(intType instanceof Type); + t.true(intType instanceof IntType); + t.same(intType, IntType.create()); + t.true(intType === IntType.create()); + t.same(intType.getTypeName(), TypeName.INT); + t.same(intType.getTypeValue(), TypeName.INT.valueOf()); + t.same(intType.isScalar(), true); + t.same(intType.encodesToScalar(), true); + t.same(intType.getDefault(), 0); + t.same(intType.isBoolean(), false); + t.same(intType.isBinary(), false); + t.same(intType.isNumeric(), true); + t.same(intType.isString(), false); + t.same(intType.isMessage(), false); + t.same(intType.allowedInSet(), true); + t.same(intType.getMin(), 0); + t.same(intType.getMax(), 4294967295); + + try { + intType.test = 1; + t.fail('intType instance is mutable'); + } catch (e) { + t.pass('intType instance is immutable'); + } + + t.end(); +}); + + +test('IntType guard tests', (t) => { + const field = new Field({ name: 'test', type: IntType.create() }); + const valid = [0, 4294967295, 1, 4294967294]; + const invalid = [-1, 4294967296, '0', '4294967295', null, [], {}, '', NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('IntType guard (min/max) tests', (t) => { + const field = new Field({ name: 'test', type: IntType.create(), min: 5, max: 10 }); + const valid = [5, 6, 10, 9]; + const invalid = [4, 11]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('IntType encode tests', (t) => { + const field = new Field({ name: 'test', type: IntType.create() }); + const samples = [ + { input: 0, output: 0 }, + { input: 4294967295, output: 4294967295 }, + { input: 1, output: 1 }, + { input: 4294967294, output: 4294967294 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: NaN, output: 0 }, + { input: 3.14, output: 3 }, + { input: '3.14', output: 3 }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('IntType decode tests', (t) => { + const field = new Field({ name: 'test', type: IntType.create() }); + const samples = [ + { input: 0, output: 0 }, + { input: 4294967295, output: 4294967295 }, + { input: 1, output: 1 }, + { input: 4294967294, output: 4294967294 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: NaN, output: 0 }, + { input: 3.14, output: 3 }, + { input: '3.14', output: 3 }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/MediumBlobType.test.js b/tests/types/MediumBlobType.test.js new file mode 100644 index 0000000..2bb7612 --- /dev/null +++ b/tests/types/MediumBlobType.test.js @@ -0,0 +1,101 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import MediumBlobType from '../../src/types/MediumBlobType'; +import helpers from './helpers'; + +test('MediumBlobType property tests', (t) => { + const mediumBlobType = MediumBlobType.create(); + t.true(mediumBlobType instanceof Type); + t.true(mediumBlobType instanceof MediumBlobType); + t.same(mediumBlobType, MediumBlobType.create()); + t.true(mediumBlobType === MediumBlobType.create()); + t.same(mediumBlobType.getTypeName(), TypeName.MEDIUM_BLOB); + t.same(mediumBlobType.getTypeValue(), TypeName.MEDIUM_BLOB.valueOf()); + t.same(mediumBlobType.isScalar(), true); + t.same(mediumBlobType.encodesToScalar(), true); + t.same(mediumBlobType.getDefault(), null); + t.same(mediumBlobType.isBoolean(), false); + t.same(mediumBlobType.isBinary(), true); + t.same(mediumBlobType.isNumeric(), false); + t.same(mediumBlobType.isString(), true); + t.same(mediumBlobType.isMessage(), false); + t.same(mediumBlobType.allowedInSet(), false); + t.same(mediumBlobType.getMaxBytes(), 16777215); + + try { + mediumBlobType.test = 1; + t.fail('mediumBlobType instance is mutable'); + } catch (e) { + t.pass('mediumBlobType instance is immutable'); + } + + t.end(); +}); + + +test('MediumBlobType guard tests', (t) => { + const field = new Field({ name: 'test', type: MediumBlobType.create() }); + const valid = [ + 'test', + 'KOKVr8Kw4pahwrAp4pWv77i1IOKUu+KUgeKUuw==', + 'IGljZSDwn42mIHBvb3Ag8J+SqSBkb2gg8J+YsyA=', + '4LKgX+CyoA==', + ]; + const invalid = [-1, 1, true, false, null, [], {}, NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('MediumBlobType guard (min/max length) tests', (t) => { + const mediumBlobType = MediumBlobType.create(); + mediumBlobType.decodeFromBase64(false); + mediumBlobType.encodeToBase64(false); + + const field = new Field({ name: 'test', type: mediumBlobType, minLength: 5, maxLength: 10 }); + const valid = ['01234', '0123456789', '012345', '012345678']; + const invalid = ['0123', '01234567890']; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + + mediumBlobType.decodeFromBase64(true); + mediumBlobType.encodeToBase64(true); + t.end(); +}); + + +test('MediumBlobType encode tests', (t) => { + const field = new Field({ name: 'test', type: MediumBlobType.create() }); + const samples = [ + { input: 'test', output: 'dGVzdA==' }, + { input: 'homer simpson', output: 'aG9tZXIgc2ltcHNvbg==' }, + { input: '✓ à la mode', output: '4pyTIMOgIGxhIG1vZGU=' }, + { input: ' ice 🍦 poop 💩 doh 😳 ', output: 'aWNlIPCfjaYgcG9vcCDwn5KpIGRvaCDwn5iz' }, + { input: '(╯°□°)╯︵ ┻━┻', output: 'KOKVr8Kw4pahwrAp4pWv77i1IOKUu+KUgeKUuw==' }, + { input: 'ಠ_ಠ', output: '4LKgX+CyoA==' }, + { input: 'foo © bar 𝌆 baz', output: 'Zm9vIMKpIGJhciDwnYyGIGJheg==' }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('MediumBlobType decode tests', (t) => { + const field = new Field({ name: 'test', type: MediumBlobType.create() }); + const samples = [ + { input: 'dGVzdA==', output: 'test' }, + { input: 'aG9tZXIgc2ltcHNvbg==', output: 'homer simpson' }, + { input: '4pyTIMOgIGxhIG1vZGU=', output: '✓ à la mode' }, + { input: 'aWNlIPCfjaYgcG9vcCDwn5KpIGRvaCDwn5iz', output: 'ice 🍦 poop 💩 doh 😳' }, + { input: 'KOKVr8Kw4pahwrAp4pWv77i1IOKUu+KUgeKUuw==', output: '(╯°□°)╯︵ ┻━┻' }, + { input: '4LKgX+CyoA==', output: 'ಠ_ಠ' }, + { input: 'Zm9vIMKpIGJhciDwnYyGIGJheg==', output: 'foo © bar 𝌆 baz' }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/MediumIntType.test.js b/tests/types/MediumIntType.test.js new file mode 100644 index 0000000..645126c --- /dev/null +++ b/tests/types/MediumIntType.test.js @@ -0,0 +1,88 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import MediumIntType from '../../src/types/MediumIntType'; +import helpers from './helpers'; + +test('MediumIntType property tests', (t) => { + const mediumIntType = MediumIntType.create(); + t.true(mediumIntType instanceof Type); + t.true(mediumIntType instanceof MediumIntType); + t.same(mediumIntType, MediumIntType.create()); + t.true(mediumIntType === MediumIntType.create()); + t.same(mediumIntType.getTypeName(), TypeName.MEDIUM_INT); + t.same(mediumIntType.getTypeValue(), TypeName.MEDIUM_INT.valueOf()); + t.same(mediumIntType.isScalar(), true); + t.same(mediumIntType.encodesToScalar(), true); + t.same(mediumIntType.getDefault(), 0); + t.same(mediumIntType.isBoolean(), false); + t.same(mediumIntType.isBinary(), false); + t.same(mediumIntType.isNumeric(), true); + t.same(mediumIntType.isString(), false); + t.same(mediumIntType.isMessage(), false); + t.same(mediumIntType.allowedInSet(), true); + t.same(mediumIntType.getMin(), 0); + t.same(mediumIntType.getMax(), 16777215); + + try { + mediumIntType.test = 1; + t.fail('mediumIntType instance is mutable'); + } catch (e) { + t.pass('mediumIntType instance is immutable'); + } + + t.end(); +}); + + +test('MediumIntType guard tests', (t) => { + const field = new Field({ name: 'test', type: MediumIntType.create() }); + const valid = [0, 16777215, 1, 16777214]; + const invalid = [-1, 16777216, '0', '16777215', null, [], {}, '', NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('MediumIntType encode tests', (t) => { + const field = new Field({ name: 'test', type: MediumIntType.create() }); + const samples = [ + { input: 0, output: 0 }, + { input: 16777215, output: 16777215 }, + { input: 1, output: 1 }, + { input: 16777214, output: 16777214 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: NaN, output: 0 }, + { input: 3.14, output: 3 }, + { input: '3.14', output: 3 }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('MediumIntType decode tests', (t) => { + const field = new Field({ name: 'test', type: MediumIntType.create() }); + const samples = [ + { input: 0, output: 0 }, + { input: 16777215, output: 16777215 }, + { input: 1, output: 1 }, + { input: 16777214, output: 16777214 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: NaN, output: 0 }, + { input: 3.14, output: 3 }, + { input: '3.14', output: 3 }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/MediumTextType.test.js b/tests/types/MediumTextType.test.js new file mode 100644 index 0000000..26d36ec --- /dev/null +++ b/tests/types/MediumTextType.test.js @@ -0,0 +1,94 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import MediumTextType from '../../src/types/MediumTextType'; +import helpers from './helpers'; + +test('MediumTextType property tests', (t) => { + const mediumTextType = MediumTextType.create(); + t.true(mediumTextType instanceof Type); + t.true(mediumTextType instanceof MediumTextType); + t.same(mediumTextType, MediumTextType.create()); + t.true(mediumTextType === MediumTextType.create()); + t.same(mediumTextType.getTypeName(), TypeName.MEDIUM_TEXT); + t.same(mediumTextType.getTypeValue(), TypeName.MEDIUM_TEXT.valueOf()); + t.same(mediumTextType.isScalar(), true); + t.same(mediumTextType.encodesToScalar(), true); + t.same(mediumTextType.getDefault(), null); + t.same(mediumTextType.isBoolean(), false); + t.same(mediumTextType.isBinary(), false); + t.same(mediumTextType.isNumeric(), false); + t.same(mediumTextType.isString(), true); + t.same(mediumTextType.isMessage(), false); + t.same(mediumTextType.allowedInSet(), false); + t.same(mediumTextType.getMaxBytes(), 16777215); + + try { + mediumTextType.test = 1; + t.fail('mediumTextType instance is mutable'); + } catch (e) { + t.pass('mediumTextType instance is immutable'); + } + + t.end(); +}); + + +test('MediumTextType guard tests', (t) => { + const field = new Field({ name: 'test', type: MediumTextType.create() }); + const largeText = 'a'.repeat(field.getType().getMaxBytes()); + const valid = ['test', largeText, '(╯°□°)╯︵ ┻━┻', ' ice 🍦 poop 💩 doh 😳 ', 'ಠ_ಠ']; + const invalid = [-1, 1, `${largeText}b`, true, false, null, [], {}, NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('MediumTextType encode tests', (t) => { + const field = new Field({ name: 'test', type: MediumTextType.create() }); + const largeText = 'a'.repeat(field.getType().getMaxBytes()); + const samples = [ + { input: 'hello', output: 'hello' }, + { input: ' hello', output: 'hello' }, + { input: 'hello ', output: 'hello' }, + { input: ' hello ', output: 'hello' }, + { input: ' ', output: null }, + { input: largeText, output: largeText }, + { input: '(╯°□°)╯︵ ┻━┻', output: '(╯°□°)╯︵ ┻━┻' }, + { input: 'ಠ_ಠ', output: 'ಠ_ಠ' }, + { input: ' ice 🍦 poop 💩 doh 😳 ', output: 'ice 🍦 poop 💩 doh 😳' }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('MediumTextType decode tests', (t) => { + const field = new Field({ name: 'test', type: MediumTextType.create() }); + const largeText = 'a'.repeat(field.getType().getMaxBytes()); + const samples = [ + { input: 'hello', output: 'hello' }, + { input: ' hello', output: 'hello' }, + { input: 'hello ', output: 'hello' }, + { input: ' hello ', output: 'hello' }, + { input: ' ', output: null }, + { input: largeText, output: largeText }, + { input: '(╯°□°)╯︵ ┻━┻', output: '(╯°□°)╯︵ ┻━┻' }, + { input: 'ಠ_ಠ', output: 'ಠ_ಠ' }, + { input: ' ice 🍦 poop 💩 doh 😳 ', output: 'ice 🍦 poop 💩 doh 😳' }, + { input: false, output: 'false' }, + { input: true, output: 'true' }, + { input: '', output: null }, + { input: null, output: null }, + { input: undefined, output: null }, + { input: NaN, output: 'NaN' }, + { input: 3.14, output: '3.14' }, + { input: '3.14', output: '3.14' }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/MessageRefType.test.js b/tests/types/MessageRefType.test.js new file mode 100644 index 0000000..9f2209b --- /dev/null +++ b/tests/types/MessageRefType.test.js @@ -0,0 +1,115 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import MessageRefType from '../../src/types/MessageRefType'; +import MessageRef from '../../src/MessageRef'; +import helpers from './helpers'; + +test('MessageRefType property tests', (t) => { + const messageRefType = MessageRefType.create(); + t.true(messageRefType instanceof Type); + t.true(messageRefType instanceof MessageRefType); + t.same(messageRefType, MessageRefType.create()); + t.true(messageRefType === MessageRefType.create()); + t.same(messageRefType.getTypeName(), TypeName.MESSAGE_REF); + t.same(messageRefType.getTypeValue(), TypeName.MESSAGE_REF.valueOf()); + t.same(messageRefType.isScalar(), false); + t.same(messageRefType.encodesToScalar(), false); + t.same(messageRefType.getDefault(), null); + t.same(messageRefType.isBoolean(), false); + t.same(messageRefType.isBinary(), false); + t.same(messageRefType.isNumeric(), false); + t.same(messageRefType.isString(), false); + t.same(messageRefType.isMessage(), false); + t.same(messageRefType.allowedInSet(), true); + + try { + messageRefType.test = 1; + t.fail('MessageRefType instance is mutable'); + } catch (e) { + t.pass('MessageRefType instance is immutable'); + } + + t.end(); +}); + + +test('MessageRefType guard tests', (t) => { + const field = new Field({ name: 'test', type: MessageRefType.create() }); + const valid = [ + MessageRef.fromString('acme:blog:node:article:123#tag'), + MessageRef.fromString('acme:blog::article:2015/12/25/test:Still_The:id#2015.q4'), + ]; + const invalid = [ + 'acme:blog:node:article:123#tag', + null, + [], + {}, + '', + NaN, + undefined, + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('MessageRefType encode tests', (t) => { + const field = new Field({ name: 'test', type: MessageRefType.create() }); + const codec = { encodeMessageRef: value => value.toString() }; + const samples = [ + { + input: MessageRef.fromString('acme:blog:node:article:123#tag'), + output: 'acme:blog:node:article:123#tag', + }, + { input: 0, output: null }, + { input: 1, output: null }, + { input: 2, output: null }, + { input: false, output: null }, + { input: '', output: null }, + { input: null, output: null }, + { input: undefined, output: null }, + { input: NaN, output: null }, + ]; + + helpers.encodeSamples(field, samples, t, codec); + t.end(); +}); + + +test('MessageRefType decode tests', (t) => { + const field = new Field({ name: 'test', type: MessageRefType.create() }); + const codec = { decodeMessageRef: value => MessageRef.fromObject(value) }; + const ref = MessageRef.fromString('acme:blog::article:2015/12/25/test:Still_The:id#2015.Q4'); + const samples = [ + { + input: ref.toObject(), + output: ref, + }, + { input: ref, output: ref }, + { input: null, output: null }, + ]; + + helpers.decodeSamples(field, samples, t, codec); + t.end(); +}); + + +test('MessageRefType decode(invalid) tests', (t) => { + const field = new Field({ name: 'test', type: MessageRefType.create() }); + const codec = { decodeMessageRef: value => MessageRef.fromObject(value) }; + const samples = [ + 'nope', + { curie: 'invalid', id: 'what' }, + false, + [], + {}, + '', + NaN, + undefined, + ]; + helpers.decodeInvalidSamples(field, samples, t, codec); + t.end(); +}); diff --git a/tests/types/MessageType.test.js b/tests/types/MessageType.test.js new file mode 100644 index 0000000..9988dc2 --- /dev/null +++ b/tests/types/MessageType.test.js @@ -0,0 +1,162 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import MessageType from '../../src/types/MessageType'; +import Message from '../../src/Message'; +import helpers from './helpers'; +import SampleMessageV1 from '../fixtures/SampleMessageV1'; +import SampleOtherMessageV1 from '../fixtures/SampleOtherMessageV1'; + +test('MessageType property tests', (t) => { + const messageType = MessageType.create(); + t.true(messageType instanceof Type); + t.true(messageType instanceof MessageType); + t.same(messageType, MessageType.create()); + t.true(messageType === MessageType.create()); + t.same(messageType.getTypeName(), TypeName.MESSAGE); + t.same(messageType.getTypeValue(), TypeName.MESSAGE.valueOf()); + t.same(messageType.isScalar(), false); + t.same(messageType.encodesToScalar(), false); + t.same(messageType.getDefault(), null); + t.same(messageType.isBoolean(), false); + t.same(messageType.isBinary(), false); + t.same(messageType.isNumeric(), false); + t.same(messageType.isString(), false); + t.same(messageType.isMessage(), true); + t.same(messageType.allowedInSet(), false); + + try { + messageType.test = 1; + t.fail('MessageType instance is mutable'); + } catch (e) { + t.pass('MessageType instance is immutable'); + } + + t.end(); +}); + + +test('MessageType guard tests', (t) => { + const field = new Field({ name: 'test', type: MessageType.create(), classProto: SampleMessageV1 }); + const valid = [ + SampleMessageV1.create().set('string_single', 'test'), + SampleMessageV1.create().set('mixin_int', 5), + SampleOtherMessageV1.create(), + ]; + const invalid = [ + 'test', + null, + [], + {}, + '', + NaN, + undefined, + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('MessageType guard (anyOfCuries) tests', (t) => { + const field = new Field({ + name: 'test', + type: MessageType.create(), + classProto: SampleMessageV1, + anyOfCuries: ['gdbots:pbj.tests::sample-message'], + }); + const valid = [SampleMessageV1.create()]; + const invalid = [SampleOtherMessageV1.create()]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('MessageType encode tests', (t) => { + const field = new Field({ name: 'test', type: MessageType.create(), classProto: SampleMessageV1 }); + const codec = { encodeMessage: value => value.toJSON() }; + const samples = [ + { + input: SampleMessageV1.create().set('string_single', 'test'), + output: { + _schema: 'pbj:gdbots:pbj.tests::sample-message:1-0-0', + mixin_int: 0, + string_single: 'test', + }, + }, + { + input: SampleMessageV1.create().set('mixin_int', 5), + output: { + _schema: 'pbj:gdbots:pbj.tests::sample-message:1-0-0', + mixin_int: 5, + }, + }, + { input: 0, output: null }, + { input: 1, output: null }, + { input: 2, output: null }, + { input: false, output: null }, + { input: '', output: null }, + { input: null, output: null }, + { input: undefined, output: null }, + { input: NaN, output: null }, + ]; + + helpers.encodeSamples(field, samples, t, codec); + t.end(); +}); + + +test('MessageType decode tests', (t) => { + const field = new Field({ name: 'test', type: MessageType.create(), classProto: SampleMessageV1 }); + const codec = { decodeMessage: value => Message.fromObject(value) }; + const message = SampleMessageV1.create().set('string_single', 'test'); + const samples = [ + { + input: { + _schema: 'pbj:gdbots:pbj.tests::sample-message:1-0-0', + mixin_int: 0, + string_single: 'test', + }, + output: SampleMessageV1.create().set('string_single', 'test'), + }, + { + input: { + _schema: 'pbj:gdbots:pbj.tests::sample-message:1-0-0', + mixin_int: 5, + }, + output: SampleMessageV1.create().set('mixin_int', 5), + }, + { input: message, output: message }, + { input: null, output: null }, + ]; + + helpers.decodeSamples(field, samples, t, codec); + t.end(); +}); + + +test('MessageType decode(invalid) tests', (t) => { + const field = new Field({ + name: 'test', + type: MessageType.create(), + classProto: SampleMessageV1, + anyOfCuries: ['gdbots:pbj.tests::sample-message'], + }); + const codec = { decodeMessage: value => Message.fromObject(value) }; + const samples = [ + SampleMessageV1, + 'nope', + { name: 'test', nothing: true }, + { name: 'test' }, + false, + [], + {}, + '', + NaN, + undefined, + ]; + helpers.decodeInvalidSamples(field, samples, t, codec); + t.end(); +}); diff --git a/tests/types/MicrotimeType.test.js b/tests/types/MicrotimeType.test.js new file mode 100644 index 0000000..6000ac9 --- /dev/null +++ b/tests/types/MicrotimeType.test.js @@ -0,0 +1,107 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import MicrotimeType from '../../src/types/MicrotimeType'; +import Microtime from '../../src/well-known/Microtime'; +import helpers from './helpers'; + +test('MicrotimeType property tests', (t) => { + const microtimeType = MicrotimeType.create(); + t.true(microtimeType instanceof Type); + t.true(microtimeType instanceof MicrotimeType); + t.same(microtimeType, MicrotimeType.create()); + t.true(microtimeType === MicrotimeType.create()); + t.same(microtimeType.getTypeName(), TypeName.MICROTIME); + t.same(microtimeType.getTypeValue(), TypeName.MICROTIME.valueOf()); + t.same(microtimeType.isScalar(), false); + t.same(microtimeType.encodesToScalar(), true); + t.true(microtimeType.getDefault() instanceof Microtime); + t.same(microtimeType.isBoolean(), false); + t.same(microtimeType.isBinary(), false); + t.same(microtimeType.isNumeric(), true); + t.same(microtimeType.isString(), false); + t.same(microtimeType.isMessage(), false); + t.same(microtimeType.allowedInSet(), true); + + try { + microtimeType.test = 1; + t.fail('MicrotimeType instance is mutable'); + } catch (e) { + t.pass('MicrotimeType instance is immutable'); + } + + t.end(); +}); + + +test('MicrotimeType guard tests', (t) => { + const field = new Field({ name: 'test', type: MicrotimeType.create() }); + const valid = [ + Microtime.create(), + Microtime.fromString('1495766080123456'), + new Microtime('1495766080123456'), + ]; + const invalid = [ + '1495766080123456', + null, + [], + {}, + '', + NaN, + undefined, + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('MicrotimeType encode tests', (t) => { + const field = new Field({ name: 'test', type: MicrotimeType.create() }); + const mtime = Microtime.create(); + const samples = [ + { + input: Microtime.fromString('1495766080123456'), + output: '1495766080123456', + }, + { input: mtime, output: mtime.toString() }, + { input: 0, output: null }, + { input: 1, output: null }, + { input: 2, output: null }, + { input: false, output: null }, + { input: '', output: null }, + { input: null, output: null }, + { input: undefined, output: null }, + { input: NaN, output: null }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('MicrotimeType decode tests', (t) => { + const field = new Field({ name: 'test', type: MicrotimeType.create() }); + const mtime = Microtime.create(); + const samples = [ + { + input: '1495766080123456', + output: Microtime.fromString('1495766080123456'), + }, + { input: mtime.toString(), output: mtime }, + { input: mtime, output: mtime }, + { input: null, output: null }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); + + +test('MicrotimeType decode(invalid) tests', (t) => { + const field = new Field({ name: 'test', type: MicrotimeType.create() }); + const samples = ['nope', '1495766080', false, [], {}, '', NaN, undefined]; + helpers.decodeInvalidSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/SignedBigIntType.test.js b/tests/types/SignedBigIntType.test.js new file mode 100644 index 0000000..58aee81 --- /dev/null +++ b/tests/types/SignedBigIntType.test.js @@ -0,0 +1,106 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import SignedBigIntType from '../../src/types/SignedBigIntType'; +import BigNumber from '../../src/well-known/BigNumber'; +import helpers from './helpers'; + +test('SignedBigIntType property tests', (t) => { + const signedBigIntType = SignedBigIntType.create(); + t.true(signedBigIntType instanceof Type); + t.true(signedBigIntType instanceof SignedBigIntType); + t.same(signedBigIntType, SignedBigIntType.create()); + t.true(signedBigIntType === SignedBigIntType.create()); + t.same(signedBigIntType.getTypeName(), TypeName.SIGNED_BIG_INT); + t.same(signedBigIntType.getTypeValue(), TypeName.SIGNED_BIG_INT.valueOf()); + t.same(signedBigIntType.isScalar(), false); + t.same(signedBigIntType.encodesToScalar(), true); + t.same(signedBigIntType.getDefault(), new BigNumber(0)); + t.same(signedBigIntType.isBoolean(), false); + t.same(signedBigIntType.isBinary(), false); + t.same(signedBigIntType.isNumeric(), true); + t.same(signedBigIntType.isString(), false); + t.same(signedBigIntType.isMessage(), false); + t.same(signedBigIntType.allowedInSet(), true); + + try { + signedBigIntType.test = 1; + t.fail('SignedBigIntType instance is mutable'); + } catch (e) { + t.pass('SignedBigIntType instance is immutable'); + } + + t.end(); +}); + + +test('SignedBigIntType guard tests', (t) => { + const field = new Field({ name: 'test', type: SignedBigIntType.create() }); + const valid = [ + new BigNumber(0), + new BigNumber('-9223372036854775808'), + new BigNumber('-9223372036854775807'), + new BigNumber('9223372036854775807'), + new BigNumber('9223372036854775806'), + ]; + const invalid = [ + -1, + new BigNumber('-9223372036854775809'), + new BigNumber('9223372036854775808'), + '0', + '1', + '2', + null, + [], + {}, + '', + NaN, + undefined, + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('SignedBigIntType encode tests', (t) => { + const field = new Field({ name: 'test', type: SignedBigIntType.create() }); + const samples = [ + { input: new BigNumber('-9223372036854775808'), output: '-9223372036854775808' }, + { input: new BigNumber('-9223372036854775807.111'), output: '-9223372036854775807' }, + { input: new BigNumber('9223372036854775807'), output: '9223372036854775807' }, + { input: new BigNumber('9223372036854775806.111'), output: '9223372036854775806' }, + { input: new BigNumber(1), output: '1' }, + { input: new BigNumber(1.44444), output: '1' }, + { input: 0, output: '0' }, + { input: 1, output: '0' }, + { input: 2, output: '0' }, + { input: false, output: '0' }, + { input: '', output: '0' }, + { input: null, output: '0' }, + { input: undefined, output: '0' }, + { input: NaN, output: '0' }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('SignedBigIntType decode tests', (t) => { + const field = new Field({ name: 'test', type: SignedBigIntType.create() }); + const samples = [ + { input: '-9223372036854775808', output: new BigNumber('-9223372036854775808') }, + { input: '-9223372036854775807', output: new BigNumber('-9223372036854775807') }, + { input: '9223372036854775807', output: new BigNumber('9223372036854775807') }, + { input: '9223372036854775806', output: new BigNumber('9223372036854775806') }, + { input: '0', output: new BigNumber('0') }, + { input: '0', output: new BigNumber(0) }, + { input: new BigNumber(1), output: new BigNumber(1) }, + { input: null, output: null }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/SignedIntType.test.js b/tests/types/SignedIntType.test.js new file mode 100644 index 0000000..19d55d8 --- /dev/null +++ b/tests/types/SignedIntType.test.js @@ -0,0 +1,94 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import SignedIntType from '../../src/types/SignedIntType'; +import helpers from './helpers'; + +test('SignedIntType property tests', (t) => { + const signedIntType = SignedIntType.create(); + t.true(signedIntType instanceof Type); + t.true(signedIntType instanceof SignedIntType); + t.same(signedIntType, SignedIntType.create()); + t.true(signedIntType === SignedIntType.create()); + t.same(signedIntType.getTypeName(), TypeName.SIGNED_INT); + t.same(signedIntType.getTypeValue(), TypeName.SIGNED_INT.valueOf()); + t.same(signedIntType.isScalar(), true); + t.same(signedIntType.encodesToScalar(), true); + t.same(signedIntType.getDefault(), 0); + t.same(signedIntType.isBoolean(), false); + t.same(signedIntType.isBinary(), false); + t.same(signedIntType.isNumeric(), true); + t.same(signedIntType.isString(), false); + t.same(signedIntType.isMessage(), false); + t.same(signedIntType.allowedInSet(), true); + t.same(signedIntType.getMin(), -2147483648); + t.same(signedIntType.getMax(), 2147483647); + + try { + signedIntType.test = 1; + t.fail('signedIntType instance is mutable'); + } catch (e) { + t.pass('signedIntType instance is immutable'); + } + + t.end(); +}); + + +test('SignedIntType guard tests', (t) => { + const field = new Field({ name: 'test', type: SignedIntType.create() }); + const valid = [0, -2147483648, 2147483647, -2147483647, 2147483646]; + const invalid = [-2147483649, 2147483648, '-2147483648', '2147483647', null, [], {}, '', NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('SignedIntType encode tests', (t) => { + const field = new Field({ name: 'test', type: SignedIntType.create() }); + const samples = [ + { input: -2147483648, output: -2147483648 }, + { input: 2147483647, output: 2147483647 }, + { input: -2147483647, output: -2147483647 }, + { input: 2147483646, output: 2147483646 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: 0, output: 0 }, + { input: NaN, output: 0 }, + { input: 3.14, output: 3 }, + { input: -3.14, output: -3 }, + { input: '3.14', output: 3 }, + { input: '-3.14', output: -3 }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('SignedIntType decode tests', (t) => { + const field = new Field({ name: 'test', type: SignedIntType.create() }); + const samples = [ + { input: -2147483648, output: -2147483648 }, + { input: 2147483647, output: 2147483647 }, + { input: -2147483647, output: -2147483647 }, + { input: 2147483646, output: 2147483646 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: 0, output: 0 }, + { input: NaN, output: 0 }, + { input: 3.14, output: 3 }, + { input: -3.14, output: -3 }, + { input: '3.14', output: 3 }, + { input: '-3.14', output: -3 }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/SignedMediumIntType.test.js b/tests/types/SignedMediumIntType.test.js new file mode 100644 index 0000000..c05c0b1 --- /dev/null +++ b/tests/types/SignedMediumIntType.test.js @@ -0,0 +1,94 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import SignedMediumIntType from '../../src/types/SignedMediumIntType'; +import helpers from './helpers'; + +test('SignedMediumIntType property tests', (t) => { + const signedMediumIntType = SignedMediumIntType.create(); + t.true(signedMediumIntType instanceof Type); + t.true(signedMediumIntType instanceof SignedMediumIntType); + t.same(signedMediumIntType, SignedMediumIntType.create()); + t.true(signedMediumIntType === SignedMediumIntType.create()); + t.same(signedMediumIntType.getTypeName(), TypeName.SIGNED_MEDIUM_INT); + t.same(signedMediumIntType.getTypeValue(), TypeName.SIGNED_MEDIUM_INT.valueOf()); + t.same(signedMediumIntType.isScalar(), true); + t.same(signedMediumIntType.encodesToScalar(), true); + t.same(signedMediumIntType.getDefault(), 0); + t.same(signedMediumIntType.isBoolean(), false); + t.same(signedMediumIntType.isBinary(), false); + t.same(signedMediumIntType.isNumeric(), true); + t.same(signedMediumIntType.isString(), false); + t.same(signedMediumIntType.isMessage(), false); + t.same(signedMediumIntType.allowedInSet(), true); + t.same(signedMediumIntType.getMin(), -8388608); + t.same(signedMediumIntType.getMax(), 8388607); + + try { + signedMediumIntType.test = 1; + t.fail('signedMediumIntType instance is mutable'); + } catch (e) { + t.pass('signedMediumIntType instance is immutable'); + } + + t.end(); +}); + + +test('SignedMediumIntType guard tests', (t) => { + const field = new Field({ name: 'test', type: SignedMediumIntType.create() }); + const valid = [0, -8388608, 8388607, -8388607, 8388606]; + const invalid = [-8388609, 8388608, '-8388608', '8388607', null, [], {}, '', NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('SignedMediumIntType encode tests', (t) => { + const field = new Field({ name: 'test', type: SignedMediumIntType.create() }); + const samples = [ + { input: -8388608, output: -8388608 }, + { input: 8388607, output: 8388607 }, + { input: -8388607, output: -8388607 }, + { input: 8388606, output: 8388606 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: 0, output: 0 }, + { input: NaN, output: 0 }, + { input: 3.14, output: 3 }, + { input: -3.14, output: -3 }, + { input: '3.14', output: 3 }, + { input: '-3.14', output: -3 }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('SignedMediumIntType decode tests', (t) => { + const field = new Field({ name: 'test', type: SignedMediumIntType.create() }); + const samples = [ + { input: -8388608, output: -8388608 }, + { input: 8388607, output: 8388607 }, + { input: -8388607, output: -8388607 }, + { input: 8388606, output: 8388606 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: 0, output: 0 }, + { input: NaN, output: 0 }, + { input: 3.14, output: 3 }, + { input: -3.14, output: -3 }, + { input: '3.14', output: 3 }, + { input: '-3.14', output: -3 }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/SignedSmallIntType.test.js b/tests/types/SignedSmallIntType.test.js new file mode 100644 index 0000000..0314e60 --- /dev/null +++ b/tests/types/SignedSmallIntType.test.js @@ -0,0 +1,94 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import SignedSmallIntType from '../../src/types/SignedSmallIntType'; +import helpers from './helpers'; + +test('SignedSmallIntType property tests', (t) => { + const signedSmallIntType = SignedSmallIntType.create(); + t.true(signedSmallIntType instanceof Type); + t.true(signedSmallIntType instanceof SignedSmallIntType); + t.same(signedSmallIntType, SignedSmallIntType.create()); + t.true(signedSmallIntType === SignedSmallIntType.create()); + t.same(signedSmallIntType.getTypeName(), TypeName.SIGNED_SMALL_INT); + t.same(signedSmallIntType.getTypeValue(), TypeName.SIGNED_SMALL_INT.valueOf()); + t.same(signedSmallIntType.isScalar(), true); + t.same(signedSmallIntType.encodesToScalar(), true); + t.same(signedSmallIntType.getDefault(), 0); + t.same(signedSmallIntType.isBoolean(), false); + t.same(signedSmallIntType.isBinary(), false); + t.same(signedSmallIntType.isNumeric(), true); + t.same(signedSmallIntType.isString(), false); + t.same(signedSmallIntType.isMessage(), false); + t.same(signedSmallIntType.allowedInSet(), true); + t.same(signedSmallIntType.getMin(), -32768); + t.same(signedSmallIntType.getMax(), 32767); + + try { + signedSmallIntType.test = 1; + t.fail('SignedSmallIntType instance is mutable'); + } catch (e) { + t.pass('SignedSmallIntType instance is immutable'); + } + + t.end(); +}); + + +test('SignedSmallIntType guard tests', (t) => { + const field = new Field({ name: 'test', type: SignedSmallIntType.create() }); + const valid = [0, -32768, 32767, -32767, 32766]; + const invalid = [-32769, 32768, '-32768', '32767', null, [], {}, '', NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('SignedSmallIntType encode tests', (t) => { + const field = new Field({ name: 'test', type: SignedSmallIntType.create() }); + const samples = [ + { input: -32768, output: -32768 }, + { input: 32767, output: 32767 }, + { input: -32767, output: -32767 }, + { input: 32766, output: 32766 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: 0, output: 0 }, + { input: NaN, output: 0 }, + { input: 3.14, output: 3 }, + { input: -3.14, output: -3 }, + { input: '3.14', output: 3 }, + { input: '-3.14', output: -3 }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('SignedSmallIntType decode tests', (t) => { + const field = new Field({ name: 'test', type: SignedSmallIntType.create() }); + const samples = [ + { input: -32768, output: -32768 }, + { input: 32767, output: 32767 }, + { input: -32767, output: -32767 }, + { input: 32766, output: 32766 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: 0, output: 0 }, + { input: NaN, output: 0 }, + { input: 3.14, output: 3 }, + { input: -3.14, output: -3 }, + { input: '3.14', output: 3 }, + { input: '-3.14', output: -3 }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/SignedTinyIntType.test.js b/tests/types/SignedTinyIntType.test.js new file mode 100644 index 0000000..450b696 --- /dev/null +++ b/tests/types/SignedTinyIntType.test.js @@ -0,0 +1,94 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import SignedTinyIntType from '../../src/types/SignedTinyIntType'; +import helpers from './helpers'; + +test('SignedTinyIntType property tests', (t) => { + const signedTinyIntType = SignedTinyIntType.create(); + t.true(signedTinyIntType instanceof Type); + t.true(signedTinyIntType instanceof SignedTinyIntType); + t.same(signedTinyIntType, SignedTinyIntType.create()); + t.true(signedTinyIntType === SignedTinyIntType.create()); + t.same(signedTinyIntType.getTypeName(), TypeName.SIGNED_TINY_INT); + t.same(signedTinyIntType.getTypeValue(), TypeName.SIGNED_TINY_INT.valueOf()); + t.same(signedTinyIntType.isScalar(), true); + t.same(signedTinyIntType.encodesToScalar(), true); + t.same(signedTinyIntType.getDefault(), 0); + t.same(signedTinyIntType.isBoolean(), false); + t.same(signedTinyIntType.isBinary(), false); + t.same(signedTinyIntType.isNumeric(), true); + t.same(signedTinyIntType.isString(), false); + t.same(signedTinyIntType.isMessage(), false); + t.same(signedTinyIntType.allowedInSet(), true); + t.same(signedTinyIntType.getMin(), -128); + t.same(signedTinyIntType.getMax(), 127); + + try { + signedTinyIntType.test = 1; + t.fail('signedTinyIntType instance is mutable'); + } catch (e) { + t.pass('signedTinyIntType instance is immutable'); + } + + t.end(); +}); + + +test('SignedTinyIntType guard tests', (t) => { + const field = new Field({ name: 'test', type: SignedTinyIntType.create() }); + const valid = [0, -128, 127, -127, 126]; + const invalid = [-129, 128, '-128', '127', null, [], {}, '', NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('SignedTinyIntType encode tests', (t) => { + const field = new Field({ name: 'test', type: SignedTinyIntType.create() }); + const samples = [ + { input: -128, output: -128 }, + { input: 127, output: 127 }, + { input: -127, output: -127 }, + { input: 126, output: 126 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: 0, output: 0 }, + { input: NaN, output: 0 }, + { input: 3.14, output: 3 }, + { input: -3.14, output: -3 }, + { input: '3.14', output: 3 }, + { input: '-3.14', output: -3 }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('SignedTinyIntType decode tests', (t) => { + const field = new Field({ name: 'test', type: SignedTinyIntType.create() }); + const samples = [ + { input: -128, output: -128 }, + { input: 127, output: 127 }, + { input: -127, output: -127 }, + { input: 126, output: 126 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: 0, output: 0 }, + { input: NaN, output: 0 }, + { input: 3.14, output: 3 }, + { input: -3.14, output: -3 }, + { input: '3.14', output: 3 }, + { input: '-3.14', output: -3 }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/SmallIntType.test.js b/tests/types/SmallIntType.test.js new file mode 100644 index 0000000..73ceaa6 --- /dev/null +++ b/tests/types/SmallIntType.test.js @@ -0,0 +1,88 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import SmallIntType from '../../src/types/SmallIntType'; +import helpers from './helpers'; + +test('SmallIntType property tests', (t) => { + const smallIntType = SmallIntType.create(); + t.true(smallIntType instanceof Type); + t.true(smallIntType instanceof SmallIntType); + t.same(smallIntType, SmallIntType.create()); + t.true(smallIntType === SmallIntType.create()); + t.same(smallIntType.getTypeName(), TypeName.SMALL_INT); + t.same(smallIntType.getTypeValue(), TypeName.SMALL_INT.valueOf()); + t.same(smallIntType.isScalar(), true); + t.same(smallIntType.encodesToScalar(), true); + t.same(smallIntType.getDefault(), 0); + t.same(smallIntType.isBoolean(), false); + t.same(smallIntType.isBinary(), false); + t.same(smallIntType.isNumeric(), true); + t.same(smallIntType.isString(), false); + t.same(smallIntType.isMessage(), false); + t.same(smallIntType.allowedInSet(), true); + t.same(smallIntType.getMin(), 0); + t.same(smallIntType.getMax(), 65535); + + try { + smallIntType.test = 1; + t.fail('SmallIntType instance is mutable'); + } catch (e) { + t.pass('SmallIntType instance is immutable'); + } + + t.end(); +}); + + +test('SmallIntType guard tests', (t) => { + const field = new Field({ name: 'test', type: SmallIntType.create() }); + const valid = [0, 65535, 1, 65534]; + const invalid = [-1, 65536, '0', '65535', null, [], {}, '', NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('SmallIntType encode tests', (t) => { + const field = new Field({ name: 'test', type: SmallIntType.create() }); + const samples = [ + { input: 0, output: 0 }, + { input: 65535, output: 65535 }, + { input: 1, output: 1 }, + { input: 65534, output: 65534 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: NaN, output: 0 }, + { input: 3.14, output: 3 }, + { input: '3.14', output: 3 }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('SmallIntType decode tests', (t) => { + const field = new Field({ name: 'test', type: SmallIntType.create() }); + const samples = [ + { input: 0, output: 0 }, + { input: 65535, output: 65535 }, + { input: 1, output: 1 }, + { input: 65534, output: 65534 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: NaN, output: 0 }, + { input: 3.14, output: 3 }, + { input: '3.14', output: 3 }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/StringEnumType.test.js b/tests/types/StringEnumType.test.js new file mode 100644 index 0000000..7e9f653 --- /dev/null +++ b/tests/types/StringEnumType.test.js @@ -0,0 +1,90 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import StringEnumType from '../../src/types/StringEnumType'; +import SampleIntEnum from '../fixtures/enums/SampleIntEnum'; +import SampleStringEnum from '../fixtures/enums/SampleStringEnum'; +import helpers from './helpers'; + +test('StringEnumType property tests', (t) => { + const stringEnumType = StringEnumType.create(); + t.true(stringEnumType instanceof Type); + t.true(stringEnumType instanceof StringEnumType); + t.same(stringEnumType, StringEnumType.create()); + t.true(stringEnumType === StringEnumType.create()); + t.same(stringEnumType.getTypeName(), TypeName.STRING_ENUM); + t.same(stringEnumType.getTypeValue(), TypeName.STRING_ENUM.valueOf()); + t.same(stringEnumType.isScalar(), false); + t.same(stringEnumType.encodesToScalar(), true); + t.same(stringEnumType.getDefault(), null); + t.same(stringEnumType.isBoolean(), false); + t.same(stringEnumType.isBinary(), false); + t.same(stringEnumType.isNumeric(), false); + t.same(stringEnumType.isString(), true); + t.same(stringEnumType.isMessage(), false); + t.same(stringEnumType.allowedInSet(), true); + t.same(stringEnumType.getMaxBytes(), 100); + + try { + stringEnumType.test = 1; + t.fail('StringEnumType instance is mutable'); + } catch (e) { + t.pass('StringEnumType instance is immutable'); + } + + t.end(); +}); + + +test('StringEnumType guard tests', (t) => { + const field = new Field({ name: 'test', type: StringEnumType.create(), classProto: SampleStringEnum }); + const valid = [SampleStringEnum.UNKNOWN, SampleStringEnum.ENUM1, SampleStringEnum.ENUM2]; + const invalid = [0, 1, 2, '0', '1', '2', null, [], {}, '', NaN, undefined, SampleIntEnum.UNKNOWN]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('StringEnumType encode tests', (t) => { + const field = new Field({ name: 'test', type: StringEnumType.create(), classProto: SampleStringEnum }); + const samples = [ + { input: SampleStringEnum.UNKNOWN, output: 'unknown' }, + { input: SampleStringEnum.ENUM1, output: 'val1' }, + { input: SampleStringEnum.ENUM2, output: 'val2' }, + { input: 0, output: null }, + { input: 1, output: null }, + { input: 2, output: null }, + { input: false, output: null }, + { input: '', output: null }, + { input: null, output: null }, + { input: undefined, output: null }, + { input: NaN, output: null }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('StringEnumType decode tests', (t) => { + const field = new Field({ name: 'test', type: StringEnumType.create(), classProto: SampleStringEnum }); + const samples = [ + { input: 'unknown', output: SampleStringEnum.UNKNOWN }, + { input: 'val1', output: SampleStringEnum.ENUM1 }, + { input: 'val2', output: SampleStringEnum.ENUM2 }, + { input: null, output: null }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); + + +test('StringEnumType decode(invalid) tests', (t) => { + const field = new Field({ name: 'test', type: StringEnumType.create(), classProto: SampleStringEnum }); + const samples = ['nope', false, [], {}, '', NaN, undefined, SampleIntEnum.UNKNOWN]; + helpers.decodeInvalidSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/StringType.test.js b/tests/types/StringType.test.js new file mode 100644 index 0000000..127ddd1 --- /dev/null +++ b/tests/types/StringType.test.js @@ -0,0 +1,333 @@ +import test from 'tape'; +import Format from '../../src/enums/Format'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import StringType from '../../src/types/StringType'; +import helpers from './helpers'; + +test('StringType property tests', (t) => { + const stringType = StringType.create(); + t.true(stringType instanceof Type); + t.true(stringType instanceof StringType); + t.same(stringType, StringType.create()); + t.true(stringType === StringType.create()); + t.same(stringType.getTypeName(), TypeName.STRING); + t.same(stringType.getTypeValue(), TypeName.STRING.valueOf()); + t.same(stringType.isScalar(), true); + t.same(stringType.encodesToScalar(), true); + t.same(stringType.getDefault(), null); + t.same(stringType.isBoolean(), false); + t.same(stringType.isBinary(), false); + t.same(stringType.isNumeric(), false); + t.same(stringType.isString(), true); + t.same(stringType.isMessage(), false); + t.same(stringType.allowedInSet(), true); + t.same(stringType.getMaxBytes(), 255); + + try { + stringType.test = 1; + t.fail('StringType instance is mutable'); + } catch (e) { + t.pass('StringType instance is immutable'); + } + + t.end(); +}); + + +test('StringType guard tests', (t) => { + const field = new Field({ name: 'test', type: StringType.create() }); + const largeText = 'a'.repeat(field.getType().getMaxBytes()); + const valid = ['test', largeText, '(╯°□°)╯︵ ┻━┻', ' ice 🍦 poop 💩 doh 😳 ', 'ಠ_ಠ']; + const invalid = [-1, 1, `${largeText}b`, true, false, null, [], {}, NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('StringType guard (min/max length) tests', (t) => { + const field = new Field({ name: 'test', type: StringType.create(), minLength: 5, maxLength: 10 }); + const valid = ['01234', '0123456789', '012345', '012345678']; + const invalid = ['0123', '01234567890']; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('StringType guard (custom pattern) tests', (t) => { + const field = new Field({ name: 'test', type: StringType.create(), pattern: '^\\w+$' }); + const valid = ['AValidValue', 'a_zA_Z0_9', 'all_lower', 'ALL_UPPER']; + const invalid = ['No spaces, commas, etc.', 'nope!', 'http://www.', '--', '', '#test', 'ಠ_ಠ']; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('StringType guard (format=date) tests', (t) => { + const field = new Field({ name: 'test', type: StringType.create(), format: Format.DATE }); + const valid = ['2015-12-25', '1999-12-31']; + const invalid = [ + '01-01-2000', + 'nope!', + '20151225', + '2000-1-1', + '2015/12/25', + '12/25/2015', + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('StringType guard (format=date-time) tests', (t) => { + const field = new Field({ name: 'test', type: StringType.create(), format: Format.DATE_TIME }); + const valid = ['2017-05-25T02:54:18Z', '2017-05-25T02:54:18+00:00']; + const invalid = [ + '01-01-2000', + 'nope!', + '2000-1-1', + '2015/12/25', + '12/25/2015', + '2017-05-25 23:31:53.197954 Z', + '2017-05-25T02:54:18a', + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('StringType guard (format=slug) tests', (t) => { + const field = new Field({ name: 'test', type: StringType.create(), format: Format.SLUG }); + const valid = [ + 'slug-case', + 'gCcx85zbxz4', + 'b8ib4r_UqFM', + '2015/12/25/slug-test', + '2015/12/25/slug_test', + '2015-12-25-Slug_Test', + ]; + const invalid = [ + 'Not A Slug', + 'nope!', + 'http://nope.', + '(╯°□°)╯︵ ┻━┻', + 'ice 🍦 poop 💩 doh 😳', + + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('StringType guard (format=email) tests', (t) => { + const field = new Field({ name: 'test', type: StringType.create(), format: Format.EMAIL }); + const valid = ['homer@simpsons.com', 'TEST@WHAT.co.uk']; + const invalid = ['Not An Email', 'nope!', 'http://www.', 'test@what', '@']; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('StringType guard (format=hashtag) tests', (t) => { + const field = new Field({ name: 'test', type: StringType.create(), format: Format.HASHTAG }); + const valid = ['#Hashtag', 'NotherHashtag']; + const invalid = ['Not A Hashtag', 'nope!', 'http://www.', '111', '_111']; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('StringType guard (format=ipv4) tests', (t) => { + const field = new Field({ name: 'test', type: StringType.create(), format: Format.IPV4 }); + const valid = ['192.168.0.10', '4.2.2.2', '10.0.0.0']; + const invalid = ['Not An IPv4', 'nope!', 'http://www.', '10.1.2.', '10.1.2', '10.1', '.10.1.']; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('StringType guard (format=ipv6) tests', (t) => { + const field = new Field({ name: 'test', type: StringType.create(), format: Format.IPV6 }); + const valid = ['fe80::6ae3:b5ff:fe92:330e', '2001:0db8:0a0b:12f0:0000:0000:0000:0001']; + const invalid = [ + 'Not An IPv6', + 'nope!', + 'http://www.', + '192.168.0.10', + '03:1d:f2:64:6a:01', + 'fe80::35:92ff:fe24:24a3/64', + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('StringType guard (format=hostname) tests', (t) => { + const field = new Field({ name: 'test', type: StringType.create(), format: Format.HOSTNAME }); + const valid = ['test.com', 'localhost', 'local-dev', 'test.whatever.com']; + const invalid = [ + 'Not A Hostname', + 'nope!', + '1234', + '192.168.0.2000', + 'http://www.mydomain.com', + 'www.mydomain.com/page', + 'mydomain.com#page', + '_domain', + '*hi*', + '-hi-', + ':54:sda54', + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('StringType guard (format=uri) tests', (t) => { + const field = new Field({ name: 'test', type: StringType.create(), format: Format.URI }); + const valid = [ + 'tel:+1-816-555-1212', + 'telnet://192.0.2.16:80/', + 'gdbots:iam:command:create-user', + 'urn:isbn:0451450523', + ]; + const invalid = [ + 'Not A Uri', + 'nope!', + '1234', + 'http://➡.ws/䨹', + 'http://⌘.ws', + 'http://foo.com/unicode_(✪)_in_parens', + 'http://☺.damowmow.com/', + 'foo', + 'mailto:user@[255:192:168:1]', + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('StringType guard (format=url) tests', (t) => { + const field = new Field({ name: 'test', type: StringType.create(), format: Format.URL }); + const valid = [ + 'http://www.foo.bar./', + 'http://userid:password@example.com:8080', + 'http://userid:password@example.com:8080/', + 'http://userid@example.com', + 'http://userid@example.com/', + 'http://userid@example.com:8080', + 'http://userid@example.com:80', + 'http://userid:password@example.com', + 'http://userid:password@example.com/', + 'http://142.42.1.1/', + 'http://127.0.0.1:8080/', + 'http://foo.com/blah_(wikipedia)#cite-1', + 'http://foo.com/blah_(wikipedia)_blah#cite-1', + 'http://foo.com/(something)?after=parens', + 'http://code.google.com/events/sub/#&product=browser', + 'http://j.mp', + ]; + const invalid = [ + 'Not A Url', + 'nope!', + '1234', + 'http://⌘.ws', + 'http://foo.com/unicode_(✪)_in_parens', + 'http://☺.damowmow.com/', + 'htt://shouldfailed.com', + 'scheme://shouldfailed.com', + 'emailto:info@example.com', + 'http://##', + 'http://##/', + 'http://foo.bar?q=Spaces should be encoded', + '//', + '//a', + '///a', + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('StringType guard (format=uuid) tests', (t) => { + const field = new Field({ name: 'test', type: StringType.create(), format: Format.UUID }); + const valid = [ + 'e452dd74-41b5-11e7-a919-92ebcb67fe33', + 'd0410f23-75b0-4524-9ce7-2fcc008a7afd', + '093dc7f7-5915-56a5-87de-033e20310b14', + ]; + const invalid = [ + 'Not A UUID', + 'nope!', + '1234', + '11111111-2222-3333-4444-555555556', + '_xxxxxxxx-yyyy-zzzz-0000-11111111', + 'xxxxxxxx-yyyy-zzzz-0000-11111111_', + '1111111122223333444455555555', + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('StringType encode tests', (t) => { + const field = new Field({ name: 'test', type: StringType.create() }); + const largeText = 'a'.repeat(field.getType().getMaxBytes()); + const samples = [ + { input: 'hello', output: 'hello' }, + { input: ' hello', output: 'hello' }, + { input: 'hello ', output: 'hello' }, + { input: ' hello ', output: 'hello' }, + { input: ' ', output: null }, + { input: largeText, output: largeText }, + { input: '(╯°□°)╯︵ ┻━┻', output: '(╯°□°)╯︵ ┻━┻' }, + { input: 'ಠ_ಠ', output: 'ಠ_ಠ' }, + { input: ' ice 🍦 poop 💩 doh 😳 ', output: 'ice 🍦 poop 💩 doh 😳' }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('StringType decode tests', (t) => { + const field = new Field({ name: 'test', type: StringType.create() }); + const largeText = 'a'.repeat(field.getType().getMaxBytes()); + const samples = [ + { input: 'hello', output: 'hello' }, + { input: ' hello', output: 'hello' }, + { input: 'hello ', output: 'hello' }, + { input: ' hello ', output: 'hello' }, + { input: ' ', output: null }, + { input: largeText, output: largeText }, + { input: '(╯°□°)╯︵ ┻━┻', output: '(╯°□°)╯︵ ┻━┻' }, + { input: 'ಠ_ಠ', output: 'ಠ_ಠ' }, + { input: ' ice 🍦 poop 💩 doh 😳 ', output: 'ice 🍦 poop 💩 doh 😳' }, + { input: false, output: 'false' }, + { input: true, output: 'true' }, + { input: '', output: null }, + { input: null, output: null }, + { input: undefined, output: null }, + { input: NaN, output: 'NaN' }, + { input: 3.14, output: '3.14' }, + { input: '3.14', output: '3.14' }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/TextType.test.js b/tests/types/TextType.test.js new file mode 100644 index 0000000..e605bdb --- /dev/null +++ b/tests/types/TextType.test.js @@ -0,0 +1,94 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import TextType from '../../src/types/TextType'; +import helpers from './helpers'; + +test('TextType property tests', (t) => { + const textType = TextType.create(); + t.true(textType instanceof Type); + t.true(textType instanceof TextType); + t.same(textType, TextType.create()); + t.true(textType === TextType.create()); + t.same(textType.getTypeName(), TypeName.TEXT); + t.same(textType.getTypeValue(), TypeName.TEXT.valueOf()); + t.same(textType.isScalar(), true); + t.same(textType.encodesToScalar(), true); + t.same(textType.getDefault(), null); + t.same(textType.isBoolean(), false); + t.same(textType.isBinary(), false); + t.same(textType.isNumeric(), false); + t.same(textType.isString(), true); + t.same(textType.isMessage(), false); + t.same(textType.allowedInSet(), false); + t.same(textType.getMaxBytes(), 65535); + + try { + textType.test = 1; + t.fail('TextType instance is mutable'); + } catch (e) { + t.pass('TextType instance is immutable'); + } + + t.end(); +}); + + +test('TextType guard tests', (t) => { + const field = new Field({ name: 'test', type: TextType.create() }); + const largeText = 'a'.repeat(field.getType().getMaxBytes()); + const valid = ['test', largeText, '(╯°□°)╯︵ ┻━┻', ' ice 🍦 poop 💩 doh 😳 ', 'ಠ_ಠ']; + const invalid = [-1, 1, `${largeText}b`, true, false, null, [], {}, NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('TextType encode tests', (t) => { + const field = new Field({ name: 'test', type: TextType.create() }); + const largeText = 'a'.repeat(field.getType().getMaxBytes()); + const samples = [ + { input: 'hello', output: 'hello' }, + { input: ' hello', output: 'hello' }, + { input: 'hello ', output: 'hello' }, + { input: ' hello ', output: 'hello' }, + { input: ' ', output: null }, + { input: largeText, output: largeText }, + { input: '(╯°□°)╯︵ ┻━┻', output: '(╯°□°)╯︵ ┻━┻' }, + { input: 'ಠ_ಠ', output: 'ಠ_ಠ' }, + { input: ' ice 🍦 poop 💩 doh 😳 ', output: 'ice 🍦 poop 💩 doh 😳' }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('TextType decode tests', (t) => { + const field = new Field({ name: 'test', type: TextType.create() }); + const largeText = 'a'.repeat(field.getType().getMaxBytes()); + const samples = [ + { input: 'hello', output: 'hello' }, + { input: ' hello', output: 'hello' }, + { input: 'hello ', output: 'hello' }, + { input: ' hello ', output: 'hello' }, + { input: ' ', output: null }, + { input: largeText, output: largeText }, + { input: '(╯°□°)╯︵ ┻━┻', output: '(╯°□°)╯︵ ┻━┻' }, + { input: 'ಠ_ಠ', output: 'ಠ_ಠ' }, + { input: ' ice 🍦 poop 💩 doh 😳 ', output: 'ice 🍦 poop 💩 doh 😳' }, + { input: false, output: 'false' }, + { input: true, output: 'true' }, + { input: '', output: null }, + { input: null, output: null }, + { input: undefined, output: null }, + { input: NaN, output: 'NaN' }, + { input: 3.14, output: '3.14' }, + { input: '3.14', output: '3.14' }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/TimeUuidType.test.js b/tests/types/TimeUuidType.test.js new file mode 100644 index 0000000..2f49047 --- /dev/null +++ b/tests/types/TimeUuidType.test.js @@ -0,0 +1,119 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import TimeUuidType from '../../src/types/TimeUuidType'; +import TimeUuidIdentifier from '../../src/well-known/TimeUuidIdentifier'; +import SampleTimeUuidIdentifier from '../fixtures/well-known/SampleTimeUuidIdentifier'; +import helpers from './helpers'; + +test('TimeUuidType property tests', (t) => { + const timeUuidType = TimeUuidType.create(); + t.true(timeUuidType instanceof Type); + t.true(timeUuidType instanceof TimeUuidType); + t.same(timeUuidType, TimeUuidType.create()); + t.true(timeUuidType === TimeUuidType.create()); + t.same(timeUuidType.getTypeName(), TypeName.TIME_UUID); + t.same(timeUuidType.getTypeValue(), TypeName.TIME_UUID.valueOf()); + t.same(timeUuidType.isScalar(), false); + t.same(timeUuidType.encodesToScalar(), true); + t.true(timeUuidType.getDefault() instanceof TimeUuidIdentifier); + t.same(timeUuidType.isBoolean(), false); + t.same(timeUuidType.isBinary(), false); + t.same(timeUuidType.isNumeric(), false); + t.same(timeUuidType.isString(), true); + t.same(timeUuidType.isMessage(), false); + t.same(timeUuidType.allowedInSet(), true); + + try { + timeUuidType.test = 1; + t.fail('TimeUuidType instance is mutable'); + } catch (e) { + t.pass('TimeUuidType instance is immutable'); + } + + t.end(); +}); + + +test('TimeUuidType guard tests', (t) => { + const field = new Field({ name: 'test', type: TimeUuidType.create(), classProto: SampleTimeUuidIdentifier }); + const valid = [ + SampleTimeUuidIdentifier.generate(), + SampleTimeUuidIdentifier.fromString('b385af9a-4413-11e7-a919-92ebcb67fe33'), + new SampleTimeUuidIdentifier('b385af9a-4413-11e7-a919-92ebcb67fe33'), + ]; + const invalid = [ + TimeUuidIdentifier.generate(), + 'b385af9a-4413-11e7-a919-92ebcb67fe33', + null, + [], + {}, + '', + NaN, + undefined, + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('TimeUuidType encode tests', (t) => { + const field = new Field({ name: 'test', type: TimeUuidType.create(), classProto: SampleTimeUuidIdentifier }); + const id = SampleTimeUuidIdentifier.generate(); + const samples = [ + { + input: SampleTimeUuidIdentifier.fromString('b385af9a-4413-11e7-a919-92ebcb67fe33'), + output: 'b385af9a-4413-11e7-a919-92ebcb67fe33', + }, + { input: id, output: id.toString() }, + { input: 0, output: null }, + { input: 1, output: null }, + { input: 2, output: null }, + { input: false, output: null }, + { input: '', output: null }, + { input: null, output: null }, + { input: undefined, output: null }, + { input: NaN, output: null }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('TimeUuidType decode tests', (t) => { + const field = new Field({ name: 'test', type: TimeUuidType.create(), classProto: SampleTimeUuidIdentifier }); + const id = SampleTimeUuidIdentifier.generate(); + const samples = [ + { + input: 'b385af9a-4413-11e7-a919-92ebcb67fe33', + output: SampleTimeUuidIdentifier.fromString('b385af9a-4413-11e7-a919-92ebcb67fe33'), + }, + { input: id.toString(), output: id }, + { input: id, output: id }, + { input: null, output: null }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); + + +test('TimeUuidType decode(invalid) tests', (t) => { + const field = new Field({ name: 'test', type: TimeUuidType.create(), classProto: SampleTimeUuidIdentifier }); + const samples = [ + 'nope', + '4b268351-2445-4d98-a777-b461330d5c7f', + 'b385af9a-4413-11e7-a919-92ebcb67fe33X', + false, + [], + {}, + '', + NaN, + undefined, + ]; + helpers.decodeInvalidSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/TimestampType.test.js b/tests/types/TimestampType.test.js new file mode 100644 index 0000000..ccd651d --- /dev/null +++ b/tests/types/TimestampType.test.js @@ -0,0 +1,84 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import TimestampType from '../../src/types/TimestampType'; +import helpers from './helpers'; + +test('TimestampType property tests', (t) => { + const timestampType = TimestampType.create(); + t.true(timestampType instanceof Type); + t.true(timestampType instanceof TimestampType); + t.same(timestampType, TimestampType.create()); + t.true(timestampType === TimestampType.create()); + t.same(timestampType.getTypeName(), TypeName.TIMESTAMP); + t.same(timestampType.getTypeValue(), TypeName.TIMESTAMP.valueOf()); + t.same(timestampType.isScalar(), true); + t.same(timestampType.encodesToScalar(), true); + t.same(timestampType.getDefault(), Math.floor(Date.now() / 1000)); + t.same(timestampType.isBoolean(), false); + t.same(timestampType.isBinary(), false); + t.same(timestampType.isNumeric(), true); + t.same(timestampType.isString(), false); + t.same(timestampType.isMessage(), false); + t.same(timestampType.allowedInSet(), true); + + try { + timestampType.test = 1; + t.fail('TimestampType instance is mutable'); + } catch (e) { + t.pass('TimestampType instance is immutable'); + } + + t.end(); +}); + + +test('TimestampType guard tests', (t) => { + const field = new Field({ name: 'test', type: TimestampType.create() }); + const valid = [1451001600, 1495053313]; + const invalid = [-1, '1451001600', '1495053313', true, false, null, [], {}, '', NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('TimestampType encode tests', (t) => { + const field = new Field({ name: 'test', type: TimestampType.create() }); + const samples = [ + { input: 1451001600, output: 1451001600 }, + { input: 1495053313, output: 1495053313 }, + { input: '1451001600', output: 1451001600 }, + { input: '1495053313', output: 1495053313 }, + { input: false, output: 0 }, + { input: true, output: 1 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: NaN, output: 0 }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('TimestampType decode tests', (t) => { + const field = new Field({ name: 'test', type: TimestampType.create() }); + const samples = [ + { input: 1451001600, output: 1451001600 }, + { input: 1495053313, output: 1495053313 }, + { input: '1451001600', output: 1451001600 }, + { input: '1495053313', output: 1495053313 }, + { input: false, output: 0 }, + { input: true, output: 1 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: NaN, output: 0 }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/TinyIntType.test.js b/tests/types/TinyIntType.test.js new file mode 100644 index 0000000..fd58ebe --- /dev/null +++ b/tests/types/TinyIntType.test.js @@ -0,0 +1,88 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import TinyIntType from '../../src/types/TinyIntType'; +import helpers from './helpers'; + +test('TinyIntType property tests', (t) => { + const tinyIntType = TinyIntType.create(); + t.true(tinyIntType instanceof Type); + t.true(tinyIntType instanceof TinyIntType); + t.same(tinyIntType, TinyIntType.create()); + t.true(tinyIntType === TinyIntType.create()); + t.same(tinyIntType.getTypeName(), TypeName.TINY_INT); + t.same(tinyIntType.getTypeValue(), TypeName.TINY_INT.valueOf()); + t.same(tinyIntType.isScalar(), true); + t.same(tinyIntType.encodesToScalar(), true); + t.same(tinyIntType.getDefault(), 0); + t.same(tinyIntType.isBoolean(), false); + t.same(tinyIntType.isBinary(), false); + t.same(tinyIntType.isNumeric(), true); + t.same(tinyIntType.isString(), false); + t.same(tinyIntType.isMessage(), false); + t.same(tinyIntType.allowedInSet(), true); + t.same(tinyIntType.getMin(), 0); + t.same(tinyIntType.getMax(), 255); + + try { + tinyIntType.test = 1; + t.fail('TinyIntType instance is mutable'); + } catch (e) { + t.pass('TinyIntType instance is immutable'); + } + + t.end(); +}); + + +test('TinyIntType guard tests', (t) => { + const field = new Field({ name: 'test', type: TinyIntType.create() }); + const valid = [0, 255, 1, 254]; + const invalid = [-1, 256, '0', '255', null, [], {}, '', NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('TinyIntType encode tests', (t) => { + const field = new Field({ name: 'test', type: TinyIntType.create() }); + const samples = [ + { input: 0, output: 0 }, + { input: 255, output: 255 }, + { input: 1, output: 1 }, + { input: 254, output: 254 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: NaN, output: 0 }, + { input: 3.14, output: 3 }, + { input: '3.14', output: 3 }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('TinyIntType decode tests', (t) => { + const field = new Field({ name: 'test', type: TinyIntType.create() }); + const samples = [ + { input: 0, output: 0 }, + { input: 255, output: 255 }, + { input: 1, output: 1 }, + { input: 254, output: 254 }, + { input: false, output: 0 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: NaN, output: 0 }, + { input: 3.14, output: 3 }, + { input: '3.14', output: 3 }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/TrinaryType.test.js b/tests/types/TrinaryType.test.js new file mode 100644 index 0000000..ef25b2d --- /dev/null +++ b/tests/types/TrinaryType.test.js @@ -0,0 +1,90 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import TrinaryType from '../../src/types/TrinaryType'; +import helpers from './helpers'; + +test('TrinaryType property tests', (t) => { + const trinaryType = TrinaryType.create(); + t.true(trinaryType instanceof Type); + t.true(trinaryType instanceof TrinaryType); + t.same(trinaryType, TrinaryType.create()); + t.true(trinaryType === TrinaryType.create()); + t.same(trinaryType.getTypeName(), TypeName.TRINARY); + t.same(trinaryType.getTypeValue(), TypeName.TRINARY.valueOf()); + t.same(trinaryType.isScalar(), true); + t.same(trinaryType.encodesToScalar(), true); + t.same(trinaryType.getDefault(), 0); + t.same(trinaryType.isBoolean(), false); + t.same(trinaryType.isBinary(), false); + t.same(trinaryType.isNumeric(), true); + t.same(trinaryType.isString(), false); + t.same(trinaryType.isMessage(), false); + t.same(trinaryType.allowedInSet(), false); + t.same(trinaryType.getMin(), 0); + t.same(trinaryType.getMax(), 2); + + try { + trinaryType.test = 1; + t.fail('trinaryType instance is mutable'); + } catch (e) { + t.pass('trinaryType instance is immutable'); + } + + t.end(); +}); + + +test('TrinaryType guard tests', (t) => { + const field = new Field({ name: 'test', type: TrinaryType.create() }); + const valid = [0, 1, 2]; + const invalid = [-1, 3, '0', '1', '2', true, false, null, [], {}, '', NaN, undefined]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('TrinaryType encode tests', (t) => { + const field = new Field({ name: 'test', type: TrinaryType.create() }); + const samples = [ + { input: 0, output: 0 }, + { input: 1, output: 1 }, + { input: 2, output: 2 }, + { input: '0', output: 0 }, + { input: '1', output: 1 }, + { input: '2', output: 2 }, + { input: false, output: 0 }, + { input: true, output: 1 }, // this is weird, true becomes 1 in _.toSafeInteger + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: NaN, output: 0 }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('TrinaryType decode tests', (t) => { + const field = new Field({ name: 'test', type: TrinaryType.create() }); + const samples = [ + { input: 0, output: 0 }, + { input: 1, output: 1 }, + { input: 2, output: 2 }, + { input: '0', output: 0 }, + { input: '1', output: 1 }, + { input: '2', output: 2 }, + { input: false, output: 0 }, + { input: true, output: 1 }, + { input: '', output: 0 }, + { input: null, output: 0 }, + { input: undefined, output: 0 }, + { input: NaN, output: 0 }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/UuidType.test.js b/tests/types/UuidType.test.js new file mode 100644 index 0000000..4ce8e86 --- /dev/null +++ b/tests/types/UuidType.test.js @@ -0,0 +1,109 @@ +import test from 'tape'; +import TypeName from '../../src/enums/TypeName'; +import Type from '../../src/types/Type'; +import Field from '../../src/Field'; +import UuidType from '../../src/types/UuidType'; +import UuidIdentifier from '../../src/well-known/UuidIdentifier'; +import SampleUuidIdentifier from '../fixtures/well-known/SampleUuidIdentifier'; +import helpers from './helpers'; + +test('UuidType property tests', (t) => { + const uuidType = UuidType.create(); + t.true(uuidType instanceof Type); + t.true(uuidType instanceof UuidType); + t.same(uuidType, UuidType.create()); + t.true(uuidType === UuidType.create()); + t.same(uuidType.getTypeName(), TypeName.UUID); + t.same(uuidType.getTypeValue(), TypeName.UUID.valueOf()); + t.same(uuidType.isScalar(), false); + t.same(uuidType.encodesToScalar(), true); + t.true(uuidType.getDefault() instanceof UuidIdentifier); + t.same(uuidType.isBoolean(), false); + t.same(uuidType.isBinary(), false); + t.same(uuidType.isNumeric(), false); + t.same(uuidType.isString(), true); + t.same(uuidType.isMessage(), false); + t.same(uuidType.allowedInSet(), true); + + try { + uuidType.test = 1; + t.fail('UuidType instance is mutable'); + } catch (e) { + t.pass('UuidType instance is immutable'); + } + + t.end(); +}); + + +test('UuidType guard tests', (t) => { + const field = new Field({ name: 'test', type: UuidType.create(), classProto: SampleUuidIdentifier }); + const valid = [ + SampleUuidIdentifier.generate(), + SampleUuidIdentifier.fromString('4b268351-2445-4d98-a777-b461330d5c7f'), + new SampleUuidIdentifier('4b268351-2445-4d98-a777-b461330d5c7a'), + ]; + const invalid = [ + UuidIdentifier.generate(), + '4b268351-2445-4d98-a777-b461330d5c7f', + null, + [], + {}, + '', + NaN, + undefined, + ]; + helpers.guardValidSamples(field, valid, t); + helpers.guardInvalidSamples(field, invalid, t); + t.end(); +}); + + +test('UuidType encode tests', (t) => { + const field = new Field({ name: 'test', type: UuidType.create(), classProto: SampleUuidIdentifier }); + const id = SampleUuidIdentifier.generate(); + const samples = [ + { + input: SampleUuidIdentifier.fromString('4b268351-2445-4d98-a777-b461330d5c7f'), + output: '4b268351-2445-4d98-a777-b461330d5c7f', + }, + { input: id, output: id.toString() }, + { input: 0, output: null }, + { input: 1, output: null }, + { input: 2, output: null }, + { input: false, output: null }, + { input: '', output: null }, + { input: null, output: null }, + { input: undefined, output: null }, + { input: NaN, output: null }, + ]; + + helpers.encodeSamples(field, samples, t); + t.end(); +}); + + +test('UuidType decode tests', (t) => { + const field = new Field({ name: 'test', type: UuidType.create(), classProto: SampleUuidIdentifier }); + const id = SampleUuidIdentifier.generate(); + const samples = [ + { + input: '4b268351-2445-4d98-a777-b461330d5c7f', + output: SampleUuidIdentifier.fromString('4b268351-2445-4d98-a777-b461330d5c7f'), + }, + { input: id.toString(), output: id }, + { input: id, output: id }, + { input: null, output: null }, + ]; + + helpers.decodeSamples(field, samples, t); + t.end(); +}); + + +test('UuidType decode(invalid) tests', (t) => { + const field = new Field({ name: 'test', type: UuidType.create(), classProto: SampleUuidIdentifier }); + const samples = ['nope', '4b268351-2445-4d98-a777-b461330d5c7fX', false, [], {}, '', NaN, undefined]; + helpers.decodeInvalidSamples(field, samples, t); + t.end(); +}); diff --git a/tests/types/helpers.js b/tests/types/helpers.js new file mode 100644 index 0000000..ea59531 --- /dev/null +++ b/tests/types/helpers.js @@ -0,0 +1,114 @@ +import toString from 'lodash/toString'; +import truncate from 'lodash/truncate'; + +/** + * Runs guard against an array of valid samples for the provided + * type and asserts that it *MUST* pass. + * + * @param {Field} field - The field object. + * @param {Array} samples - An array of valid samples to test. + * @param {Object} test - The test provider (with pass/fail methods). + */ +function guardValidSamples(field, samples, test) { + const type = field.getType(); + samples.forEach((value) => { + try { + type.guard(value, field); + const truncated = truncate(JSON.stringify(value)); + test.pass(`${type.getTypeName().getName()}.guard accepted valid value [${truncated}].`); + } catch (e) { + test.fail(e.message); + } + }); +} + +/** + * Runs guard against an array of invalid samples for the provided + * type and asserts that it *MUST* fail. + * + * @param {Field} field - The field object. + * @param {Array} samples - An array of invalid samples to test. + * @param {Object} test - The test provider (with pass/fail methods). + */ +function guardInvalidSamples(field, samples, test) { + const type = field.getType(); + samples.forEach((value) => { + try { + type.guard(value, field); + test.fail(`${type.getTypeName().getName()}.guard accepted invalid value [${JSON.stringify(value)}].`); + } catch (e) { + test.pass(e.message); + } + }); +} + +/** + * Runs encode against the field's type with an array of samples containing + * the input to run and the expected output. + * + * @param {Field} field - The field object. + * @param {Array} samples - An array of objects with input/output properties. + * @param {Object} test - The test provider (with pass/fail methods). + * @param {Object} codec - Codec to use when type requires it. + */ +function encodeSamples(field, samples, test, codec = null) { + samples.forEach((obj) => { + try { + const actual = field.getType().encode(obj.input, field, codec); + test.same(actual, obj.output); + test.same(toString(actual), toString(obj.output)); + } catch (e) { + test.fail(e.message); + } + }); +} + +/** + * Runs decode against the field's type with an array of samples containing + * the input to run and the expected output. + * + * @param {Field} field - The field object. + * @param {Array} samples - An array of objects with input/output properties. + * @param {Object} test - The test provider (with pass/fail methods). + * @param {Object} codec - Codec to use when type requires it. + */ +function decodeSamples(field, samples, test, codec = null) { + samples.forEach((obj) => { + try { + const actual = field.getType().decode(obj.input, field, codec); + test.same(actual, obj.output); + test.same(toString(actual), toString(obj.output)); + } catch (e) { + test.fail(e.message); + } + }); +} + +/** + * Runs decode against an array of invalid samples for the provided + * type and asserts that it *MUST* pass. + * + * @param {Field} field - The field object. + * @param {Array} samples - An array of invalid samples to test. + * @param {Object} test - The test provider (with pass/fail methods). + * @param {Object} codec - Codec to use when type requires it. + */ +function decodeInvalidSamples(field, samples, test, codec = null) { + const type = field.getType(); + samples.forEach((value) => { + try { + const decoded = type.decode(value, field, codec); + test.fail(`${type.getTypeName().getName()}.decode accepted invalid value [${JSON.stringify(value)}] and returned [${JSON.stringify(decoded)}].`); + } catch (e) { + test.pass(e.message); + } + }); +} + +export default { + guardValidSamples, + guardInvalidSamples, + encodeSamples, + decodeSamples, + decodeInvalidSamples, +}; diff --git a/tests/well-known/DatedSlugIdentifier.test.js b/tests/well-known/DatedSlugIdentifier.test.js new file mode 100644 index 0000000..a7fdeb3 --- /dev/null +++ b/tests/well-known/DatedSlugIdentifier.test.js @@ -0,0 +1,83 @@ +import test from 'tape'; +import Identifier from '../../src/well-known/Identifier'; +import DatedSlugIdentifier from '../../src/well-known/DatedSlugIdentifier'; +import SampleDatedSlugIdentifier from '../fixtures/well-known/SampleDatedSlugIdentifier'; + +test('DatedSlugIdentifier tests', (t) => { + const id = new SampleDatedSlugIdentifier('2015/12/25/homer-simpson'); + t.true(id instanceof Identifier); + t.true(id instanceof DatedSlugIdentifier); + t.true(id instanceof SampleDatedSlugIdentifier); + t.true(id.equals(SampleDatedSlugIdentifier.fromString(`${id}`))); + + try { + id.test = 1; + t.fail('id instance is mutable'); + } catch (e) { + t.pass('id instance is immutable'); + } + + t.end(); +}); + + +test('DatedSlugIdentifier fromString tests', (t) => { + const slug1 = '2015/12/25/homer-simpson'; + const slug2 = '2016/12/25/bart-simpson'; + const id = SampleDatedSlugIdentifier.fromString(slug1); + t.true(id instanceof Identifier); + t.true(id instanceof DatedSlugIdentifier); + t.true(id instanceof SampleDatedSlugIdentifier); + + t.same(slug1, id.toString()); + t.same(slug1, id.valueOf()); + t.same(slug1, `${id}`); + t.same(JSON.stringify(slug1), JSON.stringify(id)); + t.true(id.equals(SampleDatedSlugIdentifier.fromString(slug1))); + t.false(id.equals(SampleDatedSlugIdentifier.fromString(slug2))); + + t.end(); +}); + + +test('DatedSlugIdentifier create tests', (t) => { + const slug = '2015/12/25/homer-simpson'; + const date = new Date(2015, 11, 25); + const id = SampleDatedSlugIdentifier.create('Homer Simpson', date); + t.true(id instanceof Identifier); + t.true(id instanceof DatedSlugIdentifier); + t.true(id instanceof SampleDatedSlugIdentifier); + + t.same(slug, id.toString()); + t.same(slug, id.valueOf()); + t.same(slug, `${id}`); + t.same(JSON.stringify(slug), JSON.stringify(id)); + t.true(id.equals(SampleDatedSlugIdentifier.fromString(slug))); + + t.end(); +}); + + +test('DatedSlugIdentifier (invalid) tests', (t) => { + const invalid = [ + 'Not a dated Slug', + 'not-a-dated-slug', + false, + [], + {}, + '', + NaN, + undefined, + ]; + + invalid.forEach((value) => { + try { + SampleDatedSlugIdentifier.fromString(value); + t.fail(`SampleDatedSlugIdentifier.fromString accepted invalid value [${JSON.stringify(value)}].`); + } catch (e) { + t.pass(e.message); + } + }); + + t.end(); +}); diff --git a/tests/well-known/DynamicField.test.js b/tests/well-known/DynamicField.test.js new file mode 100644 index 0000000..9520e2e --- /dev/null +++ b/tests/well-known/DynamicField.test.js @@ -0,0 +1,118 @@ +import test from 'tape'; +import DynamicField from '../../src/well-known/DynamicField'; +import Field from '../../src/Field'; + +test('DynamicField property tests', (t) => { + const df1 = DynamicField.createStringVal('test1', 'taco'); + const df2 = DynamicField.createStringVal('test2', 'pizza'); + t.true(df1 instanceof DynamicField); + t.true(df1.getField() instanceof Field); + t.true(df2.getField() instanceof Field); + t.true(df1.getField() === df2.getField()); // ensures flyweight "field" instances + t.same(df1.getName(), 'test1'); + t.same(df1.getKind(), 'string_val'); + t.same(df1.getValue(), 'taco'); + t.same(df1.toJSON(), { name: 'test1', string_val: 'taco' }); + t.same(df1.toString(), '{"name":"test1","string_val":"taco"}'); + + t.same(df2.getName(), 'test2'); + t.same(df2.getKind(), 'string_val'); + t.same(df2.getValue(), 'pizza'); + t.same(df2.toJSON(), { name: 'test2', string_val: 'pizza' }); + t.same(df2.toString(), '{"name":"test2","string_val":"pizza"}'); + + t.false(df1.equals(df2)); + t.false(df2.equals(df1)); + + try { + df1.test = 1; + t.fail('df1 instance is mutable'); + } catch (e) { + t.pass('df1 instance is immutable'); + } + + try { + df2.test = 1; + t.fail('df2 instance is mutable'); + } catch (e) { + t.pass('df2 instance is immutable'); + } + + t.end(); +}); + + +test('DynamicField createBoolVal tests', (t) => { + const df = DynamicField.createBoolVal('test', true); + t.true(df instanceof DynamicField); + t.true(df.getField() instanceof Field); + t.same(df.getName(), 'test'); + t.same(df.getKind(), 'bool_val'); + t.same(df.getValue(), true); + t.same(df.toJSON(), { name: 'test', bool_val: true }); + t.same(df.toString(), '{"name":"test","bool_val":true}'); + t.same(df, DynamicField.fromJSON(df.toString())); + + t.end(); +}); + + +test('DynamicField createDateVal tests', (t) => { + const date = new Date(Date.UTC(2015, 11, 25)); + const df = DynamicField.createDateVal('test', date); + t.true(df instanceof DynamicField); + t.true(df.getField() instanceof Field); + t.same(df.getName(), 'test'); + t.same(df.getKind(), 'date_val'); + t.same(df.getValue(), date); + t.same(df.toJSON(), { name: 'test', date_val: '2015-12-25' }); + t.same(df.toString(), '{"name":"test","date_val":"2015-12-25"}'); + t.same(df, DynamicField.fromJSON(df.toString())); + + t.end(); +}); + + +test('DynamicField createFloatVal tests', (t) => { + const df = DynamicField.createFloatVal('test', 3.14); + t.true(df instanceof DynamicField); + t.true(df.getField() instanceof Field); + t.same(df.getName(), 'test'); + t.same(df.getKind(), 'float_val'); + t.same(df.getValue(), 3.14); + t.same(df.toJSON(), { name: 'test', float_val: 3.14 }); + t.same(df.toString(), '{"name":"test","float_val":3.14}'); + t.same(df, DynamicField.fromJSON(df.toString())); + + t.end(); +}); + + +test('DynamicField createIntVal tests', (t) => { + const df = DynamicField.createIntVal('test', 9000); + t.true(df instanceof DynamicField); + t.true(df.getField() instanceof Field); + t.same(df.getName(), 'test'); + t.same(df.getKind(), 'int_val'); + t.same(df.getValue(), 9000); + t.same(df.toJSON(), { name: 'test', int_val: 9000 }); + t.same(df.toString(), '{"name":"test","int_val":9000}'); + t.same(df, DynamicField.fromJSON(df.toString())); + + t.end(); +}); + + +test('DynamicField createTextVal tests', (t) => { + const df = DynamicField.createTextVal('test', 'ice 🍦 poop 💩 doh 😳'); + t.true(df instanceof DynamicField); + t.true(df.getField() instanceof Field); + t.same(df.getName(), 'test'); + t.same(df.getKind(), 'text_val'); + t.same(df.getValue(), 'ice 🍦 poop 💩 doh 😳'); + t.same(df.toJSON(), { name: 'test', text_val: 'ice 🍦 poop 💩 doh 😳' }); + t.same(df.toString(), '{"name":"test","text_val":"ice 🍦 poop 💩 doh 😳"}'); + t.same(df, DynamicField.fromJSON(df.toString())); + + t.end(); +}); diff --git a/tests/well-known/GeoPoint.test.js b/tests/well-known/GeoPoint.test.js new file mode 100644 index 0000000..4a83732 --- /dev/null +++ b/tests/well-known/GeoPoint.test.js @@ -0,0 +1,91 @@ +import test from 'tape'; +import GeoPoint from '../../src/well-known/GeoPoint'; + +test('GeoPoint new tests', (t) => { + const gp = new GeoPoint(0.5, 102.0); + t.true(gp instanceof GeoPoint); + t.true(gp.equals(new GeoPoint(gp.getLatitude(), gp.getLongitude()))); + t.true(gp.equals(GeoPoint.fromString(`${gp}`))); + t.true(gp.equals(GeoPoint.fromJSON(JSON.stringify(gp)))); + + t.same(gp.getLatitude(), 0.5); + t.same(gp.getLongitude(), 102); + t.same(`${gp.getLatitude()}`, '0.5'); + t.same(`${gp.getLongitude()}`, '102'); + t.same(gp.toString(), '0.5,102'); + t.same(gp.toJSON(), { type: 'Point', coordinates: [102, 0.5] }); + t.same(JSON.stringify(gp), '{"type":"Point","coordinates":[102,0.5]}'); + + try { + gp.test = 1; + t.fail('gp instance is mutable'); + } catch (e) { + t.pass('gp instance is immutable'); + } + + t.end(); +}); + + +test('GeoPoint fromString tests', (t) => { + const gp = GeoPoint.fromString('34.1789335,-118.347594'); + t.true(gp instanceof GeoPoint); + t.true(gp.equals(new GeoPoint(34.1789335, -118.347594))); + t.true(gp.equals(GeoPoint.fromString(`${gp}`))); + t.true(gp.equals(GeoPoint.fromJSON(JSON.stringify(gp)))); + + t.same(gp.getLatitude(), 34.1789335); + t.same(gp.getLongitude(), -118.347594); + t.same(`${gp.getLatitude()}`, '34.1789335'); + t.same(`${gp.getLongitude()}`, '-118.347594'); + t.same(gp.toString(), '34.1789335,-118.347594'); + t.same(gp.toJSON(), { type: 'Point', coordinates: [-118.347594, 34.1789335] }); + t.same(JSON.stringify(gp), '{"type":"Point","coordinates":[-118.347594,34.1789335]}'); + t.end(); +}); + + +test('GeoPoint fromJSON tests', (t) => { + const gp = GeoPoint.fromJSON('{"type":"Point","coordinates":[-118.347594,34.1789335]}'); + t.true(gp instanceof GeoPoint); + t.true(gp.equals(new GeoPoint(34.1789335, -118.347594))); + t.true(gp.equals(GeoPoint.fromString(`${gp}`))); + t.true(gp.equals(GeoPoint.fromJSON(JSON.stringify(gp)))); + + t.same(gp.getLatitude(), 34.1789335); + t.same(gp.getLongitude(), -118.347594); + t.same(`${gp.getLatitude()}`, '34.1789335'); + t.same(`${gp.getLongitude()}`, '-118.347594'); + t.same(gp.toString(), '34.1789335,-118.347594'); + t.same(gp.toJSON(), { type: 'Point', coordinates: [-118.347594, 34.1789335] }); + t.same(JSON.stringify(gp), '{"type":"Point","coordinates":[-118.347594,34.1789335]}'); + t.end(); +}); + + +test('GeoPoint fromJSON(invalid) tests', (t) => { + const invalid = [ + '[-118.347594,34.1789335]', // not even GeoJson + '{"type":"Point","coordinates":[34.1789335,-118.347594]}', // wrong order lat/long + '{"type":"Point","coordinates":[190,-100]}', + 'a,b', + 'nope', + false, + [], + {}, + '', + NaN, + undefined, + ]; + + invalid.forEach((value) => { + try { + GeoPoint.fromJSON(value); + t.fail(`GeoPoint.fromJSON accepted invalid value [${JSON.stringify(value)}].`); + } catch (e) { + t.pass(e.message); + } + }); + + t.end(); +}); diff --git a/tests/well-known/Microtime.test.js b/tests/well-known/Microtime.test.js new file mode 100644 index 0000000..9b55aaf --- /dev/null +++ b/tests/well-known/Microtime.test.js @@ -0,0 +1,64 @@ +import test from 'tape'; +import moment from 'moment'; +import Microtime from '../../src/well-known/Microtime'; + +test('Microtime create tests', (t) => { + const m = Microtime.create(); + t.true(m instanceof Microtime); + t.true(m.toDate() instanceof Date); + t.true(m.toMoment() instanceof moment); + t.true(moment.isMoment(m.toMoment())); + t.true(/^[0-9]{16}$/.test(m.toString())); + t.same(m.toString().length, 16); + t.same(`${m}`.length, 16); + + try { + m.test = 1; + t.fail('m instance is mutable'); + } catch (e) { + t.pass('m instance is immutable'); + } + + t.end(); +}); + + +test('Microtime fromString tests', (t) => { + const mString = '1495766080123456'; + const mMoment = moment.unix(1495766080.123456); + const mDate = mMoment.toDate(); + const m = Microtime.fromString(mString); + + t.true(m instanceof Microtime); + t.true(m.equals(Microtime.fromString(mString))); + t.same(m.toString(), mString); + t.same(m.valueOf(), mString); + t.same(m.toJSON(), mString); + t.same(JSON.stringify(m), JSON.stringify(mString)); + t.same(`${m.toNumber()}`, '1495766080.123456'); + t.same(m.toMoment(), mMoment); + t.same(m.toDate(), mDate); + + t.end(); +}); + + +test('Microtime fromDate tests', (t) => { + const mString = '1495766080123000'; + const mMoment = moment.unix(1495766080.123456); + const mDate = mMoment.toDate(); + const m = Microtime.fromDate(mDate); + + t.true(m instanceof Microtime); + t.true(m.equals(Microtime.fromString(mString))); + t.same(m.toString(), mString); + t.same(m.valueOf(), mString); + t.same(m.toJSON(), mString); + t.same(JSON.stringify(m), JSON.stringify(mString)); + t.same(`${m.toNumber()}`, '1495766080.123'); + t.true(m.toMoment().isSame(mMoment)); + t.same(m.toDate().toISOString(), mDate.toISOString()); + t.same(m.toDate().toISOString(), '2017-05-26T02:34:40.123Z'); + + t.end(); +}); diff --git a/tests/well-known/SlugIdentifier.test.js b/tests/well-known/SlugIdentifier.test.js new file mode 100644 index 0000000..7251f8e --- /dev/null +++ b/tests/well-known/SlugIdentifier.test.js @@ -0,0 +1,82 @@ +import test from 'tape'; +import Identifier from '../../src/well-known/Identifier'; +import SlugIdentifier from '../../src/well-known/SlugIdentifier'; +import SampleSlugIdentifier from '../fixtures/well-known/SampleSlugIdentifier'; + +test('SlugIdentifier tests', (t) => { + const id = new SampleSlugIdentifier('homer-simpson'); + t.true(id instanceof Identifier); + t.true(id instanceof SlugIdentifier); + t.true(id instanceof SampleSlugIdentifier); + t.true(id.equals(SampleSlugIdentifier.fromString(`${id}`))); + + try { + id.test = 1; + t.fail('id instance is mutable'); + } catch (e) { + t.pass('id instance is immutable'); + } + + t.end(); +}); + + +test('SlugIdentifier fromString tests', (t) => { + const slug1 = 'homer-simpson'; + const slug2 = 'bart-simpson'; + const id = SampleSlugIdentifier.fromString(slug1); + t.true(id instanceof Identifier); + t.true(id instanceof SlugIdentifier); + t.true(id instanceof SampleSlugIdentifier); + + t.same(slug1, id.toString()); + t.same(slug1, id.valueOf()); + t.same(slug1, `${id}`); + t.same(JSON.stringify(slug1), JSON.stringify(id)); + t.true(id.equals(SampleSlugIdentifier.fromString(slug1))); + t.false(id.equals(SampleSlugIdentifier.fromString(slug2))); + + t.end(); +}); + + +test('SlugIdentifier create tests', (t) => { + const slug = 'homer-simpson'; + const id = SampleSlugIdentifier.create('Homer Simpson'); + t.true(id instanceof Identifier); + t.true(id instanceof SlugIdentifier); + t.true(id instanceof SampleSlugIdentifier); + + t.same(slug, id.toString()); + t.same(slug, id.valueOf()); + t.same(slug, `${id}`); + t.same(JSON.stringify(slug), JSON.stringify(id)); + t.true(id.equals(SampleSlugIdentifier.fromString(slug))); + + t.end(); +}); + + +test('SlugIdentifier (invalid) tests', (t) => { + const invalid = [ + 'Not a Slug', + '2015/12/25/not-a-simple-slug', + false, + [], + {}, + '', + NaN, + undefined, + ]; + + invalid.forEach((value) => { + try { + SampleSlugIdentifier.fromString(value); + t.fail(`SampleSlugIdentifier.fromString accepted invalid value [${JSON.stringify(value)}].`); + } catch (e) { + t.pass(e.message); + } + }); + + t.end(); +}); diff --git a/tests/well-known/TimeUuidIdentifier.test.js b/tests/well-known/TimeUuidIdentifier.test.js new file mode 100644 index 0000000..e5fe326 --- /dev/null +++ b/tests/well-known/TimeUuidIdentifier.test.js @@ -0,0 +1,71 @@ +import test from 'tape'; +import Identifier from '../../src/well-known/Identifier'; +import UuidIdentifier from '../../src/well-known/UuidIdentifier'; +import TimeUuidIdentifier from '../../src/well-known/TimeUuidIdentifier'; +import SampleTimeUuidIdentifier from '../fixtures/well-known/SampleTimeUuidIdentifier'; + +test('TimeUuidIdentifier generate tests', (t) => { + const id = SampleTimeUuidIdentifier.generate(); + t.true(id instanceof Identifier); + t.true(id instanceof UuidIdentifier); + t.true(id instanceof TimeUuidIdentifier); + t.true(id instanceof SampleTimeUuidIdentifier); + t.true(/^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-1[0-9A-Fa-f]{3}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/.test(id)); + t.true(id.equals(SampleTimeUuidIdentifier.fromString(`${id}`))); + + try { + id.test = 1; + t.fail('id instance is mutable'); + } catch (e) { + t.pass('id instance is immutable'); + } + + t.end(); +}); + + +test('TimeUuidIdentifier fromString tests', (t) => { + const idString = 'b385af9a-4413-11e7-a919-92ebcb67fe33'; + const idString2 = 'b385af9a-4413-11e7-a919-92ebcb67fe34'; + const id = SampleTimeUuidIdentifier.fromString(idString); + t.true(id instanceof Identifier); + t.true(id instanceof UuidIdentifier); + t.true(id instanceof TimeUuidIdentifier); + t.true(id instanceof SampleTimeUuidIdentifier); + + t.same(idString, id.toString()); + t.same(idString, id.valueOf()); + t.same(idString, `${id}`); + t.same(JSON.stringify(idString), JSON.stringify(id)); + t.true(id.equals(SampleTimeUuidIdentifier.fromString(idString))); + t.false(id.equals(SampleTimeUuidIdentifier.fromString(idString2))); + + t.end(); +}); + + +test('TimeUuidIdentifier (invalid) tests', (t) => { + const invalid = [ + 'b385af9a-4413-11e7-a919-92ebcb67fe33X', + 'b385af9a-4413-11e7-a919-92ebcb67fe3', + 'b385af9a441311e7a91992ebcb67fe33', + 'nope', + false, + [], + {}, + '', + NaN, + undefined, + ]; + + invalid.forEach((value) => { + try { + SampleTimeUuidIdentifier.fromString(value); + t.fail(`SampleTimeUuidIdentifier.fromString accepted invalid value [${JSON.stringify(value)}].`); + } catch (e) { + t.pass(e.message); + } + }); + + t.end(); +}); diff --git a/tests/well-known/UuidIdentifier.test.js b/tests/well-known/UuidIdentifier.test.js new file mode 100644 index 0000000..258fbc7 --- /dev/null +++ b/tests/well-known/UuidIdentifier.test.js @@ -0,0 +1,68 @@ +import test from 'tape'; +import Identifier from '../../src/well-known/Identifier'; +import UuidIdentifier from '../../src/well-known/UuidIdentifier'; +import SampleUuidIdentifier from '../fixtures/well-known/SampleUuidIdentifier'; + +test('UuidIdentifier generate tests', (t) => { + const id = SampleUuidIdentifier.generate(); + t.true(id instanceof Identifier); + t.true(id instanceof UuidIdentifier); + t.true(id instanceof SampleUuidIdentifier); + t.true(/^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/.test(id)); + t.true(id.equals(SampleUuidIdentifier.fromString(`${id}`))); + + try { + id.test = 1; + t.fail('id instance is mutable'); + } catch (e) { + t.pass('id instance is immutable'); + } + + t.end(); +}); + + +test('UuidIdentifier fromString tests', (t) => { + const idString = '4b268351-2445-4d98-a777-b461330d5c7f'; + const idString2 = '4b268351-2445-4d98-a777-b461330d5c7a'; + const id = SampleUuidIdentifier.fromString(idString); + t.true(id instanceof Identifier); + t.true(id instanceof UuidIdentifier); + t.true(id instanceof SampleUuidIdentifier); + + t.same(idString, id.toString()); + t.same(idString, id.valueOf()); + t.same(idString, `${id}`); + t.same(JSON.stringify(idString), JSON.stringify(id)); + t.true(id.equals(SampleUuidIdentifier.fromString(idString))); + t.false(id.equals(SampleUuidIdentifier.fromString(idString2))); + + t.end(); +}); + + +test('UuidIdentifier (invalid) tests', (t) => { + const invalid = [ + '4b268351-2445-4d98-a777-b461330d5c7fX', + '4b268351-2445-4d98-a777-b461330d5c7', + '4b26835124454d98a777b461330d5c7f', + 'nope', + false, + [], + {}, + '', + NaN, + undefined, + ]; + + invalid.forEach((value) => { + try { + SampleUuidIdentifier.fromString(value); + t.fail(`SampleUuidIdentifier.fromString accepted invalid value [${JSON.stringify(value)}].`); + } catch (e) { + t.pass(e.message); + } + }); + + t.end(); +}); diff --git a/tests/well-known/dynamic-field-test.js b/tests/well-known/dynamic-field-test.js deleted file mode 100644 index b061381..0000000 --- a/tests/well-known/dynamic-field-test.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; - -import {expect} from 'chai' -import EmailMessage from '../fixtures/email-message'; -import DynamicField from 'gdbots/pbj/well-known/dynamic-field.js'; - -const TEST_COUNT = 2500; - -describe('dynamic-field-test', function() { - it('add to message', function(done) { - let message = EmailMessage.create(); - let field = DynamicField.createFloatVal('float_val', 3.14); - - message.addToList('dynamic_fields', [field]); - - message.getFromListAt('dynamic_fields', 0).should.eql(field); - - done(); - }); -}); diff --git a/tests/well-known/microtime-test.js b/tests/well-known/microtime-test.js deleted file mode 100644 index 0b53231..0000000 --- a/tests/well-known/microtime-test.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; - -import {expect} from 'chai' -import DateUtils from 'gdbots/common/util/date-utils.js'; -import StringUtils from 'gdbots/common/util/string-utils.js'; -import Microtime from 'gdbots/pbj/well-known/microtime.js'; - -const TEST_COUNT = 2500; - -describe('microtime-test', function() { - it('check from time-of-day', function(done) { - let i = TEST_COUNT; - do { - let tod = DateUtils.gettimeofday(); - let sec = tod.sec; - let usec = tod.usec; - let str = sec + StringUtils.strPad(tod.usec, 6, '0', 'STR_PAD_LEFT'); - let m = Microtime.fromTimeOfDay(tod); - - expect(sec).to.be.eq(m.getSeconds()); - expect(sec).to.be.eq(Math.floor(m.toDateTime().getTime() / 1000)); - expect(usec).to.be.eq(m.getMicroSeconds()); - expect(str).to.be.eq(m.toString()); - - --i; - } while (i > 0); - - done(); - }); -}); diff --git a/tests/well-known/uuid-identifier-test.js b/tests/well-known/uuid-identifier-test.js deleted file mode 100644 index 6964299..0000000 --- a/tests/well-known/uuid-identifier-test.js +++ /dev/null @@ -1,39 +0,0 @@ -'use strict'; - -import {expect} from 'chai' -import UuidIdentifier from 'gdbots/pbj/well-known/uuid-identifier'; - -describe('uuid-identifier-test', function() { - it('using generate()', function(done) { - let id = UuidIdentifier.generate(); - - var v4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-[4][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; - v4Regex.test(id.toString()).should.be.true; - - done(); - }); - - it('generate from string', function(done) { - const NAMESPACE_DNS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; - - let id = UuidIdentifier.fromString(NAMESPACE_DNS); - - (id.toString() == NAMESPACE_DNS).should.be.true; - - done(); - }); - - it('check equal uuids', function(done) { - const NAMESPACE_DNS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; - const NAMESPACE_OID = '6ba7b812-9dad-11d1-80b4-00c04fd430c8'; - - let id = UuidIdentifier.fromString(NAMESPACE_DNS); - let id2 = UuidIdentifier.fromString(NAMESPACE_DNS); - let id3 = UuidIdentifier.fromString(NAMESPACE_OID); - - (id.equals(id2)).should.be.true; - (id.equals(id3)).should.be.false; - - done(); - }); -});