diff --git a/CHANGELOG.md b/CHANGELOG.md index 69c048bcf7..728f2a313c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,7 +33,9 @@ Items that are related, such as breaking changes, new features, or changes to ex - [@siteimprove/alfa-json](packages/alfa-json): `JSON.parse()` and `JSON.stringify()` are now available. -- [@siteimprove/alfa-trampoline](packages-alfa-trampoline): `Trampoline#tee()` and `Trampoline.empty()` are now available. +- [@siteimprove/alfa-trampoline](packages/alfa-trampoline): `Trampoline#tee()` and `Trampoline.empty()` are now available. + +- [@siteimprove/alfa-either](packages/alfa-either): `Either` now provides an implementation of `Hashable`. ### Fixed diff --git a/packages/alfa-either/package.json b/packages/alfa-either/package.json index f31eeb69e7..e3f6d9f12a 100644 --- a/packages/alfa-either/package.json +++ b/packages/alfa-either/package.json @@ -21,6 +21,7 @@ "@siteimprove/alfa-equatable": "^0.10.0", "@siteimprove/alfa-foldable": "^0.10.0", "@siteimprove/alfa-functor": "^0.10.0", + "@siteimprove/alfa-hash": "^0.10.0", "@siteimprove/alfa-json": "^0.10.0", "@siteimprove/alfa-mapper": "^0.10.0", "@siteimprove/alfa-monad": "^0.10.0", diff --git a/packages/alfa-either/src/either.ts b/packages/alfa-either/src/either.ts index 3629890336..0f2b291084 100644 --- a/packages/alfa-either/src/either.ts +++ b/packages/alfa-either/src/either.ts @@ -1,6 +1,7 @@ import { Equatable } from "@siteimprove/alfa-equatable"; import { Foldable } from "@siteimprove/alfa-foldable"; import { Functor } from "@siteimprove/alfa-functor"; +import { Hashable } from "@siteimprove/alfa-hash"; import { Serializable } from "@siteimprove/alfa-json"; import { Mapper } from "@siteimprove/alfa-mapper"; import { Monad } from "@siteimprove/alfa-monad"; @@ -16,6 +17,7 @@ export interface Either Foldable, Iterable, Equatable, + Hashable, Serializable> { isLeft(): this is Left; isRight(): this is Right; @@ -40,11 +42,11 @@ export namespace Either { return Left.isLeft(value) || Right.isRight(value); } - export function left(value: L): Either { + export function left(value: L): Either { return Left.of(value); } - export function right(value: R): Either { + export function right(value: R): Either { return Right.of(value); } } diff --git a/packages/alfa-either/src/left.ts b/packages/alfa-either/src/left.ts index 919f43ccfe..b58849d1fa 100644 --- a/packages/alfa-either/src/left.ts +++ b/packages/alfa-either/src/left.ts @@ -1,4 +1,5 @@ import { Equatable } from "@siteimprove/alfa-equatable"; +import { Hash, Hashable } from "@siteimprove/alfa-hash"; import { Serializable } from "@siteimprove/alfa-json"; import { Mapper } from "@siteimprove/alfa-mapper"; import { None, Option } from "@siteimprove/alfa-option"; @@ -56,10 +57,19 @@ export class Left implements Either { return reducer(accumulator, this._value); } - public equals(value: unknown): value is this { + public equals(value: Left): boolean; + + public equals(value: unknown): value is this; + + public equals(value: unknown): boolean { return value instanceof Left && Equatable.equals(value._value, this._value); } + public hash(hash: Hash): void { + Hash.writeBoolean(hash, false); + Hashable.hash(hash, this._value); + } + public *[Symbol.iterator](): Iterator { yield this._value; } diff --git a/packages/alfa-either/src/right.ts b/packages/alfa-either/src/right.ts index 3158ad65c6..7a599c4233 100644 --- a/packages/alfa-either/src/right.ts +++ b/packages/alfa-either/src/right.ts @@ -1,4 +1,5 @@ import { Equatable } from "@siteimprove/alfa-equatable"; +import { Hash, Hashable } from "@siteimprove/alfa-hash"; import { Serializable } from "@siteimprove/alfa-json"; import { Mapper } from "@siteimprove/alfa-mapper"; import { None, Option } from "@siteimprove/alfa-option"; @@ -56,12 +57,21 @@ export class Right implements Either { return reducer(accumulator, this._value); } - public equals(value: unknown): value is this { + public equals(value: Right): boolean; + + public equals(value: unknown): value is this; + + public equals(value: unknown): boolean { return ( value instanceof Right && Equatable.equals(value._value, this._value) ); } + public hash(hash: Hash): void { + Hash.writeBoolean(hash, true); + Hashable.hash(hash, this._value); + } + public *[Symbol.iterator](): Iterator { yield this._value; } diff --git a/packages/alfa-either/tsconfig.json b/packages/alfa-either/tsconfig.json index 01a2ee637e..3bc864162e 100644 --- a/packages/alfa-either/tsconfig.json +++ b/packages/alfa-either/tsconfig.json @@ -12,6 +12,9 @@ { "path": "../alfa-functor" }, + { + "path": "../alfa-hash" + }, { "path": "../alfa-json" }, diff --git a/packages/alfa-selective/package.json b/packages/alfa-selective/package.json new file mode 100644 index 0000000000..f43b9f6fcf --- /dev/null +++ b/packages/alfa-selective/package.json @@ -0,0 +1,37 @@ +{ + "$schema": "http://json.schemastore.org/package", + "name": "@siteimprove/alfa-selective", + "homepage": "https://siteimprove.com", + "version": "0.10.0", + "license": "MIT", + "description": "An implementation of a selective functor for modelling conditional function application", + "repository": { + "type": "git", + "url": "https://github.com/siteimprove/alfa.git", + "directory": "packages/alfa-selective" + }, + "bugs": "https://github.com/siteimprove/alfa/issues", + "main": "src/index.js", + "types": "src/index.d.ts", + "files": [ + "src/**/*.js", + "src/**/*.d.ts" + ], + "dependencies": { + "@siteimprove/alfa-either": "^0.10.0", + "@siteimprove/alfa-equatable": "^0.10.0", + "@siteimprove/alfa-functor": "^0.10.0", + "@siteimprove/alfa-hash": "^0.10.0", + "@siteimprove/alfa-json": "^0.10.0", + "@siteimprove/alfa-mapper": "^0.10.0", + "@siteimprove/alfa-predicate": "^0.10.0", + "@siteimprove/alfa-refinement": "^0.10.0" + }, + "devDependencies": { + "@siteimprove/alfa-test": "^0.10.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://npm.pkg.github.com/" + } +} diff --git a/packages/alfa-selective/src/index.ts b/packages/alfa-selective/src/index.ts new file mode 100644 index 0000000000..0d92aa5c85 --- /dev/null +++ b/packages/alfa-selective/src/index.ts @@ -0,0 +1 @@ +export * from "./selective"; diff --git a/packages/alfa-selective/src/selective.ts b/packages/alfa-selective/src/selective.ts new file mode 100644 index 0000000000..5e1fc60f63 --- /dev/null +++ b/packages/alfa-selective/src/selective.ts @@ -0,0 +1,103 @@ +import { Either, Left, Right } from "@siteimprove/alfa-either"; +import { Equatable } from "@siteimprove/alfa-equatable"; +import { Functor } from "@siteimprove/alfa-functor"; +import { Hash, Hashable } from "@siteimprove/alfa-hash"; +import { Serializable } from "@siteimprove/alfa-json"; +import { Mapper } from "@siteimprove/alfa-mapper"; +import { Predicate } from "@siteimprove/alfa-predicate"; +import { Refinement } from "@siteimprove/alfa-refinement"; + +export class Selective + implements + Functor, + Iterable, + Equatable, + Hashable, + Serializable> { + public static of(value: T): Selective { + return new Selective(Left.of(value)); + } + + private readonly _value: Either; + + private constructor(value: Either) { + this._value = value; + } + + public map(mapper: Mapper): Selective { + return new Selective( + this._value.either( + (value) => Left.of(value) as Either, + (value) => Right.of(mapper(value)) + ) + ); + } + + public if

( + refinement: Refinement, + mapper: Mapper + ): Selective, T | U>; + + public if( + predicate: Predicate, + mapper: Mapper + ): Selective; + + public if( + predicate: Predicate, + mapper: Mapper + ): Selective { + return this._value.either( + (value) => + predicate(value) ? new Selective(Right.of(mapper(value))) : this, + () => this + ); + } + + public else(mapper: Mapper): Selective { + return new Selective( + Right.of( + this._value.either( + (value) => mapper(value), + (value) => value + ) + ) + ); + } + + public get(): S | T { + return this._value.get(); + } + + public equals(value: Selective): boolean; + + public equals(value: unknown): value is this; + + public equals(value: unknown): boolean { + return value instanceof Selective && value._value.equals(this._value); + } + + public hash(hash: Hash): void { + this._value.hash(hash); + } + + public *iterator(): Iterator { + yield this._value.get(); + } + + public [Symbol.iterator](): Iterator { + return this.iterator(); + } + + public toJSON(): Selective.JSON { + return this._value.toJSON(); + } + + public toString(): string { + return `Selective { ${this._value} }`; + } +} + +export namespace Selective { + export type JSON = Either.JSON; +} diff --git a/packages/alfa-selective/test/selective.spec.ts b/packages/alfa-selective/test/selective.spec.ts new file mode 100644 index 0000000000..dedd7cdc02 --- /dev/null +++ b/packages/alfa-selective/test/selective.spec.ts @@ -0,0 +1,58 @@ +import { test } from "@siteimprove/alfa-test"; + +import { Refinement } from "@siteimprove/alfa-refinement"; + +import { Selective } from "../src/selective"; + +const isFoo: Refinement = (string): string is "foo" => + string === "foo"; + +const isBar: Refinement = (string): string is "bar" => + string === "bar"; + +test("#if() conditionally applies a function to a selective value", (t) => { + Selective.of("foo") + .if(isFoo, (value) => { + t.equal(value, "foo"); + }) + .if(isBar, () => { + t.fail(); + }); +}); + +test(`#else() applies a function to a selective value that matched no other + conditions`, (t) => { + Selective.of("bar") + .if(isFoo, () => { + t.fail(); + }) + .else((value) => { + t.equal(value, "bar"); + }); +}); + +test("#get() returns the value of a selective", (t) => { + t.equal( + Selective.of("foo") + .if(isFoo, () => "was foo") + .get(), + "was foo" + ); + + t.equal( + Selective.of("bar") + .if(isFoo, () => "was foo") + .get(), + "bar" + ); +}); + +test(`#map() applies a function to a matched selective value`, (t) => { + t.equal( + Selective.of("foo") + .if(isFoo, () => "was foo") + .map((string) => string.toUpperCase()) + .get(), + "WAS FOO" + ); +}); diff --git a/packages/alfa-selective/tsconfig.json b/packages/alfa-selective/tsconfig.json new file mode 100644 index 0000000000..5ca01b7524 --- /dev/null +++ b/packages/alfa-selective/tsconfig.json @@ -0,0 +1,34 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "../tsconfig.json", + "files": ["src/index.ts", "src/selective.ts", "test/selective.spec.ts"], + "references": [ + { + "path": "../alfa-either" + }, + { + "path": "../alfa-equatable" + }, + { + "path": "../alfa-functor" + }, + { + "path": "../alfa-hash" + }, + { + "path": "../alfa-json" + }, + { + "path": "../alfa-mapper" + }, + { + "path": "../alfa-predicate" + }, + { + "path": "../alfa-refinement" + }, + { + "path": "../alfa-test" + } + ] +} diff --git a/packages/tsconfig.json b/packages/tsconfig.json index fdceb3f010..7876d2500a 100644 --- a/packages/tsconfig.json +++ b/packages/tsconfig.json @@ -80,6 +80,7 @@ { "path": "alfa-rules" }, { "path": "alfa-scraper" }, { "path": "alfa-sarif" }, + { "path": "alfa-selective" }, { "path": "alfa-selector" }, { "path": "alfa-sequence" }, { "path": "alfa-set" }, diff --git a/yarn.lock b/yarn.lock index 26eeb27f56..e396658fef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9084,9 +9084,9 @@ typedarray@^0.0.6: integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= typescript@^4.0.2: - version "4.1.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7" - integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg== + version "4.1.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.4.tgz#f058636e2f4f83f94ddaae07b20fd5e14598432f" + integrity sha512-+Uru0t8qIRgjuCpiSPpfGuhHecMllk5Zsazj5LZvVsEStEjmIRRBZe+jHjGQvsgS7M1wONy2PQXd67EMyV6acg== ua-parser-js@^0.7.21: version "0.7.21"