diff --git a/.github/scripts/check-compatibility.py b/.github/scripts/check-compatibility.py new file mode 100644 index 0000000..7bbc85b --- /dev/null +++ b/.github/scripts/check-compatibility.py @@ -0,0 +1,97 @@ +"""Check public signatures, dependency metadata and a client compiled against 1.3.1.""" +import hashlib +import json +import os +from pathlib import Path +import subprocess +import sys +import urllib.request +from zipfile import ZipFile +import xml.etree.ElementTree as ET + +candidate = Path(sys.argv[1]).resolve() +work = Path('build/compatibility').resolve() +work.mkdir(parents=True, exist_ok=True) +ns = {'m': 'http://maven.apache.org/POM/4.0.0'} +dependencies = ET.parse(candidate.with_suffix('.pom')).findall('.//m:dependency', ns) +assert [[d.find('m:' + k, ns).text for k in ('groupId', 'artifactId', 'version', 'scope')] for d in dependencies] == [ + ['io.github.lambig', 'Tuplite', '1.0.1', 'compile']] +module = json.loads(candidate.with_suffix('.module').read_text()) +for variant in module['variants']: + if variant['name'] in ('apiElements', 'runtimeElements'): + assert variant['attributes']['org.gradle.jvm.version'] == 11 + assert variant['dependencies'] == [{'group': 'io.github.lambig', 'module': 'Tuplite', 'version': {'requires': '1.0.1'}}] + for f in variant['files']: + artifact = candidate.parent / f['url'] + data = artifact.read_bytes() + assert f['size'] == len(data) + for alg in ('sha512', 'sha256', 'sha1', 'md5'): + if alg in f: + assert hashlib.new(alg, data).hexdigest() == f[alg] + +def download(artifact, version): + name = f'{artifact}-{version}.jar' + url = f'https://repo.maven.apache.org/maven2/io/github/lambig/{artifact}/{version}/{name}' + target = work / name + target.write_bytes(urllib.request.urlopen(url, timeout=60).read()) + assert hashlib.sha1(target.read_bytes()).hexdigest() == urllib.request.urlopen(url + '.sha1', timeout=60).read().decode().strip() + return target + +baseline = download('Patterns', '1.3.1') +old_tuple = download('Tuplite', '1.0.0') +old_fie = download('functional-interface-extension', '1.1.1') +new_tuple = download('Tuplite', '1.0.1') +new_fie = download('functional-interface-extension', '1.1.3') +java_home = Path(os.environ['JAVA_HOME']) +runtime = Path(os.environ.get('TEST_JAVA_HOME', os.environ['JAVA_HOME'])) / 'bin/java' + +def classes(jar): + with ZipFile(jar) as archive: + names = sorted(name for name in archive.namelist() if name.endswith('.class')) + for name in names: + assert int.from_bytes(archive.read(name)[6:8], 'big') == 55, name + return [name[:-6].replace('/', '.') for name in names] + +types = classes(baseline) +assert types == classes(candidate) +for name in types: + apis = [] + for label, jar in [('baseline', baseline), ('candidate', candidate)]: + api = subprocess.check_output([str(java_home / 'bin/javap'), '-public', '-s', '-classpath', str(jar), name], text=True) + (work / (name + '-' + label + '-api.txt')).write_text(api, encoding='utf-8') + apis.append(sorted(line.strip() for line in api.splitlines() if not line.startswith('Compiled from'))) + assert apis[0] == apis[1], name + ': public signature changed' + +client = work / 'client' +client.mkdir(exist_ok=True) +source = work / 'OldClient.java' +source.write_text('''import io.github.lambig.patterns.*; +import io.github.lambig.tuplite._2.Tuple2; +import java.util.*; +import java.util.function.*; +public class OldClient { + public static void main(String[] args) { + Tuple2, Function> rule = Patterns.when(n -> n > 0, Patterns.then("positive")); + Patterns p = new Patterns<>(List.of(rule)); + if (!p.apply(1).equals("positive") || p.getOptionally(0).isPresent() + || !p.orElse("zero").apply(0).equals("zero")) throw new AssertionError(); + List seen = new ArrayList<>(); + ConsumingPatterns c = ConsumingPatterns.of(ConsumingPatterns.when(n -> n > 0, seen::add)); + c.accept(1); + c.orElseDo(seen::add).accept(0); + if (!seen.equals(List.of(1,0))) throw new AssertionError(); + Throwable cause = new IllegalArgumentException("reason"); + NoSuchPatternException e = new NoSuchPatternException(cause); + if (!"reason".equals(e.getMessage()) || e.getCause() != cause) throw new AssertionError(); + NoSuchPatternException empty = new NoSuchPatternException(); + empty.initCause(cause); + if (empty.getCause() != cause) throw new AssertionError(); + System.out.println("Old compiled client: OK"); + } +}''', encoding='utf-8') +subprocess.run([str(java_home / 'bin/javac'), '--release', '11', '-cp', os.pathsep.join(map(str, (baseline, old_tuple, old_fie))), '-d', str(client), str(source)], check=True) +for libs in [(baseline, old_tuple, old_fie), (baseline, new_tuple, new_fie), (candidate, new_tuple, new_fie)]: + subprocess.run([str(runtime), '-cp', os.pathsep.join(map(str, (client, *libs))), 'OldClient'], check=True) +print(f'Public signatures preserved: {len(types)} classes; Java 11 bytecode=55') +print(f'Baseline SHA256: {hashlib.sha256(baseline.read_bytes()).hexdigest()}') +print('Old client passed; POM/module export Tuplite 1.0.1; module file hashes match') diff --git a/.github/scripts/check-readme-example.py b/.github/scripts/check-readme-example.py new file mode 100644 index 0000000..782be03 --- /dev/null +++ b/.github/scripts/check-readme-example.py @@ -0,0 +1,25 @@ +"""Compile and execute both complete programs from the README.""" +import os +from pathlib import Path +import re +import subprocess +import sys + +work = Path('build/readme-example').resolve() +work.mkdir(parents=True, exist_ok=True) +java_home = Path(os.environ['JAVA_HOME']) +runtime = Path(os.environ.get('TEST_JAVA_HOME', os.environ['JAVA_HOME'])) / 'bin/java' +expected = { + 'ClassificationExample': ['large', 'Optional.empty', '[large, small, unknown]'], + 'RoutingExample': ['[large:12, other:0]'], +} +blocks = re.findall(r'```java\n(.*?)```', Path('README.md').read_text(encoding='utf-8'), re.S) +assert len(blocks) == len(expected) +for source in blocks: + name = re.search(r'public class (\w+)', source).group(1) + file = work / (name + '.java') + file.write_text(source, encoding='utf-8') + subprocess.run([str(java_home / 'bin/javac'), '--release', '11', '-encoding', 'UTF-8', '-cp', sys.argv[1], '-d', str(work), str(file)], check=True) + result = subprocess.check_output([str(runtime), '-cp', str(work) + os.pathsep + sys.argv[1], name], text=True) + assert result.splitlines() == expected[name], result + print(name + ': ' + result.strip().replace('\n', '; ')) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a9ad03..7f62e3e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,9 +14,13 @@ concurrency: jobs: verify: - name: Java 11 tests and documentation + name: Java ${{ matrix.java }} contract and consumers runs-on: ubuntu-24.04 timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + java: ['11', '25'] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: @@ -24,19 +28,25 @@ jobs: - uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6 with: distribution: temurin - java-version: '11' - - name: Test and build documentation with the pinned Wrapper - run: bash ./gradlew --no-daemon --console=plain clean test javadocJar sourcesJar - - name: Verify documentation archive contents - run: python3 .github/scripts/check-documentation.py + java-version: ${{ matrix.java }} + - name: Select test runtime + run: echo "TEST_JAVA_HOME=$JAVA_HOME" >> "$GITHUB_ENV" + - uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6 + with: + distribution: temurin + java-version: '17' + - name: Verify library, API compatibility and independent consumers + run: sh ./verify-consumers.sh candidate - name: Preserve test reports and documentation if: always() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 with: - name: java-11-verification + name: java-${{ matrix.java }}-verification retention-days: 14 path: | build/test-results/test/ build/reports/tests/test/ build/libs/*-javadoc.jar build/libs/*-sources.jar + build/consumer-results/ + build/compatibility/*-api.txt diff --git a/.gitignore b/.gitignore index a110e5b..51d922f 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,6 @@ hs_err_pid* .gradle/ build/ .idea/ -gradle.properties \ No newline at end of file +gradle.properties +# Maven example output +example/target/ diff --git a/DEVELOPING.md b/DEVELOPING.md new file mode 100644 index 0000000..3260162 --- /dev/null +++ b/DEVELOPING.md @@ -0,0 +1,53 @@ +# 開発・検証 + +ビルドにはJDK 17以上、導入例の確認にはMaven 3.8以上、Python 3、POSIXシェルを使います。 +Gradleは同梱Wrapperから取得します。ライブラリと導入例はJava 11のAPI・バイトコードを対象にコンパイルします。 +`TEST_JAVA_HOME`を指定すると、試験と導入例をそのJavaで実行できます。 +CIはビルドJDK 17を用い、実行環境Java 11/25を確認します。 + +```sh +sh ./verify-consumers.sh candidate +``` + +このコマンドは単体試験、sources/Javadoc JAR、1.3.1との公開シグネチャ比較、 +旧版でコンパイルした利用コード、READMEのプログラム、独立したMaven/Gradle導入例を確認します。 +候補版は`build/repository`に生成されます。導入例は一時キャッシュを作り、 +Patternsだけを直接の依存として宣言してコンパイル・実行します。 +結果は`build/consumer-results/candidate`、試験詳細は`build/reports/tests/test`に残ります。 + +## 導入例を個別に実行する + +```sh +sh ./gradlew publishMavenJavaPublicationToCandidateRepository +sh ./gradlew -p example -PcandidateRepository="$PWD/build/repository" runExample +mvn -f example/pom.xml -DcandidateRepository="file://$PWD/build/repository" \ + compile dependency:build-classpath -Dmdep.outputFile=target/runtime-classpath.txt +java -cp "example/target/classes:$(cat example/target/runtime-classpath.txt)" example.PatternExample +``` + +出力は次のとおりです。 + +```text +classification=large; fallback=unknown; events=[large:12, other:0]; tuple-api=OK +``` + +Maven Centralへの公開後は、次のコマンドで公開された1.3.2を新しいキャッシュへ取得し、 +同じ導入例とREADMEのプログラムを確認できます。 + +```sh +sh ./verify-consumers.sh public 1.3.2 +``` + +## 公開用bundle + +公開用のASCII形式秘密鍵を`SIGNING_KEY`、そのパスフレーズを`SIGNING_PASSWORD`に設定して実行します。 +認証情報は環境変数で渡します。 + +```sh +sh ./gradlew clean check -PreleaseBundle centralBundle +``` + +`build/central/Patterns-1.3.2-central.zip`には本体、sources、Javadoc、POM、 +Gradle Module Metadataと、それぞれの署名・チェックサムが入ります。 +公開者は対象コミット、検証結果とbundle内容を確認したうえでCentral Portalへ登録します。 +公開後は上記の`public`による取得・導入確認を行います。 diff --git a/README.md b/README.md index 052fb24..266a245 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,158 @@ [![CI](https://github.com/lambig/Patterns/actions/workflows/ci.yml/badge.svg)](https://github.com/lambig/Patterns/actions/workflows/ci.yml) + # Patterns -easy implementation of pattern-matcher works as a function -Note: this README is written through the help of ChatGPT. -## Overview -Patterns is a functional utility library for handling conditional logic in a more declarative and readable way. -It helps you to deal with the complex control flow by using pattern matching. +入力に応じた値や処理を、優先順位のある条件の組で表すJavaライブラリです。 +`Patterns`は分類名や料金などの結果を返す`Function`、 +`ConsumingPatterns`は通知先の振り分けなどを行う`Consumer`として使えます。 +どちらも、最初に一致した条件の結果・処理がその入力に対する答えになります。 -## Features +## 導入 -- Conditional mapping based on predicates -- Default value when no conditions are met (`orElse`) -- Custom exception throwing (`orElseThrow`) -- Handling/Denying null values -- Support for type-based pattern matching +Java 11以上が対象です。このブランチは1.3.2の公開候補です。 +Maven Centralへの公開後は、以下の依存宣言で利用できます。 +候補版を試す手順は[開発・検証手順](DEVELOPING.md)にあります。 -## Installation(gradle) +Gradle: -```gradle -implementation group: 'io.github.lambig', name: 'Patterns', version: '1.3.1' +```groovy +repositories { mavenCentral() } +dependencies { implementation 'io.github.lambig:Patterns:1.3.2' } ``` +Maven: + +```xml + + io.github.lambig + Patterns + 1.3.2 + +``` -## Usage +公開APIに現れる`Tuple2`はTuplite 1.0.1から利用できます。 +Patternsの依存宣言でTupliteとfunctional-interface-extension 1.1.3も解決されます。 -### Examples +## 入力から分類を得る ```java -Patterns target = patterns( - when(equalsTo(3), then("b")), - when(equalsTo(4), thenSupply(() -> "c")), - when(i -> i > 0, thenApply(Object::toString)), - when(i -> i < 0, thenApply(sequenceOf((UnaryOperator) i -> i + 1, Number::longValue, Object::toString))), - orElse(then("a")) -); - -List actual = Stream.of(-1, 0, 1, 2, 3, 4) - .map(target) - .collect(toList()); // actual will contain ["0", "a", "1", "2", "b", "c"] +import io.github.lambig.patterns.Patterns; +import java.util.List; +import java.util.stream.Collectors; +import static io.github.lambig.patterns.Patterns.then; +import static io.github.lambig.patterns.Patterns.when; + +public class ClassificationExample { + public static void main(String[] args) { + Patterns classification = Patterns.of( + when(n -> n >= 10, then("large")), + when(n -> n > 0, then("small"))); + + System.out.println(classification.get(12)); + System.out.println(classification.getOptionally(0)); + System.out.println(List.of(12, 3, 0).stream() + .map(classification.orElse("unknown")) + .collect(Collectors.toList())); + } +} ``` -```java -//SetUp -AtomicReference integer1 = new AtomicReference<>(); // will have 3 -AtomicReference integer2 = new AtomicReference<>(); // will have 4 -AtomicReference integer3 = new AtomicReference<>(); // will have 2 +出力: + +```text +large +Optional.empty +[large, small, unknown] +``` + +12は両方の条件に一致するため、先に指定した`large`になります。 +具体的な条件を先に、広い条件や既定の規則を後に置くことで優先順位を表現できます。 +最初に一致した条件より後の述語と関数は呼ばれません。 + +結果の使い方は次のように選べます。 + +| 呼び出し | 結果が得られる場合 | 条件が未一致の場合 | 最初に一致した関数の結果がnullの場合 | +| --- | --- | --- | --- | +| `get(key)` / `apply(key)` | 結果の値 | `NoSuchPatternException` | `NullPointerException` | +| `getOptionally(key)` / `optional().apply(key)` | 値を含むOptional | 空のOptional | 空のOptional | +| `orElse(value).apply(key)` | 結果の値 | 指定した値 | 指定した値 | +| `orElseGet(supplier).apply(key)` | 結果の値 | Supplierの結果 | Supplierの結果 | +| `orElseThrow(supplier).apply(key)` | 結果の値 | Supplierが返す例外 | Supplierが返す例外 | + +`orElseGet`のSupplierは、結果が空の入力ごとに呼ばれます。 +最初に一致した関数がnullを返す場合も、その入力の結果は空で確定します。 +`Patterns.orElse(function)`はすべての入力に一致する**規則**です。 +最後に置くと、先行する条件がすべて未一致だった入力を扱えます。 +対して、インスタンスの`classification.orElse(value)`は、上表のとおり結果の空も補います。 + +## 値・型に応じた条件と結果 + +`when(predicate, function)`は、条件と結果を求める関数を組にします。 +`then(value)`は固定値、`thenSupply(supplier)`はその規則が選ばれるたびに得る値、 +`thenApply(function)`は入力を変換した結果を表します。 +`when`の第2引数にはラムダ式やメソッド参照を直接指定できます。 -ConsumingPatterns target = - ConsumingPatterns.of( - when(equalsTo(3), thenAcceptWith(integer1::set)), - when(equalsTo(4), thenAcceptWith(integer2::set))); +`whenMatch(Integer.class, function)`はIntegerの入力に一致します。 +`whenMatch(Integer.class, predicate, function)`では、その型の値が満たす追加条件を指定できます。 +対象の型のサブクラス・実装クラスも一致し、nullは未一致になります。 +追加条件と関数は指定した型で入力を受け取れます。 -Stream.of(-1, 0, 1, 2, 3, 4).forEach(target.orElseDo(integer3::set)); +## 入力に応じた処理を行う + +```java +import io.github.lambig.patterns.ConsumingPatterns; +import java.util.ArrayList; +import java.util.List; + +public class RoutingExample { + public static void main(String[] args) { + List events = new ArrayList<>(); + ConsumingPatterns routing = ConsumingPatterns.of( + ConsumingPatterns.when(n -> n >= 10, n -> events.add("large:" + n)), + ConsumingPatterns.when(n -> n > 0, n -> events.add("small:" + n))); + routing.handle(12); + routing.orElseDo(n -> events.add("other:" + n)).accept(0); + System.out.println(events); + } +} ``` + +出力は`[large:12, other:0]`です。 +`handle`と`accept`は最初に一致した条件の処理をひとつ実行します。 +未一致の入力には`NoSuchPatternException`となり、`orElseDo`で既定の処理、 +`orElseThrow`で未一致時の例外を選べます。 +ひとつの入力に対して該当する処理をすべて実行したい用途では、各条件を個別に判定する構成が適しています。 + +## null・例外・作成後の変更 + +入力のnullは、指定した述語・関数・処理が受け取ります。 +`Patterns.equalsTo(null)`はnullの入力に一致します。 +`ConsumingPatterns.equalsTo`の比較対象は非nullです。nullの入力を条件にする場合は`Objects::isNull`を使えます。 +規則のリスト・配列、各規則、その条件と関数・処理には非nullの値を指定します。 +空のリスト・可変長引数では、すべての入力が未一致になります。 + +述語・関数・処理からの例外は呼出元へ伝わり、その入力の判定・処理はそこで終了します。 +`getOptionally`などで空を扱う場合も、例外はそのまま伝わります。 +処理が例外の前に行った更新は、その処理の結果として残ります。 + +作成後に元のリスト・配列の要素を入れ替えても、規則の組合せと順序は保たれます。 +述語・関数・処理が参照する状態は共有されるため、可変状態を参照する条件の答えは状態に応じて変わります。 +複数スレッドから使う場合は、これらの処理と参照先の状態を、その実行方法に合わせて設計します。 + +## 1.3.1から1.3.2への更新 + +1.3.1の`new Patterns<>(list)`と`ConsumingPatterns.of(list)`/`consumingPatterns(list)`では、 +作成後に元のリストで規則を差し替え・追加・削除すると、作成済みインスタンスの動作も変わりました。 +1.3.2では、いずれも作成時の規則の組合せ・順序を保持します。 +`Patterns.of(List)`と`Patterns.patterns(List)`は、1.3.1からこの扱いです。 + +動的に規則を更新する場合は、変更したリストから`Patterns`/`ConsumingPatterns`を再構築し、 +呼び出し側が使うインスタンスを置き換えてください。公開メソッドのシグネチャは同じですが、 +元のリストの変更を反映させていた利用コードには、この移行が必要です。 + +## API文書と実行例 + +- [独立したMaven/Gradleプロジェクト](example/)は、分類、型条件、処理の振り分けと`Tuple2`を含むAPIを利用します。 +- `./gradlew javadoc`で生成するAPI文書は`build/docs/javadoc/index.html`から参照できます。 +- 公開物には同じ版の`Patterns-1.3.2-sources.jar`と`Patterns-1.3.2-javadoc.jar`が含まれます。 +- [開発・検証手順](DEVELOPING.md)に候補版・公開版の導入確認と公開用bundleの作成手順を記載しています。 diff --git a/build.gradle b/build.gradle index 444d97d..51aa650 100644 --- a/build.gradle +++ b/build.gradle @@ -1,104 +1,104 @@ -plugins { - id 'java-library' - id 'maven-publish' - id 'signing' -} - -group = 'io.github.lambig' -archivesBaseName = 'Patterns' -version = '1.3.1' - -sourceCompatibility = 1.11 - -repositories { - mavenCentral() -} - -dependencies { - implementation 'org.projectlombok:lombok:1.18.28' - annotationProcessor 'org.projectlombok:lombok:1.18.28' - - implementation group: 'io.github.lambig', name: 'functional-interface-extension', version: '1.1.1' - implementation group: 'io.github.lambig', name: 'Tuplite', version: '1.0.0' - - testCompileOnly 'org.projectlombok:lombok:1.18.28' - testAnnotationProcessor 'org.projectlombok:lombok:1.18.28' - testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.6.2' - testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.6.2' - testImplementation group: 'org.assertj', name: 'assertj-core', version: '3.23.1' -} - -tasks.withType(JavaCompile).configureEach { - options.encoding = "UTF-8" -} - -tasks.register('javadocJar', Jar) { - dependsOn javadoc - archiveClassifier.set('javadoc') - from javadoc.destinationDir -} - -tasks.register('sourcesJar', Jar) { - dependsOn classes - archiveClassifier.set('sources') - from sourceSets.main.allSource -} - -artifacts { - archives javadocJar, sourcesJar -} - -test { - useJUnitPlatform() - test.outputs.upToDateWhen { false } -} - -java { - withJavadocJar() - withSourcesJar() -} - -publishing { - publications { - mavenJava(MavenPublication) { - artifactId = 'Patterns' - from components.java - pom { - name = 'Patterns' - description = 'easy implementation of pattern-matcher works as a function' - url = 'https://github.com/lambig/Patterns' - licenses { - license { - name = 'MIT License' - url = 'https://opensource.org/licenses/MIT' - } - } - developers { - developer { - id = 'lambig' - name = 'Lambig' - email = 'ceolnua51@gmail.com' - } - } - scm { - connection = 'scm:git:https://github.com/lambig/Patterns' - url = 'https://github.com/lambig/Patterns' - } - } - } - } - repositories { -// maven { -// name = "OSSRH" -// url = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" -// credentials { -// username = ossrhUsername -// password = ossrhPassword -// } -// } - } - - signing { - sign publishing.publications.mavenJava - } -} +plugins { + id 'java-library' + id 'maven-publish' + id 'signing' +} + +group = 'io.github.lambig' +version = '1.3.2' + +repositories { mavenCentral() } + +dependencies { + api 'io.github.lambig:Tuplite:1.0.1' + compileOnly 'org.projectlombok:lombok:1.18.42' + annotationProcessor 'org.projectlombok:lombok:1.18.42' + testCompileOnly 'org.projectlombok:lombok:1.18.42' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.42' + testImplementation 'io.github.lambig:functional-interface-extension:1.1.3' + testImplementation platform('org.junit:junit-bom:5.13.4') + testImplementation 'org.junit.jupiter:junit-jupiter' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testImplementation 'org.assertj:assertj-core:3.23.1' +} + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + withSourcesJar() + withJavadocJar() +} +tasks.withType(JavaCompile).configureEach { + options.release = 11 + options.encoding = 'UTF-8' +} +tasks.withType(Javadoc).configureEach { options.encoding = 'UTF-8' } +tasks.withType(Test).configureEach { + useJUnitPlatform() + if (providers.environmentVariable('TEST_JAVA_HOME').isPresent()) { + executable = providers.environmentVariable('TEST_JAVA_HOME').get() + '/bin/java' + } +} +tasks.withType(Jar).configureEach { from('LICENSE') { into 'META-INF' } } + +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + pom { + name = 'Patterns' + description = 'Ordered conditional functions and consumers for Java 11.' + url = 'https://github.com/lambig/Patterns' + licenses { + license { + name = 'MIT License' + url = 'https://opensource.org/licenses/MIT' + } + } + developers { developer { id = 'lambig'; name = 'Lambig' } } + scm { + connection = 'scm:git:https://github.com/lambig/Patterns.git' + developerConnection = 'scm:git:ssh://git@github.com/lambig/Patterns.git' + url = 'https://github.com/lambig/Patterns' + } + } + } + } + repositories { + maven { + name = 'Candidate' + url = layout.buildDirectory.dir('repository') + } + } +} + +// 署名鍵は公開用bundleの生成時だけ環境から受け取る。 +def releaseBundle = providers.gradleProperty('releaseBundle').isPresent() +signing { + required = releaseBundle + if (releaseBundle) { + if (providers.environmentVariable('SIGNING_KEY').isPresent()) { + useInMemoryPgpKeys(providers.environmentVariable('SIGNING_KEY').get(), + providers.environmentVariable('SIGNING_PASSWORD').orNull) + } + sign publishing.publications.mavenJava + } +} + +tasks.register('centralBundle', Zip) { + dependsOn 'publishMavenJavaPublicationToCandidateRepository' + archiveFileName = "${project.name}-${project.version}-central.zip" + destinationDirectory = layout.buildDirectory.dir('central') + from(layout.buildDirectory.dir('repository')) { + include "io/github/lambig/${project.name}/${project.version}/*.jar*" + include "io/github/lambig/${project.name}/${project.version}/*.pom*" + include "io/github/lambig/${project.name}/${project.version}/*.module*" + } + doFirst { + if (!releaseBundle) throw new GradleException('centralBundle requires -PreleaseBundle and signing credentials') + ['.jar', '-sources.jar', '-javadoc.jar', '.pom', '.module'].each { suffix -> + def signature = layout.buildDirectory.file("repository/io/github/lambig/${project.name}/${project.version}/${project.name}-${project.version}${suffix}.asc").get().asFile + if (!signature.isFile()) throw new GradleException("Missing signature: ${signature.name}") + } + } +} diff --git a/example/build.gradle b/example/build.gradle new file mode 100644 index 0000000..c5e5c44 --- /dev/null +++ b/example/build.gradle @@ -0,0 +1,36 @@ +plugins { id 'java' } + +repositories { + if (providers.gradleProperty('candidateRepository').isPresent()) { + exclusiveContent { + forRepository { + maven { + url = uri(providers.gradleProperty('candidateRepository').get()) + } + } + filter { includeModule('io.github.lambig', 'Patterns') } + } + } + mavenCentral() +} +dependencies { + implementation "io.github.lambig:Patterns:${providers.gradleProperty('libraryVersion').getOrElse('1.3.2')}" +} +tasks.withType(JavaCompile).configureEach { + options.release = 11 + options.encoding = 'UTF-8' +} +tasks.register('runExample', JavaExec) { + dependsOn 'classes' + classpath = sourceSets.main.runtimeClasspath + mainClass = 'example.PatternExample' + if (providers.environmentVariable('TEST_JAVA_HOME').isPresent()) { + executable = providers.environmentVariable('TEST_JAVA_HOME').get() + '/bin/java' + } + doFirst { + def jars = configurations.runtimeClasspath.files + if (jars*.name.sort() != ["Patterns-${providers.gradleProperty('libraryVersion').getOrElse('1.3.2')}.jar".toString(), 'Tuplite-1.0.1.jar', 'functional-interface-extension-1.1.3.jar'].sort()) { + throw new GradleException("Unexpected runtime dependencies: ${jars*.name}") + } + } +} diff --git a/example/pom.xml b/example/pom.xml new file mode 100644 index 0000000..38803f6 --- /dev/null +++ b/example/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + example + Patterns-example + 1.0.0 + + UTF-8 + 11 + 1.3.2 + + + + io.github.lambig + Patterns + ${libraryVersion} + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.14.0 + + + org.apache.maven.plugins + maven-dependency-plugin + 3.8.1 + + + + + + candidate + candidateRepository + + + candidate + ${candidateRepository} + fail + false + + + + + diff --git a/example/settings.gradle b/example/settings.gradle new file mode 100644 index 0000000..1eaebf8 --- /dev/null +++ b/example/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'patterns-example' diff --git a/example/src/main/java/example/PatternExample.java b/example/src/main/java/example/PatternExample.java new file mode 100644 index 0000000..cb311a1 --- /dev/null +++ b/example/src/main/java/example/PatternExample.java @@ -0,0 +1,40 @@ +package example; + +import io.github.lambig.patterns.ConsumingPatterns; +import io.github.lambig.patterns.Patterns; +import io.github.lambig.tuplite._2.Tuple2; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; +import static io.github.lambig.patterns.Patterns.then; +import static io.github.lambig.patterns.Patterns.when; + +/** Patternsだけを依存宣言した外部パッケージから、分類と処理の振り分けを利用する例。 */ +public class PatternExample { + public static void main(String[] args) { + Tuple2, Function> large = when(n -> n >= 10, then("large")); + Patterns classification = Patterns.of(large, when(n -> n > 0, then("small"))); + if (!classification.get(12).equals("large")) throw new AssertionError("first match"); + if (!classification.get(3).equals("small")) throw new AssertionError("second match"); + if (classification.getOptionally(0).isPresent()) throw new AssertionError("unmatched"); + if (!classification.orElse("unknown").apply(0).equals("unknown")) throw new AssertionError("fallback"); + + Patterns integers = Patterns.of(Patterns.whenMatch(Integer.class, + n -> n > 0, n -> "integer:" + n)); + if (!integers.get(3).equals("integer:3") || integers.getOptionally(1.5).isPresent()) { + throw new AssertionError("type condition"); + } + + List events = new ArrayList<>(); + ConsumingPatterns routing = ConsumingPatterns.of( + ConsumingPatterns.when(n -> n >= 10, n -> events.add("large:" + n)), + ConsumingPatterns.when(n -> n > 0, n -> events.add("small:" + n))); + routing.accept(12); + routing.orElseDo(n -> events.add("other:" + n)).accept(0); + if (!events.equals(List.of("large:12", "other:0"))) throw new AssertionError(events); + System.out.println("classification=" + classification.get(12) + + "; fallback=" + classification.orElse("unknown").apply(0) + + "; events=" + events + "; tuple-api=OK"); + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 87b738c..8bdaf60 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e687626..c42672d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ -#Sun May 09 02:00:51 JST 2021 -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStorePath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index af6708f..adff685 100644 --- a/gradlew +++ b/gradlew @@ -1,78 +1,128 @@ -#!/usr/bin/env sh +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# 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 +# +# https://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. +# +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m"' +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -81,92 +131,118 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=$((i+1)) + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" fi +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 0f8d593..e509b2d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,4 +1,22 @@ -@if "%DEBUG%" == "" @echo off +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -9,25 +27,29 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -35,48 +57,35 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/src/main/java/io/github/lambig/patterns/ConsumingPatterns.java b/src/main/java/io/github/lambig/patterns/ConsumingPatterns.java index 6d1c942..2904a9d 100644 --- a/src/main/java/io/github/lambig/patterns/ConsumingPatterns.java +++ b/src/main/java/io/github/lambig/patterns/ConsumingPatterns.java @@ -1,208 +1,249 @@ -package io.github.lambig.patterns; - -import io.github.lambig.tuplite._2.Tuple2; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.function.Supplier; -import java.util.stream.Stream; -import lombok.NonNull; -import static io.github.lambig.tuplite._2.Tuple2.tuple; -import static java.util.stream.Collectors.toList; - -/** - * 副作用前提の疑似パターンマッチ定義クラス - * パターンは定義順に評価します。 - * - * @param キー型 - */ -public class ConsumingPatterns implements Consumer { - - private final List, Consumer>> mappings; - - private ConsumingPatterns(@NonNull List, Consumer>> patterns) { - this.mappings = patterns; - } - - /** - * パターンを宣言します。 - * - * @param patterns 宣言する各パターン - * @param 入力型 - * @return パターンによるマッピング - */ - @SafeVarargs - public static ConsumingPatterns of(@NonNull Tuple2, Consumer>... patterns) { - return consumingPatterns(patterns); - } - - /** - * パターンを宣言します。 - * - * @param patterns 宣言する各パターン - * @param 入力型 - * @return パターンによるマッピング - */ - public static ConsumingPatterns of(@NonNull List, Consumer>> patterns) { - return consumingPatterns(patterns); - } - - /** - * パターンを宣言します。 - * - * @param patterns 宣言する各パターン - * @param 入力型 - * @return パターンによるマッピング - */ - @SafeVarargs - public static ConsumingPatterns consumingPatterns(@NonNull Tuple2, Consumer>... patterns) { - return consumingPatterns(Stream.of(patterns).collect(toList())); - } - - /** - * パターンを宣言します。 - * - * @param patterns 宣言する各パターン - * @param 入力型 - * @return パターンによるマッピング - */ - public static ConsumingPatterns consumingPatterns(@NonNull List, Consumer>> patterns) { - return new ConsumingPatterns<>(patterns); - } - - /** - * キーを最初に該当するパターンの処理に与えて実行します。 - * - * @param key キー - * @throws NoSuchPatternException 該当するパターンがない場合 - */ - @NonNull - public void handle(K key) { - this.getConsumer(key) - .orElseThrow(() -> new NoSuchPatternException("for key: " + key + ". To allow this pattern to accept value that match no defined pattern, consider setting default consumer.")) - .accept(key); - } - - @NonNull - private Optional> getConsumer(K key) { - return this.mappings.stream() - .filter(entry -> entry._1().test(key)) - .map(Tuple2::_2) - .findFirst(); - } - - /** - * キーを該当パターンの処理またはデフォルト処理に与えて実行します。 - * - * @param defaultConsumer デフォルト処理 - * @return このPatternでデフォルト値付きで適用するConsumer - */ - public Consumer orElseDo(@NonNull Consumer defaultConsumer) { - return k -> this.getConsumer(k).orElse(defaultConsumer).accept(k); - } - - /** - * キーを処理に与えて実行し、キーが未設定の場合例外を送出するConsumerを返します。 - * - * @param exceptionSupplier 指定例外のSupplier - * @return このPatternでmapするFunction Functionはキーが未設定の場合指定例外を送出する - */ - public Consumer orElseThrow(@NonNull Supplier exceptionSupplier) { - return k -> this.getConsumer(k).orElseThrow(exceptionSupplier).accept(k); - } - - @Override - public void accept(K k) { - this.handle(k); - } - - /** - * キーがwhenを満たした場合にキーに適用する処理を明示します。 - * (実はthenAcceptWithを書かなくても直接whenの第2引数に処理を指定すれば動きます - * - * @param consumer 適用する関数 - * @param キー型 - * @return パターンに設定する返却関数 - */ - public static Consumer thenAcceptWith(@NonNull Consumer consumer) { - return consumer::accept; - } - - /** - * パターンを定義します。 - * - * @param when キーがこのパターンに該当する条件 - * @param consumer キーがこのパターンに該当する場合、キーに適用する処理 - * @param キー型 - * @return パターン - */ - public static Tuple2, Consumer> when(@NonNull Predicate when, @NonNull Consumer consumer) { - return tuple(when::test, consumer::accept); - } - - /** - * 引数と等価であることをwhenと定義します。 - * - * @param target キーとの比較対象 - * @param キー型 - * @return 等価判定 - */ - public static Predicate equalsTo(@NonNull S target) { - return s -> Objects.equals(s, target); - } - - /** - * あらゆるキーに該当するパターンを返却します。 - * デフォルト値の設定に有用です。 - * - * @param thenAcceptWith キーがこのパターンに該当する場合、キーに適用する処理 - * @param キー型 - * @return デフォルトパターン - */ - public static Tuple2, Consumer> orElse(@NonNull Consumer thenAcceptWith) { - return tuple(anything -> true, thenAcceptWith::accept); - } - - /** - * あらゆるキーに対し例外を送出させます。 - * 想定外パターンの対応に有用です。 - * - * @param thenAcceptWith キーがこのパターンに該当する場合、キーに適用して例外を生成する関数 - * @param キー型 - * @return 例外パターン - */ - public static Tuple2, Consumer> orElseThrow(@NonNull Function thenAcceptWith) { - return tuple(anything -> true, input -> { - throw thenAcceptWith.apply(input); - }); - } - - /** - * クラスによるパターンマッチを定義します。 - * - * @param clazz キーがこのパターンに該当する条件となるクラス - * @param thenAcceptWith キーがこのパターンに該当する場合、clazzクラスにキャストしたキーに適用する処理 - * @param キー型 - * @return パターン - */ - public static Tuple2, Consumer> whenMatch(@NonNull Class clazz, @NonNull Consumer thenAcceptWith) { - return tuple(clazz::isInstance, instance -> thenAcceptWith.accept(clazz.cast(instance))); - } - - /** - * クラスによるパターンマッチを定義します。当該クラスのインスタンスとしたうえで満たすべき述語を定義できます。 - * - * @param clazz キーがこのパターンに該当する条件となるクラス - * @param when 上記クラスかつ満たすべき条件 - * @param thenAcceptWith キーがこのパターンに該当する場合、clazzクラスにキャストしたキーに適用する処理 - * @param キー型 - * @return パターン - */ - public static Tuple2, Consumer> whenMatch(@NonNull Class clazz, Predicate when, @NonNull Consumer thenAcceptWith) { - return tuple(key -> clazz.isInstance(key) && when.test(clazz.cast(key)), instance -> thenAcceptWith.accept(clazz.cast(instance))); - } - -} +package io.github.lambig.patterns; + +import io.github.lambig.tuplite._2.Tuple2; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Stream; +import lombok.NonNull; +import static io.github.lambig.tuplite._2.Tuple2.tuple; +import static java.util.stream.Collectors.toList; + +/** + * 入力に最初に一致する条件の処理を実行するConsumer。 + * + *

条件の並び順が優先順位です。一致した処理だけを実行するため、通知先の振り分けなどに使えます。 + * 一致する条件がない入力は{@link #handle(Object)}でNoSuchPatternExceptionとなり、 + * {@link #orElseDo(Consumer)}で既定の処理を選べます。 + * + *

作成後に元のリストや配列の要素を入れ替えても、条件と処理の組合せ・順序は保たれます。 + * 条件・処理が参照する状態は共有され、入力がnullの場合の結果もそれらの定義によります。 + * 条件や処理の例外は呼出元へ伝わり、その入力に対する処理はそこで終了します。 + * + * @param 入力の型 + */ +public class ConsumingPatterns implements Consumer { + + private final List, Consumer>> mappings; + + private ConsumingPatterns(@NonNull List, Consumer>> patterns) { + this.mappings = new ArrayList<>(patterns); + } + + /** + * 指定順を優先順位として、最初に一致する条件の処理を行う振り分けを作ります。 + * + *

元の配列の変更にかかわらず、作成時の組合せ・順序を保持します。空の指定ではすべての入力が未一致となります。 + * + * @param 入力の型 + * @param patterns 条件と処理の組。各組とその要素は非null + * @return 指定した条件と順序で入力を扱うConsumingPatterns + * @throws NullPointerException patterns自体がnullの場合 + */ + @SafeVarargs + public static ConsumingPatterns of(@NonNull Tuple2, Consumer>... patterns) { + return consumingPatterns(patterns); + } + + /** + * 指定順を優先順位として、最初に一致する条件の処理を行う振り分けを作ります。 + * + *

元のリストの変更にかかわらず、作成時の組合せ・順序を保持します。空の指定ではすべての入力が未一致となります。 + * + * @param 入力の型 + * @param patterns 条件と処理の組。各組とその要素は非null + * @return 指定した条件と順序で入力を扱うConsumingPatterns + * @throws NullPointerException patterns自体がnullの場合 + */ + public static ConsumingPatterns of(@NonNull List, Consumer>> patterns) { + return consumingPatterns(patterns); + } + + /** + * 指定順を優先順位として、最初に一致する条件の処理を行う振り分けを作ります。 + * + *

元の配列の変更にかかわらず、作成時の組合せ・順序を保持します。空の指定ではすべての入力が未一致となります。 + * + * @param 入力の型 + * @param patterns 条件と処理の組。各組とその要素は非null + * @return 指定した条件と順序で入力を扱うConsumingPatterns + * @throws NullPointerException patterns自体がnullの場合 + */ + @SafeVarargs + public static ConsumingPatterns consumingPatterns(@NonNull Tuple2, Consumer>... patterns) { + return consumingPatterns(Stream.of(patterns).collect(toList())); + } + + /** + * 指定順を優先順位として、最初に一致する条件の処理を行う振り分けを作ります。 + * + *

元のリストの変更にかかわらず、作成時の組合せ・順序を保持します。空の指定ではすべての入力が未一致となります。 + * + * @param 入力の型 + * @param patterns 条件と処理の組。各組とその要素は非null + * @return 指定した条件と順序で入力を扱うConsumingPatterns + * @throws NullPointerException patterns自体がnullの場合 + */ + public static ConsumingPatterns consumingPatterns(@NonNull List, Consumer>> patterns) { + return new ConsumingPatterns<>(patterns); + } + + /** + * 入力に最初に一致する条件の処理を実行します。 + * + * @param key 入力 + * @throws NoSuchPatternException 一致する条件がない場合 + */ + public void handle(K key) { + this.getConsumer(key) + .orElseThrow(() -> new NoSuchPatternException("for key: " + key + ". To allow this pattern to accept value that match no defined pattern, consider setting default consumer.")) + .accept(key); + } + + @NonNull + private Optional> getConsumer(K key) { + return this.mappings.stream() + .filter(entry -> entry._1().test(key)) + .map(Tuple2::_2) + .findFirst(); + } + + /** + * 一致する条件がない入力に、既定の処理を行うConsumerを返します。 + * + * @param defaultConsumer 未一致の入力に対する処理 + * @return 一致した条件の処理、または既定の処理を行うConsumer + * @throws NullPointerException defaultConsumerがnullの場合 + */ + public Consumer orElseDo(@NonNull Consumer defaultConsumer) { + return k -> this.getConsumer(k).orElse(defaultConsumer).accept(k); + } + + /** + * 一致する条件がない入力に、指定した例外を送出するConsumerを返します。 + * + * @param exceptionSupplier 未一致の場合に送出する例外を提供するSupplier + * @return 一致した処理を実行し、未一致なら指定した例外を送出するConsumer + * @throws NullPointerException exceptionSupplierがnullの場合 + */ + public Consumer orElseThrow(@NonNull Supplier exceptionSupplier) { + return k -> this.getConsumer(k).orElseThrow(exceptionSupplier).accept(k); + } + + /** + * 入力に最初に一致する条件の処理を実行します。 + * + * @param k 入力 + * @throws NoSuchPatternException 一致する条件がない場合 + */ + @Override + public void accept(K k) { + this.handle(k); + } + + /** + * 入力に指定した処理を行うConsumerを返します。 + * + * @param 入力の型 + * @param consumer 入力に対する処理 + * @return 指定した処理を行うConsumer + * @throws NullPointerException consumerがnullの場合 + */ + public static Consumer thenAcceptWith(@NonNull Consumer consumer) { + return consumer::accept; + } + + /** + * 条件と、一致した入力に対する処理をひとつの規則として返します。 + * + * @param 入力の型 + * @param when この規則に該当する入力の条件 + * @param consumer 一致した入力に適用する処理 + * @return 第1要素が条件、第2要素が処理のTuple2 + * @throws NullPointerException 条件または処理がnullの場合 + */ + public static Tuple2, Consumer> when(@NonNull Predicate when, @NonNull Consumer consumer) { + return tuple(when::test, consumer::accept); + } + + /** + * 入力が比較対象と等しいかを表す述語を返します。 + * + * @param 入力の型 + * @param target 比較対象。非null + * @return Objects.equalsで入力とtargetが等しい場合にtrueとなる述語 + * @throws NullPointerException targetがnullの場合 + */ + public static Predicate equalsTo(@NonNull S target) { + return s -> Objects.equals(s, target); + } + + /** + * すべての入力に一致する既定の規則を返します。 + * + *

先行する条件がすべて未一致だった入力を扱うには、最後の規則として指定します。 + * + * @param 入力の型 + * @param thenAcceptWith 既定の処理 + * @return 常に一致する条件と処理の組 + * @throws NullPointerException thenAcceptWithがnullの場合 + */ + public static Tuple2, Consumer> orElse(@NonNull Consumer thenAcceptWith) { + return tuple(anything -> true, thenAcceptWith::accept); + } + + /** + * すべての入力に一致し、その入力から作った例外を送出する規則を返します。 + * + *

最後の規則として指定すると、先行する条件がすべて未一致だった入力を例外として報告できます。 + * + * @param 入力の型 + * @param thenAcceptWith 入力に応じた例外を作る関数 + * @return 常に一致する条件と例外を送出する処理の組 + * @throws NullPointerException thenAcceptWithがnullの場合 + */ + public static Tuple2, Consumer> orElseThrow(@NonNull Function thenAcceptWith) { + return tuple(anything -> true, input -> { + throw thenAcceptWith.apply(input); + }); + } + + /** + * 指定した型のインスタンスに一致する規則を返します。 + * + *

入力がnullの場合は未一致となります。 + * + * @param 入力の型 + * @param 条件が対象とする型 + * @param clazz 一致する型。サブクラス・実装クラスのインスタンスも対象 + * @param thenAcceptWith 型Tの入力に適用する処理 + * @return 型の条件と処理の組 + * @throws NullPointerException clazzまたはthenAcceptWithがnullの場合 + */ + public static Tuple2, Consumer> whenMatch(@NonNull Class clazz, @NonNull Consumer thenAcceptWith) { + return tuple(clazz::isInstance, instance -> thenAcceptWith.accept(clazz.cast(instance))); + } + + /** + * 指定した型のインスタンスで、追加の条件も満たす入力に一致する規則を返します。 + * + *

入力がnullの場合は未一致となります。追加の条件は型が一致した入力だけを受け取ります。 + * + * @param 入力の型 + * @param 条件が対象とする型 + * @param clazz 一致する型。サブクラス・実装クラスのインスタンスも対象 + * @param when 型が一致した入力に求める追加の条件 + * @param thenAcceptWith 型Tの入力に適用する処理 + * @return 型の条件と処理の組 + * @throws NullPointerException clazzまたはthenAcceptWithがnullの場合 + */ + public static Tuple2, Consumer> whenMatch(@NonNull Class clazz, Predicate when, @NonNull Consumer thenAcceptWith) { + return tuple(key -> clazz.isInstance(key) && when.test(clazz.cast(key)), instance -> thenAcceptWith.accept(clazz.cast(instance))); + } + +} diff --git a/src/main/java/io/github/lambig/patterns/NoSuchPatternException.java b/src/main/java/io/github/lambig/patterns/NoSuchPatternException.java index 3b6e27b..6f6d0f2 100644 --- a/src/main/java/io/github/lambig/patterns/NoSuchPatternException.java +++ b/src/main/java/io/github/lambig/patterns/NoSuchPatternException.java @@ -1,7 +1,38 @@ -package io.github.lambig.patterns; - -import lombok.experimental.StandardException; - -@StandardException -public class NoSuchPatternException extends RuntimeException { -} +package io.github.lambig.patterns; + +/** + * 入力に一致する条件がなく、結果または処理を選べなかったことを表す例外。 + * {@link Patterns#get(Object)}と{@link ConsumingPatterns#handle(Object)}から送出されます。 + */ +public class NoSuchPatternException extends RuntimeException { + /** 詳細メッセージと原因が空の例外を作ります。 */ + public NoSuchPatternException() { + this(null, null); + } + + /** + * 詳細メッセージを持つ例外を作ります。 + * @param message 入力や該当条件に関する説明。nullも指定可能 + */ + public NoSuchPatternException(String message) { + this(message, null); + } + + /** + * 原因と、その詳細メッセージを持つ例外を作ります。 + * @param cause 原因。nullの場合は詳細メッセージもnull + */ + public NoSuchPatternException(Throwable cause) { + this(cause == null ? null : cause.getMessage(), cause); + } + + /** + * 指定した詳細メッセージと原因を持つ例外を作ります。 + * @param message 詳細メッセージ。nullも指定可能 + * @param cause 原因。nullも指定可能 + */ + public NoSuchPatternException(String message, Throwable cause) { + super(message); + if (cause != null) initCause(cause); + } +} diff --git a/src/main/java/io/github/lambig/patterns/Patterns.java b/src/main/java/io/github/lambig/patterns/Patterns.java index 5fad06f..0490119 100644 --- a/src/main/java/io/github/lambig/patterns/Patterns.java +++ b/src/main/java/io/github/lambig/patterns/Patterns.java @@ -11,33 +11,50 @@ import java.util.function.Supplier; import java.util.stream.Stream; import lombok.NonNull; -import lombok.experimental.StandardException; import static io.github.lambig.tuplite._2.Tuple2._2mapWith; import static io.github.lambig.tuplite._2.Tuple2.tuple; import static java.util.stream.Collectors.toList; /** - * 疑似パターンマッチ定義クラス - * パターンは定義順に評価します。 + * 入力に最初に一致する条件の結果を返すFunction。 * - * @param キー型 - * @param 処理結果の戻り型 + *

条件の並び順が優先順位です。分類名や料金など、入力に応じたひとつの結果を求める場合に使えます。 + * {@link #get(Object)}は値が得られる入力に、{@link #getOptionally(Object)}は + * 一致する条件や値がない入力も含めて扱う場合に使います。 + * + *

作成後に元のリストや配列の要素を入れ替えても、条件と関数の組合せ・順序は保たれます。 + * 条件・関数が参照する状態は共有され、入力がnullの場合の結果もそれらの定義によります。 + * 条件や関数の例外は呼出元へ伝わり、その入力に対する処理はそこで終了します。 + * + * @param 入力の型 + * @param 結果の型 */ public class Patterns implements Function { private final List, Function>> mappings; + /** + * 指定順を優先順位として、最初に一致する条件の結果を返す関数を作ります。 + * + *

元のリストの変更にかかわらず、作成時の組合せ・順序を保持します。空の指定ではすべての入力が未一致となります。 + * + * @param patterns 条件と結果を求める関数の組。各組とその要素は非null + * @throws NullPointerException patterns自体がnullの場合 + */ public Patterns(List, Function>> patterns) { - this.mappings = patterns; + this.mappings = new ArrayList<>(patterns); } /** - * パターンを宣言します。 + * 指定順を優先順位として、最初に一致する条件の結果を返す関数を作ります。 + * + *

元の配列の変更にかかわらず、作成時の組合せ・順序を保持します。空の指定ではすべての入力が未一致となります。 * - * @param patterns 宣言する各パターン - * @param 入力型 - * @param 出力型 - * @return パターンによるマッピング + * @param 入力の型 + * @param 結果の型 + * @param patterns 条件と結果を求める関数の組。各組とその要素は非null + * @return 指定した条件と順序で入力を扱うPatterns + * @throws NullPointerException patterns自体がnullの場合 */ @SafeVarargs public static Patterns of(Tuple2, Function>... patterns) { @@ -45,24 +62,30 @@ public static Patterns of(Tuple2, Function>... p } /** - * パターンを宣言します。 + * 指定順を優先順位として、最初に一致する条件の結果を返す関数を作ります。 * - * @param patterns 宣言する各パターン - * @param 入力型 - * @param 出力型 - * @return パターンによるマッピング + *

元のリストの変更にかかわらず、作成時の組合せ・順序を保持します。空の指定ではすべての入力が未一致となります。 + * + * @param 入力の型 + * @param 結果の型 + * @param patterns 条件と結果を求める関数の組。各組とその要素は非null + * @return 指定した条件と順序で入力を扱うPatterns + * @throws NullPointerException patterns自体がnullの場合 */ public static Patterns of(List, Function>> patterns) { return patterns(patterns); } /** - * パターンを宣言します。 + * 指定順を優先順位として、最初に一致する条件の結果を返す関数を作ります。 + * + *

元の配列の変更にかかわらず、作成時の組合せ・順序を保持します。空の指定ではすべての入力が未一致となります。 * - * @param patterns 宣言する各パターン - * @param 入力型 - * @param 出力型 - * @return パターンによるマッピング + * @param 入力の型 + * @param 結果の型 + * @param patterns 条件と結果を求める関数の組。各組とその要素は非null + * @return 指定した条件と順序で入力を扱うPatterns + * @throws NullPointerException patterns自体がnullの場合 */ @SafeVarargs public static Patterns patterns(Tuple2, Function>... patterns) { @@ -70,24 +93,27 @@ public static Patterns patterns(Tuple2, Function } /** - * パターンを宣言します。 + * 指定順を優先順位として、最初に一致する条件の結果を返す関数を作ります。 + * + *

元のリストの変更にかかわらず、作成時の組合せ・順序を保持します。空の指定ではすべての入力が未一致となります。 * - * @param patterns 宣言する各パターン - * @param 入力型 - * @param 出力型 - * @return パターンによるマッピング + * @param 入力の型 + * @param 結果の型 + * @param patterns 条件と結果を求める関数の組。各組とその要素は非null + * @return 指定した条件と順序で入力を扱うPatterns + * @throws NullPointerException patterns自体がnullの場合 */ public static Patterns patterns(List, Function>> patterns) { - return new Patterns<>(new ArrayList<>(patterns)); + return new Patterns<>(patterns); } /** - * キーが最初に該当するパターンの返却値を取得します + * 入力に最初に一致する条件の結果を返します。 * - * @param key キー - * @return 該当するパターンがあればその返却値、なければnull - * @throws NoSuchPatternException 該当するパターンがない場合 - * @throws NullPointerException 該当するパターンがnullを返却した場合 + * @param key 入力 + * @return 一致した条件の非nullの結果 + * @throws NoSuchPatternException 一致する条件がない場合 + * @throws NullPointerException 一致した条件の関数がnullを返した場合 */ @NonNull public V get(K key) { @@ -101,12 +127,12 @@ public V get(K key) { } /** - * キーが最初に該当するパターンの返却値のをOptionalを取得します。 - * 該当するパターンがない場合やパターンの返却値がnullの場合、 - * Optionalは空となります。 + * 入力に最初に一致する条件の結果をOptionalで返します。 * - * @param key キー - * @return 該当するパターンがあればその返却値のOptional + *

最初に一致した条件の結果がnullの場合も、その入力の結果は空となります。後続の条件は対象になりません。 + * + * @param key 入力 + * @return 結果を含むOptional。条件の未一致または結果がnullなら空のOptional */ @NonNull public Optional getOptionally(K key) { @@ -117,135 +143,149 @@ public Optional getOptionally(K key) { } /** - * キーを、値の代わりに「値のOptional」にmapするFunctionを返します。 + * 入力に対する結果の有無をOptionalで返す関数を返します。 + * + *

最初に一致した条件の結果がnullの場合も、その入力の結果は空となります。後続の条件は対象になりません。 * - * @return このPatternでOptionallyにmapするFunction + * @return 結果をOptionalに包む関数。条件の未一致または結果がnullなら空のOptional */ public Function> optional() { return this::getOptionally; } - /** - * キーを値またはデフォルト値にmapするFunctionを返します。 + * 条件の未一致または結果がnullの入力に、既定値を返す関数を返します。 * - * @param defaultValue デフォルト値 - * @return このPatternでデフォルト値付きでmapするFunction + * @param defaultValue 結果が空の場合の値。nullも指定可能 + * @return 結果があればその値、空なら既定値を返す関数 */ public Function orElse(V defaultValue) { return this.optional().andThen(optional -> optional.orElse(defaultValue)); } /** - * キーを値またはデフォルト値にmapするFunctionを返します。 + * 条件の未一致または結果がnullの入力に、Supplierから得た値を返す関数を返します。 * - * @param defaultValueSupplier デフォルト値のSupplier - * @return このPatternでデフォルト値付きでmapするFunction + *

Supplierは結果が空の入力ごとに呼ばれ、その結果がnullの場合はnullを返します。 + * + * @param defaultValueSupplier 結果が空の場合に値を提供するSupplier + * @return 結果があればその値、空ならSupplierの結果を返す関数 */ public Function orElseGet(Supplier defaultValueSupplier) { return this.optional().andThen(optional -> optional.orElseGet(defaultValueSupplier)); } /** - * キーを値にmapし、キーが未設定の場合例外を送出するFunctionを返します。 + * 条件の未一致または結果がnullの入力に、指定した例外を送出する関数を返します。 * - * @param exceptionSupplier 指定例外のSupplier - * @return このPatternでmapするFunction Functionはキーが未設定の場合指定例外を送出する + * @param exceptionSupplier 結果が空の場合に送出する例外を提供するSupplier + * @return 結果を返し、空なら指定した例外を送出する関数 */ public Function orElseThrow(Supplier exceptionSupplier) { return this.optional().andThen(optional -> optional.orElseThrow(exceptionSupplier)); } + /** + * 入力に最初に一致する条件の結果を返します。 + * + * @param k 入力 + * @return 一致した条件の非nullの結果 + * @throws NoSuchPatternException 一致する条件がない場合 + * @throws NullPointerException 一致した条件の関数がnullを返した場合 + */ @Override public V apply(K k) { return this.get(k); } /** - * キーがwhenを満たした場合に返却する値を指定することで - * その値を返却する関数を返却します。 - * 関数でなく返却値を直接設定できます。 - * - * @param value 返却する値 - * @param キー型 - * @param 値型 - * @return パターンに設定する返却関数(固定値返却) + * 指定した値を結果とする関数を返します。 + * + * @param 入力の型 + * @param 結果の型 + * @param value 結果の値。nullも指定可能 + * @return どの入力にもvalueを返す関数 */ public static Function then(O value) { return anything -> value; } /** - * キーがwhenを満たした場合に値を返却するsupplierを指定することで - * 入力値を無視してその値を返却する関数を返却します。 + * Supplierが提供する値を結果とする関数を返します。 + * + *

whenの結果に指定すると、その条件が選ばれるたびにSupplierが呼ばれます。 * - * @param supplier 返却する値のsupplier - * @param キー型 - * @param 値型 - * @return パターンに設定する返却関数 + * @param 入力の型 + * @param 結果の型 + * @param supplier 結果を提供するSupplier + * @return 入力ごとにSupplierから結果を得る関数 */ public static Function thenSupply(Supplier supplier) { return anything -> supplier.get(); } /** - * キーがwhenを満たした場合にキーに適用して返却する関数を明示します。 - * (実はthenApplyを書かなくても直接whenの第2引数に関数を指定すれば動きます + * 入力を指定した関数で変換した結果を返す関数を返します。 * - * @param function 適用する関数 - * @param キー型 - * @param 関数の戻り型 - * @return パターンに設定する返却関数 + * @param 入力の型 + * @param 結果の型 + * @param function 入力から結果を求める関数 + * @return functionと同じ結果を返す関数 + * @throws NullPointerException functionがnullの場合 */ public static Function thenApply(Function function) { return function::apply; } /** - * パターンを定義します。 + * 条件と、一致した入力から結果を求める関数をひとつの規則として返します。 * - * @param when キーがこのパターンに該当する条件 - * @param thenApply キーがこのパターンに該当する場合、キーに適用する関数 - * @param キー型 - * @param 関数の戻り型 - * @return パターン + * @param 入力の型 + * @param 結果の型 + * @param when この規則に該当する入力の条件 + * @param thenApply 一致した入力に適用する関数 + * @return 第1要素が条件、第2要素が関数のTuple2 + * @throws NullPointerException 条件または関数がnullの場合 */ public static Tuple2, Function> when(Predicate when, Function thenApply) { return tuple(when::test, thenApply::apply); } /** - * 引数と等価であることをwhenと定義します。 + * 入力が比較対象と等しいかを表す述語を返します。 * - * @param target キーとの比較対象 - * @param キー型 - * @return 等価判定 + * @param 入力の型 + * @param target 比較対象。nullも指定可能 + * @return Objects.equalsで入力とtargetが等しい場合にtrueとなる述語 */ public static Predicate equalsTo(S target) { return s -> Objects.equals(s, target); } /** - * あらゆるキーに該当するパターンを返却します。 - * デフォルト値の設定に有用です。 + * すべての入力に一致する既定の規則を返します。 * - * @param thenApply キーがこのパターンに該当する場合、キーに適用する関数 - * @param キー型 - * @param 関数の戻り型 - * @return デフォルトパターン + *

先行する条件がすべて未一致だった入力を扱うには、最後の規則として指定します。 + * + * @param 入力の型 + * @param 結果の型 + * @param thenApply 既定の結果を求める関数 + * @return 常に一致する条件と関数の組 + * @throws NullPointerException thenApplyがnullの場合 */ public static Tuple2, Function> orElse(Function thenApply) { return tuple(anything -> true, thenApply::apply); } /** - * あらゆるキーに対し例外を送出させます。 - * 想定外パターンの対応に有用です。 + * すべての入力に一致し、その入力から作った例外を送出する規則を返します。 + * + *

最後の規則として指定すると、先行する条件がすべて未一致だった入力を例外として報告できます。 * - * @param thenApply キーがこのパターンに該当する場合、キーに適用して例外を生成する関数 - * @param キー型 - * @param 関数の戻り型 - * @return 例外処理送出処理 + * @param 入力の型 + * @param 結果の型 + * @param thenApply 入力に応じた例外を作る関数 + * @return 常に一致する条件と例外を送出する処理の組 */ public static Tuple2, Function> orElseThrow(Function thenApply) { return tuple(anything -> true, input -> { @@ -254,31 +294,37 @@ public static Tuple2, Function> orElseThrow(Function入力がnullの場合は未一致となります。 * - * @param clazz キーがこのパターンに該当する条件となるクラス - * @param thenApply キーがこのパターンに該当する場合、clazzクラスにキャストしたキーに適用する関数 - * @param キー型 - * @param 関数の戻り型 - * @return パターン + * @param 入力の型 + * @param 条件が対象とする型 + * @param 結果の型 + * @param clazz 一致する型。サブクラス・実装クラスのインスタンスも対象 + * @param thenApply 型Tの入力に適用する関数 + * @return 型の条件と関数の組 + * @throws NullPointerException clazzがnullの場合 */ public static Tuple2, Function> whenMatch(Class clazz, Function thenApply) { return tuple(clazz::isInstance, instance -> thenApply.apply(clazz.cast(instance))); } /** - * クラスによるパターンマッチを定義します。当該クラスのインスタンスとしたうえで満たすべき述語を定義できます。 - * - * @param clazz キーがこのパターンに該当する条件となるクラス - * @param when 上記クラスかつ満たすべき条件 - * @param thenApply キーがこのパターンに該当する場合、clazzクラスにキャストしたキーに適用する関数 - * @param キー型 - * @param 関数の戻り型 - * @return パターン + * 指定した型のインスタンスで、追加の条件も満たす入力に一致する規則を返します。 + * + *

入力がnullの場合は未一致となります。追加の条件は型が一致した入力だけを受け取ります。 + * + * @param 入力の型 + * @param 条件が対象とする型 + * @param 結果の型 + * @param clazz 一致する型。サブクラス・実装クラスのインスタンスも対象 + * @param when 型が一致した入力に求める追加の条件 + * @param thenApply 型Tの入力に適用する関数 + * @return 型の条件と関数の組 */ public static Tuple2, Function> whenMatch(Class clazz, Predicate when, Function thenApply) { return tuple(key -> clazz.isInstance(key) && when.test(clazz.cast(key)), instance -> thenApply.apply(clazz.cast(instance))); } } - diff --git a/src/test/java/io/github/lambig/patterns/PublicContractTest.java b/src/test/java/io/github/lambig/patterns/PublicContractTest.java new file mode 100644 index 0000000..a9760a0 --- /dev/null +++ b/src/test/java/io/github/lambig/patterns/PublicContractTest.java @@ -0,0 +1,177 @@ +package io.github.lambig.patterns; + +import io.github.lambig.tuplite._2.Tuple2; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import org.junit.jupiter.api.Test; +import static io.github.lambig.patterns.Patterns.then; +import static io.github.lambig.patterns.Patterns.when; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.fail; + +class PublicContractTest { + @Test + void constructorAndListFactoriesKeepOriginalRules() { + List, Function>> rules = new ArrayList<>( + List.of(when(n -> n > 0, then("original")))); + List> classifiers = List.of(new Patterns<>(rules), Patterns.of(rules), Patterns.patterns(rules)); + rules.set(0, when(n -> true, then("changed"))); + rules.clear(); + classifiers.forEach(p -> assertThat(p.get(1)).isEqualTo("original")); + } + + @Test + void consumingListFactoriesKeepOriginalRules() { + List events = new ArrayList<>(); + List, Consumer>> rules = new ArrayList<>(List.of( + ConsumingPatterns.when(n -> n > 0, n -> events.add("original")))); + List> handlers = List.of(ConsumingPatterns.of(rules), ConsumingPatterns.consumingPatterns(rules)); + rules.set(0, ConsumingPatterns.when(n -> true, n -> events.add("changed"))); + rules.clear(); + handlers.forEach(p -> p.accept(1)); + assertThat(events).containsExactly("original", "original"); + } + + @Test + @SuppressWarnings({"unchecked", "rawtypes"}) + void arrayFactoriesKeepOriginalRules() { + Tuple2, Function>[] rules = new Tuple2[]{when(n -> true, then("original"))}; + Patterns classifier = Patterns.of(rules); + rules[0] = when(n -> true, then("changed")); + assertThat(classifier.get(1)).isEqualTo("original"); + List events = new ArrayList<>(); + Tuple2, Consumer>[] actions = new Tuple2[]{ConsumingPatterns.when(n -> true, n -> events.add("original"))}; + ConsumingPatterns handler = ConsumingPatterns.of(actions); + actions[0] = ConsumingPatterns.when(n -> true, n -> events.add("changed")); + handler.accept(1); + assertThat(events).containsExactly("original"); + } + + @Test + void matchingNullResultEndsSelectionAndUsesOptionalFallback() { + Patterns p = Patterns.of(when(n -> true, then(null)), + when(n -> { fail("later predicate"); return true; }, then("later"))); + assertThatThrownBy(() -> p.get(1)).isInstanceOf(NullPointerException.class); + assertThat(p.getOptionally(1)).isEmpty(); + assertThat(p.optional().apply(1)).isEmpty(); + assertThat(p.orElse("fallback").apply(1)).isEqualTo("fallback"); + assertThat(p.orElseGet(() -> "supplied").apply(1)).isEqualTo("supplied"); + RuntimeException expected = new IllegalArgumentException("empty result"); + assertThatThrownBy(() -> p.orElseThrow(() -> expected).apply(1)).isSameAs(expected); + } + + @Test + void onlyFirstMatchingConsumerRuns() { + List events = new ArrayList<>(); + ConsumingPatterns p = ConsumingPatterns.of(ConsumingPatterns.when(n -> true, events::add), + ConsumingPatterns.when(n -> { fail("later predicate"); return true; }, n -> fail("later consumer"))); + p.handle(2); + p.orElseDo(n -> fail("default consumer")).accept(3); + assertThat(events).containsExactly(2, 3); + } + + @Test + void emptyRulesHaveTheSameUnmatchedBehaviorAsRejectedConditions() { + Patterns p = Patterns.of(); + assertThatThrownBy(() -> p.apply(0)).isInstanceOf(NoSuchPatternException.class); + assertThat(p.getOptionally(0)).isEmpty(); + assertThat(p.orElse((String) null).apply(0)).isNull(); + ConsumingPatterns c = ConsumingPatterns.of(); + assertThatThrownBy(() -> c.accept(0)).isInstanceOf(NoSuchPatternException.class); + AtomicInteger value = new AtomicInteger(); + c.orElseDo(value::set).accept(7); + assertThat(value).hasValue(7); + } + + @Test + void suppliersAreCalledOnlyForSelectedResultsOrEmptyFallbacks() { + AtomicInteger results = new AtomicInteger(); + AtomicInteger fallbacks = new AtomicInteger(); + Patterns p = Patterns.of(when(n -> n > 0, Patterns.thenSupply(results::incrementAndGet))); + Function f = p.orElseGet(fallbacks::incrementAndGet); + assertThat(f.apply(1)).isEqualTo(1); + assertThat(f.apply(1)).isEqualTo(2); + assertThat(fallbacks).hasValue(0); + assertThat(f.apply(0)).isEqualTo(1); + assertThat(results).hasValue(2); + } + + @Test + void nullInputsCanBeMatchedExplicitlyAndAreUnmatchedByTypeRules() { + assertThat(Patterns.equalsTo(null).test(null)).isTrue(); + assertThat(Patterns.of(when(Patterns.equalsTo(null), then("null"))).get(null)).isEqualTo("null"); + assertThatThrownBy(() -> ConsumingPatterns.equalsTo(null)).isInstanceOf(NullPointerException.class); + List events = new ArrayList<>(); + ConsumingPatterns.of(ConsumingPatterns.when(java.util.Objects::isNull, n -> events.add("null"))).accept(null); + assertThat(events).containsExactly("null"); + Patterns p = Patterns.of(Patterns.whenMatch(Number.class, n -> n.intValue() > 0, Object::toString)); + assertThat(p.get(2)).isEqualTo("2"); + assertThat(p.getOptionally(null)).isEmpty(); + assertThat(p.getOptionally("2")).isEmpty(); + assertThat(p.getOptionally(-1)).isEmpty(); + } + + @Test + void typeGuardReceivesOnlyInstancesOfTheTargetType() { + List events = new ArrayList<>(); + ConsumingPatterns c = ConsumingPatterns.of(ConsumingPatterns.whenMatch(Integer.class, + n -> n > 0, events::add)); + c.orElseDo(n -> {}).accept(null); + c.orElseDo(n -> {}).accept("2"); + c.orElseDo(n -> {}).accept(-1); + c.accept(2); + assertThat(events).containsExactly(2); + } + + @Test + void exceptionsFromConditionsAndFunctionsRemainTheOriginalException() { + RuntimeException expected = new IllegalStateException("condition"); + Patterns p = Patterns.of(when(n -> { throw expected; }, then("unused"))); + assertThatThrownBy(() -> p.orElse("fallback").apply(1)).isSameAs(expected); + Patterns f = Patterns.of(when(n -> true, n -> { throw expected; })); + assertThatThrownBy(() -> f.getOptionally(1)).isSameAs(expected); + ConsumingPatterns c = ConsumingPatterns.of(ConsumingPatterns.when(n -> { throw expected; }, n -> fail())); + assertThatThrownBy(() -> c.orElseDo(n -> fail()).accept(1)).isSameAs(expected); + } + + @Test + void consumerUpdatesBeforeAnExceptionRemainVisible() { + List events = new ArrayList<>(); + RuntimeException expected = new IllegalStateException("consumer"); + ConsumingPatterns p = ConsumingPatterns.of(ConsumingPatterns.when(n -> true, + n -> { events.add(n); throw expected; })); + assertThatThrownBy(() -> p.orElseDo(n -> fail()).accept(2)).isSameAs(expected); + assertThat(events).containsExactly(2); + } + + @Test + void ruleStateRemainsSharedWhileTheListIsIndependent() { + AtomicInteger threshold = new AtomicInteger(10); + Patterns p = Patterns.of(when(n -> n >= threshold.get(), then("accepted"))); + assertThat(p.getOptionally(5)).isEmpty(); + threshold.set(3); + assertThat(p.get(5)).isEqualTo("accepted"); + } + + @Test + void nullRuleContainersFailAtConstruction() { + assertThatThrownBy(() -> new Patterns(null)).isInstanceOf(NullPointerException.class); + assertThatThrownBy(() -> Patterns.of((List, Function>>) null)).isInstanceOf(NullPointerException.class); + assertThatThrownBy(() -> ConsumingPatterns.of((List, Consumer>>) null)).isInstanceOf(NullPointerException.class); + } + + @Test + void exceptionConstructorsPreserveMessagesAndCauses() { + RuntimeException cause = new IllegalStateException("reason"); + assertThat(new NoSuchPatternException()).hasMessage(null).hasCause(null); + assertThat(new NoSuchPatternException("detail")).hasMessage("detail").hasCause(null); + assertThat(new NoSuchPatternException(cause)).hasMessage("reason").hasCause(cause); + assertThat(new NoSuchPatternException("detail", cause)).hasMessage("detail").hasCause(cause); + assertThat(new NoSuchPatternException((Throwable) null)).hasMessage(null).hasCause(null); + } +} diff --git a/verify-consumers.sh b/verify-consumers.sh new file mode 100644 index 0000000..f5657c2 --- /dev/null +++ b/verify-consumers.sh @@ -0,0 +1,35 @@ +#!/bin/sh +set -eu +root=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +cd "$root" +mode=${1:-candidate} +version=${2:-1.3.2} +case "$mode" in candidate|public) ;; *) echo 'Usage: verify-consumers.sh [candidate|public] [version]' >&2; exit 2;; esac +results="$root/build/consumer-results/$mode" +mkdir -p "$results" +java_runtime="${TEST_JAVA_HOME:-$JAVA_HOME}/bin/java" +repository="$root/build/repository" +if [ "$mode" = candidate ]; then + sh ./gradlew --no-daemon --console=plain check publishMavenJavaPublicationToCandidateRepository > "$results/library.log" 2>&1 + python3 .github/scripts/check-documentation.py > "$results/documentation.log" + python3 .github/scripts/check-compatibility.py "$repository/io/github/lambig/Patterns/$version/Patterns-$version.jar" > "$results/compatibility.log" 2>&1 +fi +cache=$(mktemp -d) +trap 'rm -rf "$cache"' EXIT HUP INT TERM +if [ "$mode" = candidate ]; then set -- "-PcandidateRepository=$repository"; else set --; fi +GRADLE_USER_HOME="$cache/gradle" sh ./gradlew -p example --no-daemon --console=plain \ + "-PlibraryVersion=$version" "$@" clean runExample > "$results/gradle.log" 2>&1 +if [ "$mode" = candidate ]; then set -- "-DcandidateRepository=file://$repository"; else set --; fi +mvn -B -ntp -f example/pom.xml "-Dmaven.repo.local=$cache/maven" "-DlibraryVersion=$version" "$@" \ + clean compile dependency:build-classpath -Dmdep.outputFile=target/runtime-classpath.txt > "$results/maven.log" 2>&1 +classpath=$(cat example/target/runtime-classpath.txt) +python3 - "$classpath" "$version" <<'PY' +import os, sys +from pathlib import Path +assert sorted(Path(p).name for p in sys.argv[1].split(os.pathsep)) == sorted([ + 'Patterns-' + sys.argv[2] + '.jar', 'Tuplite-1.0.1.jar', 'functional-interface-extension-1.1.3.jar']) +PY +"$java_runtime" -cp "example/target/classes:$classpath" example.PatternExample >> "$results/maven.log" 2>&1 +python3 .github/scripts/check-readme-example.py "$classpath" > "$results/readme.log" 2>&1 +grep -F 'classification=large; fallback=unknown; events=[large:12, other:0]; tuple-api=OK' "$results/gradle.log" "$results/maven.log" +echo "Consumer verification passed: $mode $version"