diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 0000000..ae8ec14 --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,99 @@ +# CLAUDE.md — apythia + +## Project Overview + +Published Kotlin Multiplatform testing library for HTTP API mocking and assertions. +The `HttpApythia` DSL lets consumers mock HTTP responses and assert outgoing requests +in a serialization-agnostic, client-agnostic way (with first-class Ktor + OkHttp adapters). + +- Root package: `io.github.ackeecz.apythia` +- Maven coordinates: `io.github.ackeecz:apythia-*` (see `lib.properties`) +- Build tooling: convention plugins in `build-logic/`, Gradle Version Catalogs +- **Public API is the deliverable** — every `.api` dump under `/api/` is part of the contract + +## Module Structure + +``` +:bom — BOM artifact (apythia-bom); pins compatible versions +:http — Core: HttpApythia abstract class + DSL; serialization-agnostic +:http-ext-json-kotlinx-serialization — Optional JSON DSL extensions backed by Kotlinx Serialization +:http-ktor — Ktor-backed HttpApythia impl +:http-okhttp — OkHttp-backed HttpApythia impl +:http-testing — Shared test infra: BaseHttpApythiaImplTest, HttpApythiaMock, factories +:sample-app — Internal sample showing library usage; not published +``` + +## KMP Targets & Compiler Configuration + +Targets (per `KmpLibraryPlugin`): Android, JVM, iosX64, iosArm64, iosSimulatorArm64. + +Compiler flags applied project-wide: + +- `allWarningsAsErrors = true` — even a deprecation warning fails the build. +- `explicitApi()` — every public declaration needs an explicit visibility modifier. +- `-Xconsistent-data-class-copy-visibility` — `internal` data classes get `internal` `copy()`. +- Auto opt-in: `io.github.ackeecz.apythia.http.ExperimentalHttpApi` is added to `optIn`, so apythia's own code never needs `@OptIn(ExperimentalHttpApi::class)`. External users still must opt in. + +## ABI Validation + +Built-in Kotlin ABI validation (not the BCV plugin) is enabled by default on every KMP library module. +Public API dumps live at `/api/` and are committed. Any signature change to a `public` declaration must be reflected there. + +Workflow when public API changes: + +1. `./gradlew updateLegacyAbi` to regenerate dumps. +2. Commit the dumps with the code change. + +If the intent is a public API change, treat the `.api` diff as part of the review. If the dump didn't move, the change wasn't public. + +## Convention Plugins (`build-logic/`) + +| Alias | Purpose | +|---|---| +| `apythia.kotlin.multiplatform.library` | KMP library (Android + iOS + JVM), explicit API, ABI validation, Detekt | +| `apythia.kotlin.multiplatform.library-with-testing` | Above + Kotest test deps | +| `apythia.kotlin.jvm.library` | JVM-only library | +| `apythia.kotlin.jvm.library-with-testing` | JVM-only library + Kotest | +| `apythia.android.application` | Android app (sample-app only) | +| `apythia.publishing` | Maven Central publishing + `verifyPublishing` / `checkIfUpdateNeededSinceCurrentTag` | +| `apythia.preflightchecks` | Registers `prePublishCheck` aggregating release-time checks | + +## Publishing + +Source of truth for the release procedure: `RELEASING.md`. Below is the mental model. + +### Versioning + +Every artifact has an independent version in `lib.properties` (`HTTP_VERSION`, `HTTP_KTOR_VERSION`, `BOM_VERSION`, …). The BOM pins a compatible set. Artifacts can release individually, but `verifyPublishing` forces co-release when an internal-only module had breaking changes — internal-API breaks can corrupt binary compat between artifacts that link against the same internal. + +### Release Gradle tasks + +| Task | Purpose | +|---|---| +| `checkIfUpdateNeededSinceCurrentTag` | List artifacts whose code changed since the last tag | +| `verifyPublishing` | Fail if internal-module breakage forces a co-release | +| `verifyBomVersion` | Fail if BOM version in `lib.properties` doesn't match the pushed git tag | +| `prePublishCheck` | Aggregated CI-equivalent check; run locally before pushing the tag | + +### Publishing skipping + +`PublishingPlugin` probes Maven Central before publishing: + +- **404** → publish. +- **2xx** → skip (artifact at this version already exists). +- **Anything else** → fail. + +Re-pushing the same tag is safe (publishes nothing). To re-publish, bump the version — never force. + +### Dokka + signing + +`com.vanniktech.maven.publish` + Dokka. `signAllPublications()` requires GPG secrets — provided by CI only. Local publishing fails signing; expected. + +## Code Style + +- Always put a blank line after a type body opening brace and before the first member. Applies to `class`, `interface`, `sealed class`, `object`, `enum class` — any construct that declares a type with a `{ }` body block. +- **Does NOT apply** to constructor parameter lists `(...)` or lambda/DSL bodies (e.g. `module { }`, `launch { }`, `test { }`). + +## Plans + +At the end of each plan, give me a list of unresolved questions to answer, if any. Make the questions extremely concise. Sacrifice grammar for the sake of concision. Use AskUserQuestionTool. diff --git a/.claude/rules/dsl.md b/.claude/rules/dsl.md new file mode 100644 index 0000000..fdac81b --- /dev/null +++ b/.claude/rules/dsl.md @@ -0,0 +1,97 @@ +--- +globs: + - "http/**" + - "http-ext-json-kotlinx-serialization/**" + - "http-ktor/**" + - "http-okhttp/**" + - "http-testing/**" +--- + +# DSL Conventions + +apythia is, mechanically, a builder DSL. Every public DSL surface follows these patterns. + +## DslMarker per scope + +Scope-isolating `@DslMarker` annotation per logical scope. `public`, `BINARY` retention. + +```kotlin +@DslMarker +@Retention(AnnotationRetention.BINARY) +public annotation class HttpResponseDslMarker +``` + +Existing markers: `HttpResponseDslMarker`, `HttpRequestDslMarker`, `ConfigsDslMarker`. +Apply on the **interface**, not the impl. + +## Public interface + internal impl split + +Every DSL surface has a `public interface` and an `internal class *Impl`. Consumers only see the interface. + +```kotlin +@HttpResponseDslMarker +public interface HttpResponseMockBuilder : DslExtensionConfigProvider { + + public fun statusCode(code: Int) + public fun headers(mockHeaders: HeadersMockBuilder.() -> Unit) +} + +internal class HttpResponseMockBuilderImpl( + private val dslExtensionConfigProvider: DslExtensionConfigProvider, +) : HttpResponseMockBuilder, DslExtensionConfigProvider by dslExtensionConfigProvider { + // ... +} +``` + +## DSL constraint enforcement + +Constraints on builder usage are enforced at runtime via dedicated checkers, not by clever types. + +- `CallCountChecker` — caps how many times a builder method may be called (e.g. `statusCode` once, `body` once). +- `MutualExclusivityChecker` — ensures only one of a set of mutually exclusive options is picked. + +```kotlin +private val statusCodeCallCountChecker = CallCountChecker("statusCode", maxCallCount = 1) +``` + +When adding a new builder method, decide explicitly: is it one-shot, repeatable, or mutually exclusive with siblings? Wire the appropriate checker. + +## `@ExperimentalHttpApi` gating + +Mark evolving / not-fully-settled public API with `@ExperimentalHttpApi`. The annotation is **auto-opted-in** for apythia source via `compilerOptions.optIn`, so internal code uses it freely. External users must opt in themselves. + +```kotlin +@RequiresOptIn( + message = "This API is experimental and may change in future releases.", + level = RequiresOptIn.Level.WARNING, +) +@Retention(AnnotationRetention.BINARY) +public annotation class ExperimentalHttpApi +``` + +Use it on: + +- DSL members whose shape may still change (e.g. `statusCode`, `bytesBody`, `plainTextBody`). +- Extension points (`DslExtensionConfig`). + +## DSL extension config mechanism + +Optional extensions (like `http-ext-json-kotlinx-serialization`) plug in via `DslExtensionConfig`: + +```kotlin +@ExperimentalHttpApi +public interface DslExtensionConfig + +public abstract class HttpApythia( + dslExtensionConfigs: DslExtensionConfigs.() -> Unit, +) +``` + +The config is then surfaced on scopes like `BodyAssertion` / `HttpResponseMockBuilder` via `DslExtensionConfigProvider`, so extension methods read their configuration without forcing callers to thread it through every call site. New DSL extensions in separate modules should follow `http-ext-json-kotlinx-serialization` as the reference. + +## Builders vs Assertions + +Response side is **mocking** → `*MockBuilder` types (`HttpResponseMockBuilder`, `HeadersMockBuilder`). +Request side is **verification** → `*Assertion` types (`HttpRequestAssertion`, `BodyAssertion`, `HeadersAssertion`, `UrlAssertion`). + +Don't mix the verbs — a builder *configures*, an assertion *checks*. diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md new file mode 100644 index 0000000..c8126f6 --- /dev/null +++ b/.claude/rules/testing.md @@ -0,0 +1,86 @@ +--- +globs: + - "**/src/**Test/**" + - "http-testing/**" +--- + +# Testing Conventions + +## Framework + +- **Kotest `FunSpec` only** — no other spec styles (BehaviorSpec, StringSpec, etc.). Verified across all 27 test files in the repo. +- Test style: `test("description") { ... }` blocks inside the spec constructor. + +## System Under Test + +- Always name the instance under test `underTest` — never `sut`, `apythia`, `client`, etc. +- For most tests: declare `underTest` + dependencies as `private lateinit var` at **file level** (not inside `init` or `beforeEach`); recreate them in `beforeEach`. + +```kotlin +private lateinit var httpApythia: HttpApythiaMock +private lateinit var underTest: HttpResponseMockBuilderImpl + +internal class HttpResponseMockBuilderImplTest : FunSpec({ + + beforeEach { + httpApythia = HttpApythiaMock() + underTest = HttpResponseMockBuilderImpl( + dslExtensionConfigProvider = httpApythia, + ) + } + + test("status code defaults to 200") { + underTest.httpResponse.statusCode shouldBe 200 + } +}) +``` + +## Reusable test suites + +Cross-impl invariants (core, Ktor, OkHttp, future impls) are tested by **shared suites declared as extension functions on `FunSpecContainerScope`**: + +```kotlin +internal suspend fun FunSpecContainerScope.bodyTestSuite( + fixture: HttpApythiaTest.Fixture, + arrangeHeaders: (Map>) -> Unit, + arrangeBody: (ByteArray) -> Unit, + assertBody: suspend HttpRequestAssertion.(BodyAssertion.() -> Unit) -> Unit, +) = with(fixture) { + context("body") { + actualBodyTests(fixture, arrangeHeaders, arrangeBody, assertBody) + emptyBodyTests(fixture, arrangeBody, assertBody) + // ... + } +} +``` + +Suites are composed from test classes via `context { bodyTestSuite(fixture, ...) }`. When adding behaviour that must hold for every `HttpApythia` impl, write it as a suite under `:http/commonTest/...` (or `:http-testing`), not as a one-off test in a single impl. + +## `BaseHttpApythiaImplTest` — the contract test + +`public abstract class BaseHttpApythiaImplTest : FunSpec()` in `:http-testing` is the conformance test every `HttpApythia` impl must pass. It's **public** because external developers integrating their own HTTP client run it against their own impl. + +Changes here are public API changes — regenerate the `:http-testing` `.api` dump and bump its version when releasing. + +## Test doubles + +- **Hand-written only** — no MockK, no Mockito. +- Suffix naming: + - `*Mock` — controllable double with state (e.g. `HttpApythiaMock`, `DslExtensionConfigMock`). + - `*Stub` — fixed-response stand-in. +- Shared doubles live in `:http-testing` (e.g. `HttpApythiaMock` is `public` and exposed to consumers). +- One-off doubles live next to the test that uses them. + +## Test class visibility + +- Test classes are `internal class FooTest` — keeps them out of the public API. +- Exception: `BaseHttpApythiaImplTest` and its enabler types (`RemoteDataSource`, factories) are `public` because they're consumer-facing. + +## KMP test source sets + +Tests are written per source set as appropriate: + +- `commonTest` — KMP-shared tests; rely on Kotest only, no platform engines. +- `androidUnitTest`, `jvmTest`, `iosX64Test`, `iosArm64Test`, `iosSimulatorArm64Test` — platform-specific. + +When a test in a KMP module requires a platform engine (e.g. an OkHttp `MockEngine` in `:http-okhttp`), keep it in the platform-specific source set instead of `commonTest`. diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..0d79a62 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,68 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "permissions": { + "allow": [ + "Bash(./gradlew *)", + "Bash(cat *)", + "Bash(cd *)", + "Bash(echo *)", + "Bash(find *)", + "Bash(git add *)", + "Bash(git commit *)", + "Bash(git checkout *)", + "Bash(git diff *)", + "Bash(git log *)", + "Bash(git pop *)", + "Bash(git push *)", + "Bash(git rebase *)", + "Bash(git rev-parse *)", + "Bash(git stash *)", + "Bash(git status *)", + "Bash(git worktree *)", + "Bash(grep *)", + "Bash(ls *)", + "Bash(mkdir *)", + "Bash(pwd *)" + ], + "deny": [ + "Bash(git push --force*)", + "Bash(git push * --force*)", + "Bash(git push --force-with-lease*)", + "Bash(git push * --force-with-lease*)", + "Bash(git branch -d*)", + "Bash(git branch -D*)", + "Bash(git branch * -d*)", + "Bash(git branch * -D*)", + "Bash(git push * --delete*)", + "Read(local.properties)", + "Edit(local.properties)", + "Write(local.properties)" + ] + }, + "enabledPlugins": { + "mob-and-detekt@ackee-mobile-ai-marketplace": true, + "mob-and-lint@ackee-mobile-ai-marketplace": true, + "mob-and-test@ackee-mobile-ai-marketplace": true, + "mob-and-update-deps@ackee-mobile-ai-marketplace": true, + "mob-and-verify@ackee-mobile-ai-marketplace": true, + "kotlin-lsp@claude-plugins-official": true + }, + "sandbox": { + "enabled": true, + "failIfUnavailable": true, + "autoAllowBashIfSandboxed": true, + "allowUnsandboxedCommands": false, + "filesystem": { + "allowWrite": [ + "~/.gradle/**" + ], + "denyRead": [ + "./local.properties" + ] + }, + "excludedCommands": [ + "./gradlew *" + ] + }, + "plansDirectory": "./.claude/plans" +} diff --git a/.gitignore b/.gitignore index 7507372..dad680e 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,6 @@ google-services.json # Mac OS .DS_Store + +# Claude +.claude/plans diff --git a/CHANGELOG.md b/CHANGELOG.md index f885e46..3a1bc2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,28 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +## BOM [1.0.2] - 2026-05-29 +### http +#### Changed +- Updated build to Kotlin 2.3 and AGP 9. + +### http-ext-json-kotlinx-serialization +#### Changed +- Updated `kotlinx.serialization` to 1.11.0. +- Updated dependency on `http` artifact. + +### http-ktor +#### Changed +- Updated `ktor` to 3.5.0. +- Updated dependency on `http` artifact. + +### http-okhttp +#### Changed +- Updated `okhttp` to 5.3.2. +- Updated dependency on `http` artifact. + +## BOM [1.0.1] - 2025-12-15 ### http #### Fixed - Encoded URL processing. `HttpApythia` now correctly handles encoded URLs and all URL assertions diff --git a/build-logic/logic/build.gradle.kts b/build-logic/logic/build.gradle.kts index 5c803f4..3c930c8 100644 --- a/build-logic/logic/build.gradle.kts +++ b/build-logic/logic/build.gradle.kts @@ -1,6 +1,5 @@ import org.gradle.kotlin.dsl.withType import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.dsl.KotlinVersion import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { @@ -19,26 +18,6 @@ java { targetCompatibility = JavaVersion.VERSION_17 } -// TODO This workarounds issues with incompatibility between Kotest and Gradle. Kotest is now -// compiled with Kotlin 2.2.0 and even though Gradle 9.2.0 has embedded Kotlin of version 2.2.0 as well, -// it fails tests compilation because it apparently compiles it with Kotlin compiler 2.0.0, which -// does not understand compiled libraries with 2.2.0. There was also a warning that language level 1.8 -// is deprecated and will be removed in future versions of Kotlin. Seems like Gradle has this set to -// 1.8 internally and maybe the newest compiler can't compile it anymore, so it fallbacks to older -// compiler version to compile the code, but then it clashes with Kotest? Increasing language version -// to 2.2 fixes the issue, so it looks like this or some similar issue. Try to remove this workaround -// with newer versions of Gradle than 9.2.0. -tasks.withType() - // Apply to test compilation tasks only to let the build logic src to be compiled with Gradle's - // settings. - .matching { it.name.contains("Test") } - .configureEach { - compilerOptions { - languageVersion.set(KotlinVersion.KOTLIN_2_2) - apiVersion.set(KotlinVersion.KOTLIN_2_2) - } - } - tasks.withType().configureEach { useJUnitPlatform() } @@ -47,6 +26,7 @@ dependencies { compileOnly(files(libs::class.java.superclass.protectionDomain.codeSource.location)) compileOnly(libs.android.gradlePlugin) compileOnly(libs.detekt.gradlePlugin) + compileOnly(libs.gradle.versions.gradlePlugin) compileOnly(libs.kotlin.gradlePlugin) compileOnly(libs.mavenPublish.gradlePlugin) @@ -62,6 +42,11 @@ gradlePlugin { pluginClassSimpleName = "AndroidApplicationPlugin", ) + plugin( + dependency = libs.plugins.apythia.dependency.updates, + pluginClassSimpleName = "DependencyUpdatesPlugin", + ) + plugin( dependency = libs.plugins.apythia.kotlin.multiplatform.library, pluginClassSimpleName = "KmpLibraryPlugin", diff --git a/build-logic/logic/src/main/kotlin/io/github/ackeecz/apythia/plugin/AndroidApplicationPlugin.kt b/build-logic/logic/src/main/kotlin/io/github/ackeecz/apythia/plugin/AndroidApplicationPlugin.kt index b07e840..00c04f5 100644 --- a/build-logic/logic/src/main/kotlin/io/github/ackeecz/apythia/plugin/AndroidApplicationPlugin.kt +++ b/build-logic/logic/src/main/kotlin/io/github/ackeecz/apythia/plugin/AndroidApplicationPlugin.kt @@ -3,8 +3,6 @@ package io.github.ackeecz.apythia.plugin import io.github.ackeecz.apythia.util.Constants import org.gradle.api.Plugin import org.gradle.api.Project -import org.gradle.kotlin.dsl.withType -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile internal class AndroidApplicationPlugin : Plugin { @@ -20,9 +18,8 @@ internal class AndroidApplicationPlugin : Plugin { private fun Project.configure() { pluginManager.apply(libs.plugins.android.application) - pluginManager.apply(libs.plugins.kotlin.android) - androidApp { + androidApplication { defaultConfig { targetSdk = Constants.TARGET_SDK @@ -34,7 +31,14 @@ internal class AndroidApplicationPlugin : Plugin { buildTypes { release { isMinifyEnabled = true - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + + val defaultRules = getDefaultProguardFile("proguard-android-optimize.txt") + val customProguardRules = file("proguard-rules.pro").takeIf { it.exists() } + if (customProguardRules != null) { + proguardFiles(defaultRules, customProguardRules) + } else { + proguardFiles(defaultRules) + } } } @@ -47,7 +51,7 @@ internal class AndroidApplicationPlugin : Plugin { } private fun Project.configureKotlin() { - tasks.withType().configureEach { + kotlinAndroid { compilerOptions { configureAllOptions() } diff --git a/build-logic/logic/src/main/kotlin/io/github/ackeecz/apythia/plugin/AndroidPlugin.kt b/build-logic/logic/src/main/kotlin/io/github/ackeecz/apythia/plugin/AndroidPlugin.kt index cfdf2e3..06d70e6 100644 --- a/build-logic/logic/src/main/kotlin/io/github/ackeecz/apythia/plugin/AndroidPlugin.kt +++ b/build-logic/logic/src/main/kotlin/io/github/ackeecz/apythia/plugin/AndroidPlugin.kt @@ -1,6 +1,6 @@ package io.github.ackeecz.apythia.plugin -import com.android.build.gradle.BaseExtension +import com.android.build.api.dsl.CommonExtension import io.github.ackeecz.apythia.util.Constants import org.gradle.api.Plugin import org.gradle.api.Project @@ -12,23 +12,19 @@ internal class AndroidPlugin : Plugin { } private fun Project.configure() { - androidBase { + androidCommon { configureSdkVersions() configureCompileOptions() } } - private fun BaseExtension.configureSdkVersions() { - compileSdkVersion(Constants.COMPILE_SDK) - defaultConfig { - minSdk = Constants.MIN_SDK - } + private fun CommonExtension.configureSdkVersions() { + compileSdk = Constants.COMPILE_SDK + defaultConfig.minSdk = Constants.MIN_SDK } - private fun BaseExtension.configureCompileOptions() { - compileOptions { - sourceCompatibility = Constants.JAVA_VERSION - targetCompatibility = Constants.JAVA_VERSION - } + private fun CommonExtension.configureCompileOptions() { + compileOptions.sourceCompatibility = Constants.JAVA_VERSION + compileOptions.targetCompatibility = Constants.JAVA_VERSION } } diff --git a/build-logic/logic/src/main/kotlin/io/github/ackeecz/apythia/plugin/DependencyUpdatesPlugin.kt b/build-logic/logic/src/main/kotlin/io/github/ackeecz/apythia/plugin/DependencyUpdatesPlugin.kt new file mode 100644 index 0000000..29fee92 --- /dev/null +++ b/build-logic/logic/src/main/kotlin/io/github/ackeecz/apythia/plugin/DependencyUpdatesPlugin.kt @@ -0,0 +1,30 @@ +package io.github.ackeecz.apythia.plugin + +import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.withType +import java.util.Locale + +internal class DependencyUpdatesPlugin : Plugin { + + override fun apply(target: Project) { + with(target) { + tasks.withType { + rejectVersionIf { + isNonStable(candidate.version) && !isNonStable(currentVersion) + } + outputFormatter = "json" + } + } + } + + private fun isNonStable(version: String): Boolean { + val containsStableKeyword = listOf("RELEASE", "FINAL", "GA").any { + version.uppercase(Locale.US).contains(it) + } + val stableRegex = "^[0-9,.v-]+(-r)?$".toRegex() + val isStable = containsStableKeyword || stableRegex.matches(version) + return isStable.not() + } +} diff --git a/build-logic/logic/src/main/kotlin/io/github/ackeecz/apythia/plugin/KmpLibraryPlugin.kt b/build-logic/logic/src/main/kotlin/io/github/ackeecz/apythia/plugin/KmpLibraryPlugin.kt index 29408b1..9627243 100644 --- a/build-logic/logic/src/main/kotlin/io/github/ackeecz/apythia/plugin/KmpLibraryPlugin.kt +++ b/build-logic/logic/src/main/kotlin/io/github/ackeecz/apythia/plugin/KmpLibraryPlugin.kt @@ -1,6 +1,5 @@ package io.github.ackeecz.apythia.plugin -import com.android.build.api.dsl.androidLibrary import io.github.ackeecz.apythia.util.Constants import org.gradle.api.Plugin import org.gradle.api.Project @@ -42,19 +41,20 @@ internal class KmpLibraryPlugin : Plugin { } } - androidLibrary { + android { compileSdk = Constants.COMPILE_SDK minSdk = Constants.MIN_SDK - compilations.configureEach { - compileTaskProvider.configure { - compilerOptions.configureJvmSpecificOptions() - } + compilerOptions { + configureJvmSpecificOptions() } optimization { consumerKeepRules.publish = true - consumerKeepRules.file("consumer-rules.pro") + val consumerRulesFile = target.file("consumer-rules.pro") + if (consumerRulesFile.exists()) { + consumerKeepRules.file(consumerRulesFile) + } minify = false } } diff --git a/build-logic/logic/src/main/kotlin/io/github/ackeecz/apythia/plugin/KmpLibraryWithTestingPlugin.kt b/build-logic/logic/src/main/kotlin/io/github/ackeecz/apythia/plugin/KmpLibraryWithTestingPlugin.kt index d9cadfc..7e8ce3b 100644 --- a/build-logic/logic/src/main/kotlin/io/github/ackeecz/apythia/plugin/KmpLibraryWithTestingPlugin.kt +++ b/build-logic/logic/src/main/kotlin/io/github/ackeecz/apythia/plugin/KmpLibraryWithTestingPlugin.kt @@ -1,6 +1,5 @@ package io.github.ackeecz.apythia.plugin -import com.android.build.api.dsl.androidLibrary import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.tasks.testing.Test @@ -35,12 +34,11 @@ internal class KmpLibraryWithTestingPlugin : Plugin { pluginManager.apply(libs.plugins.gradle.testLogger) } - @Suppress("UnstableApiUsage") private fun Project.configurePlugins() { kotlinMultiplatform { - androidLibrary { + android { // This nice config enables Android host (local unit) tests in KMP module 🫠 - withHostTestBuilder {}.configure {} + withHostTest {} } sourceSets.commonTest.dependencies { diff --git a/build-logic/logic/src/main/kotlin/io/github/ackeecz/apythia/plugin/Utils.kt b/build-logic/logic/src/main/kotlin/io/github/ackeecz/apythia/plugin/Utils.kt index fd96656..b3bee11 100644 --- a/build-logic/logic/src/main/kotlin/io/github/ackeecz/apythia/plugin/Utils.kt +++ b/build-logic/logic/src/main/kotlin/io/github/ackeecz/apythia/plugin/Utils.kt @@ -1,7 +1,8 @@ package io.github.ackeecz.apythia.plugin -import com.android.build.gradle.BaseExtension -import com.android.build.gradle.internal.dsl.BaseAppModuleExtension +import com.android.build.api.dsl.ApplicationExtension +import com.android.build.api.dsl.CommonExtension +import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget import io.gitlab.arturbosch.detekt.extensions.DetektExtension import org.gradle.api.NamedDomainObjectContainer import org.gradle.api.Project @@ -15,9 +16,9 @@ import org.gradle.kotlin.dsl.add import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.the import org.gradle.plugin.use.PluginDependency +import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension -import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension import org.jetbrains.kotlin.gradle.dsl.abi.AbiValidationExtension import org.jetbrains.kotlin.gradle.dsl.abi.AbiValidationMultiplatformExtension import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet @@ -31,12 +32,16 @@ internal fun PluginManager.apply(plugin: Provider) { internal val NamedDomainObjectContainer.androidHostTest: KotlinSourceSet get() = getByName("androidHostTest") -internal fun Project.androidBase(action: BaseExtension.() -> Unit) { - extensions.configure(BaseExtension::class, action) +internal fun Project.androidCommon(action: CommonExtension.() -> Unit) { + extensions.configure(CommonExtension::class, action) } -internal fun Project.androidApp(action: BaseAppModuleExtension.() -> Unit) { - extensions.configure(BaseAppModuleExtension::class, action) +internal fun Project.androidApplication(action: ApplicationExtension.() -> Unit) { + extensions.configure(ApplicationExtension::class, action) +} + +internal fun Project.kotlinAndroid(action: KotlinAndroidProjectExtension.() -> Unit) { + extensions.configure(KotlinAndroidProjectExtension::class, action) } internal fun Project.java(action: JavaPluginExtension.() -> Unit) { @@ -51,6 +56,10 @@ internal fun Project.kotlinMultiplatform(action: KotlinMultiplatformExtension.() extensions.configure(KotlinMultiplatformExtension::class, action) } +internal fun KotlinMultiplatformExtension.android(action: KotlinMultiplatformAndroidLibraryTarget.() -> Unit) { + extensions.configure(KotlinMultiplatformAndroidLibraryTarget::class.java, action) +} + internal fun KotlinMultiplatformExtension.abiValidation(action: AbiValidationMultiplatformExtension.() -> Unit) { extensions.configure(AbiValidationMultiplatformExtension::class, action) } diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts index 49ed9a2..16b184a 100644 --- a/build-logic/settings.gradle.kts +++ b/build-logic/settings.gradle.kts @@ -4,6 +4,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + gradlePluginPortal() } versionCatalogs { create("libs") { diff --git a/build.gradle.kts b/build.gradle.kts index 67459c6..9321a03 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,12 +1,13 @@ plugins { + alias(libs.plugins.apythia.dependency.updates) alias(libs.plugins.apythia.preflightchecks) apply true alias(libs.plugins.android.application) apply false alias(libs.plugins.android.kmp.library) apply false alias(libs.plugins.detekt) apply false alias(libs.plugins.dokka) apply false alias(libs.plugins.gradle.testLogger) apply false + alias(libs.plugins.gradle.versions) alias(libs.plugins.kotest.multiplatform) apply false - alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.kotlin.ksp) apply false alias(libs.plugins.kotlin.multiplatform) apply false diff --git a/gradle.properties b/gradle.properties index 132244e..707d726 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,13 +11,6 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 # This option should only be used with decoupled projects. For more details, visit # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects # org.gradle.parallel=true -# AndroidX package structure to make it clearer which packages are bundled with the -# Android operating system, and which are packaged with your app's APK -# https://developer.android.com/topic/libraries/support-library/androidx-rn -android.useAndroidX=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official -# Enables namespacing of each library's R class so that its R class includes only the -# resources declared in the library itself and none from the library's dependencies, -# thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true +android.onlyEnableUnitTestForTheTestedBuildType=false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6dd9ab8..0d6e8d7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,18 +1,19 @@ [versions] ackee-detekt-config = "1.0.1" -agp = "8.13.1" +agp = "9.2.1" detekt = "1.23.8" -dokka = "2.1.0" +dokka = "2.2.0" gradle-testLogger = "4.0.0" -junit-bom = "6.0.1" +gradle-versions = "0.54.0" +junit-bom = "6.1.0" kmpUri = "0.0.21" -kotest = "6.0.5" -kotlin = "2.2.21" -kotlin-serialization = "1.9.0" -ksp = "2.3.2" -ktor = "3.3.2" -mavenPublish = "0.35.0" -okhttp-bom = "5.3.1" +kotest = "6.1.11" +kotlin = "2.3.21" +kotlin-serialization = "1.11.0" +ksp = "2.3.9" +ktor = "3.5.0" +mavenPublish = "0.36.0" +okhttp-bom = "5.3.2" retrofit = "3.0.0" [libraries] @@ -42,6 +43,7 @@ retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit # Build-logic dependencies android-gradlePlugin = { module = "com.android.tools.build:gradle", version.ref = "agp" } detekt-gradlePlugin = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } +gradle-versions-gradlePlugin = { module = "com.github.ben-manes.versions:com.github.ben-manes.versions.gradle.plugin", version.ref = "gradle-versions" } kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } mavenPublish-gradlePlugin = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "mavenPublish" } @@ -51,8 +53,8 @@ android-kmp-library = { id = "com.android.kotlin.multiplatform.library", version detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } gradle-testLogger = { id = "com.adarshr.test-logger", version.ref = "gradle-testLogger" } +gradle-versions = { id = "com.github.ben-manes.versions", version.ref = "gradle-versions" } kotest-multiplatform = { id = "io.kotest", version.ref = "kotest" } -kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } @@ -60,6 +62,7 @@ mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublis # Convention plugins apythia-android-application = { id = "apythia.android.application" } +apythia-dependency-updates = { id = "apythia.dependency.updates" } apythia-kotlin-jvm-library = { id = "apythia.kotlin.jvm.library" } apythia-kotlin-jvm-librarywithtesting = { id = "apythia.kotlin.jvm.library-with-testing" } apythia-kotlin-multiplatform-library = { id = "apythia.kotlin.multiplatform.library" } diff --git a/gradle/release-notes-urls.toml b/gradle/release-notes-urls.toml new file mode 100644 index 0000000..590ed99 --- /dev/null +++ b/gradle/release-notes-urls.toml @@ -0,0 +1,31 @@ +# gradle/release-notes-urls.toml +# Maps Maven coordinates and Gradle plugin IDs to release-notes URLs. +# Run /find-release-notes to populate; manual edits preserved across runs. + +[libraries] +"com.android.tools.build:gradle" = "https://developer.android.com/build/releases/agp-x-x-x-release-notes" +"com.squareup.okhttp3:okhttp-bom" = "https://github.com/square/okhttp/blob/HEAD/CHANGELOG.md" +"com.vanniktech:gradle-maven-publish-plugin" = "https://github.com/vanniktech/gradle-maven-publish-plugin/blob/HEAD/CHANGELOG.md" +"io.kotest:kotest-assertions-core" = "https://github.com/kotest/kotest/releases" +"io.kotest:kotest-framework-engine" = "https://github.com/kotest/kotest/releases" +"io.kotest:kotest-runner-junit5" = "https://github.com/kotest/kotest/releases" +"io.ktor:ktor-client-logging" = "https://github.com/ktorio/ktor/blob/HEAD/CHANGELOG.md" +"io.ktor:ktor-client-mock" = "https://github.com/ktorio/ktor/blob/HEAD/CHANGELOG.md" +"org.jetbrains.kotlin:kotlin-gradle-plugin" = "https://github.com/JetBrains/kotlin/blob/HEAD/ChangeLog.md" +"org.jetbrains.kotlinx:kotlinx-serialization-json" = "https://github.com/Kotlin/kotlinx.serialization/blob/HEAD/CHANGELOG.md" +"org.junit:junit-bom" = "https://github.com/junit-team/junit-framework/releases" + +[plugins] +"com.android.application" = "https://developer.android.com/build/releases/agp-x-x-x-release-notes" +"com.android.kotlin.multiplatform.library" = "https://developer.android.com/build/releases/agp-x-x-x-release-notes" +"com.google.devtools.ksp" = "https://github.com/google/ksp/releases" +"com.vanniktech.maven.publish" = "https://github.com/vanniktech/gradle-maven-publish-plugin/blob/HEAD/CHANGELOG.md" +"io.kotest" = "https://github.com/kotest/kotest/releases" +"org.jetbrains.dokka" = "https://github.com/Kotlin/dokka/releases" +"org.jetbrains.kotlin.android" = "https://github.com/JetBrains/kotlin/blob/HEAD/ChangeLog.md" +"org.jetbrains.kotlin.jvm" = "https://github.com/JetBrains/kotlin/blob/HEAD/ChangeLog.md" +"org.jetbrains.kotlin.multiplatform" = "https://github.com/JetBrains/kotlin/blob/HEAD/ChangeLog.md" + +[unresolved] +# Entries the skill could not auto-detect. Fill in manually, then move to [libraries] or [plugins]. +# Keys here are the same shape as their target section. diff --git a/gradle/update-deps-ignore.toml b/gradle/update-deps-ignore.toml new file mode 100644 index 0000000..5a24e11 --- /dev/null +++ b/gradle/update-deps-ignore.toml @@ -0,0 +1,9 @@ +# gradle/update-deps-ignore.toml +# Dependencies pinned out of automatic /update-deps runs. +# Value = reason / tracking ticket / pinned-until-date. +# +# To re-enable a dep for /update-deps, remove its entry here (or move it to a comment). + +[libraries] + +[plugins] diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 1b33c55..f8e1ee3 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 bad7c24..058d1f8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,9 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +retries=3 +retryBackOffMs=1000 diff --git a/gradlew b/gradlew index 23d15a9..adff685 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# 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. @@ -114,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -172,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -212,7 +210,6 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" diff --git a/gradlew.bat b/gradlew.bat index 5eed7ee..e509b2d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -70,11 +70,10 @@ goto fail :execute @rem Setup the command line -set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* +"%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 diff --git a/http-ext-json-kotlinx-serialization/build.gradle.kts b/http-ext-json-kotlinx-serialization/build.gradle.kts index 7f60e95..3b3de57 100644 --- a/http-ext-json-kotlinx-serialization/build.gradle.kts +++ b/http-ext-json-kotlinx-serialization/build.gradle.kts @@ -7,7 +7,7 @@ plugins { kotlin { - androidLibrary { + android { namespace = "${Constants.NAMESPACE_PREFIX}.http.extension.json.kotlinx.serialization" } diff --git a/http-ext-json-kotlinx-serialization/src/commonTest/kotlin/io/github/ackeecz/apythia/http/extension/json/kotlinx/serialization/assertion/PartialJsonObjectTests.kt b/http-ext-json-kotlinx-serialization/src/commonTest/kotlin/io/github/ackeecz/apythia/http/extension/json/kotlinx/serialization/assertion/PartialJsonObjectTests.kt index 20a009f..1fc6db5 100644 --- a/http-ext-json-kotlinx-serialization/src/commonTest/kotlin/io/github/ackeecz/apythia/http/extension/json/kotlinx/serialization/assertion/PartialJsonObjectTests.kt +++ b/http-ext-json-kotlinx-serialization/src/commonTest/kotlin/io/github/ackeecz/apythia/http/extension/json/kotlinx/serialization/assertion/PartialJsonObjectTests.kt @@ -6,7 +6,6 @@ import io.github.ackeecz.apythia.testing.http.shouldFail import io.github.ackeecz.apythia.testing.http.shouldNotFail import io.kotest.core.spec.style.FunSpec import io.kotest.core.spec.style.scopes.FunSpecContainerScope -import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.JsonObjectBuilder import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put @@ -34,7 +33,6 @@ private suspend fun FunSpecContainerScope.propertyTest( } } -@OptIn(ExperimentalSerializationApi::class) private suspend fun FunSpecContainerScope.nullPropertyTests( fixture: AssertionDslExtensionsTest.Fixture, ) = with(fixture) { diff --git a/http-ext-json-kotlinx-serialization/src/commonTest/kotlin/io/github/ackeecz/apythia/http/extension/json/kotlinx/serialization/assertion/WholeJsonTests.kt b/http-ext-json-kotlinx-serialization/src/commonTest/kotlin/io/github/ackeecz/apythia/http/extension/json/kotlinx/serialization/assertion/WholeJsonTests.kt index 71f0e2c..484ca37 100644 --- a/http-ext-json-kotlinx-serialization/src/commonTest/kotlin/io/github/ackeecz/apythia/http/extension/json/kotlinx/serialization/assertion/WholeJsonTests.kt +++ b/http-ext-json-kotlinx-serialization/src/commonTest/kotlin/io/github/ackeecz/apythia/http/extension/json/kotlinx/serialization/assertion/WholeJsonTests.kt @@ -14,7 +14,6 @@ import io.kotest.assertions.throwables.shouldNotThrow import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.FunSpec import io.kotest.core.spec.style.scopes.FunSpecContainerScope -import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject @@ -70,7 +69,6 @@ private suspend fun FunSpecContainerScope.jsonObjectTests( } } -@OptIn(ExperimentalSerializationApi::class) private suspend fun FunSpecContainerScope.commonWholeJsonTestSuite( fixture: AssertionDslExtensionsTest.Fixture, assertJson: BodyAssertion.(JsonObject, Json) -> Unit, diff --git a/http-ktor/build.gradle.kts b/http-ktor/build.gradle.kts index 6b34ca6..e31c90b 100644 --- a/http-ktor/build.gradle.kts +++ b/http-ktor/build.gradle.kts @@ -7,7 +7,7 @@ plugins { kotlin { - androidLibrary { + android { namespace = "${Constants.NAMESPACE_PREFIX}.http.ktor" } diff --git a/http-testing/build.gradle.kts b/http-testing/build.gradle.kts index 5d104fc..669f61e 100644 --- a/http-testing/build.gradle.kts +++ b/http-testing/build.gradle.kts @@ -6,7 +6,7 @@ plugins { kotlin { - androidLibrary { + android { namespace = "${Constants.NAMESPACE_PREFIX}.http.testing" } diff --git a/http/build.gradle.kts b/http/build.gradle.kts index 4c82b06..4346ba0 100644 --- a/http/build.gradle.kts +++ b/http/build.gradle.kts @@ -7,7 +7,7 @@ plugins { kotlin { - androidLibrary { + android { namespace = "${Constants.NAMESPACE_PREFIX}.http" } diff --git a/lib.properties b/lib.properties index 776d804..8c10a1c 100644 --- a/lib.properties +++ b/lib.properties @@ -11,35 +11,35 @@ POM_SCM_DEVELOPER_CONNECTION=scm:git:ssh://github.com/AckeeCZ/apythia.git POM_SCM_URL=https://github.com/AckeeCZ/apythia/tree/main # BOM -BOM_VERSION=1.0.1 +BOM_VERSION=1.0.2 BOM_ARTIFACT_ID=apythia-bom BOM_POM_NAME=Apythia BOM BOM_POM_YEAR=2025 BOM_POM_DESCRIPTION=BOM artifact of the Apythia library. # HTTP artifact -HTTP_VERSION=1.0.1 +HTTP_VERSION=1.0.2 HTTP_ARTIFACT_ID=apythia-http HTTP_POM_NAME=Apythia HTTP HTTP_POM_YEAR=2025 HTTP_POM_DESCRIPTION=HTTP artifact of the Apythia library. Contains testing code related to HTTP APIs. # HTTP ext JSON Kotlinx Serialization artifact -HTTP_EXT_JSON_KOTLINX_SERIALIZATION_VERSION=1.0.1 +HTTP_EXT_JSON_KOTLINX_SERIALIZATION_VERSION=1.0.2 HTTP_EXT_JSON_KOTLINX_SERIALIZATION_ARTIFACT_ID=apythia-http-ext-json-kotlinx-serialization HTTP_EXT_JSON_KOTLINX_SERIALIZATION_POM_NAME=Apythia HTTP Extension JSON Kotlinx Serialization HTTP_EXT_JSON_KOTLINX_SERIALIZATION_POM_YEAR=2025 HTTP_EXT_JSON_KOTLINX_SERIALIZATION_POM_DESCRIPTION=HTTP Extension JSON Kotlinx Serialization artifact of the Apythia library. Extends HTTP DSLs with JSON support using Kotlinx Serialization. # HTTP Ktor artifact -HTTP_KTOR_VERSION=1.0.1 +HTTP_KTOR_VERSION=1.0.2 HTTP_KTOR_ARTIFACT_ID=apythia-http-ktor HTTP_KTOR_POM_NAME=Apythia HTTP Ktor HTTP_KTOR_POM_YEAR=2025 HTTP_KTOR_POM_DESCRIPTION=HTTP Ktor artifact of the Apythia library. Contains testing code related to HTTP APIs backed by Ktor. # HTTP OkHttp artifact -HTTP_OKHTTP_VERSION=1.0.1 +HTTP_OKHTTP_VERSION=1.0.2 HTTP_OKHTTP_ARTIFACT_ID=apythia-http-okhttp HTTP_OKHTTP_POM_NAME=Apythia HTTP Okhttp HTTP_OKHTTP_POM_YEAR=2025