├── .editorconfig ├── .github ├── pull_request_template.md └── workflows │ ├── build.yml │ ├── pr_review.yml │ └── release.yaml ├── .gitignore ├── JsonLogicKMP.podspec ├── LICENSE ├── Package.swift ├── README.md ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts ├── settings.gradle.kts ├── src │ └── main │ │ └── kotlin │ │ ├── Dependencies.kt │ │ ├── MultiplatformPublishConfigurationUtils.kt │ │ ├── junit-convention.gradle.kts │ │ ├── publishing-convention.gradle.kts │ │ └── versioning-convention.gradle.kts └── versionConfig.gradle ├── config └── detekt │ └── detekt.yml ├── core ├── build.gradle.kts └── src │ ├── commonMain │ └── kotlin │ │ ├── CommonJsonLogicEngine.kt │ │ ├── JsonLogicEngine.kt │ │ ├── JsonLogicResult.kt │ │ ├── evaluation │ │ ├── CommonLogicEvaluator.kt │ │ └── LogicOperations.kt │ │ └── operations │ │ ├── BooleanUnwrapStrategy.kt │ │ ├── ComparableUnwrapStrategy.kt │ │ ├── ComparingOperation.kt │ │ ├── In.kt │ │ ├── Log.kt │ │ ├── array │ │ ├── ArrayOperation.kt │ │ ├── ArrayOperationInputData.kt │ │ ├── Filter.kt │ │ ├── Map.kt │ │ ├── Merge.kt │ │ ├── NoInitialValueOperation.kt │ │ ├── Reduce.kt │ │ └── occurence │ │ │ ├── All.kt │ │ │ ├── None.kt │ │ │ ├── OccurrenceCheckInputData.kt │ │ │ ├── OccurrenceCheckOperation.kt │ │ │ └── Some.kt │ │ ├── data │ │ ├── Missing.kt │ │ ├── MissingSome.kt │ │ ├── Var.kt │ │ └── unwrap │ │ │ └── ValueFetchingUnwrapStrategy.kt │ │ ├── logic │ │ ├── And.kt │ │ ├── DoubleNegation.kt │ │ ├── If.kt │ │ ├── Negation.kt │ │ ├── Or.kt │ │ ├── equals │ │ │ ├── Equals.kt │ │ │ ├── EqualsOperation.kt │ │ │ ├── EqualsTableOfTruth.kt │ │ │ ├── NotEquals.kt │ │ │ └── strict │ │ │ │ ├── NotStrictEquals.kt │ │ │ │ ├── StrictEquals.kt │ │ │ │ └── StrictEqualsOperation.kt │ │ └── unwrap │ │ │ ├── EqualsUnwrapStrategy.kt │ │ │ ├── SingleNestedValue.kt │ │ │ ├── SingleNestedValueUnwrapStrategy.kt │ │ │ └── TruthyUnwrapStrategy.kt │ │ ├── numeric │ │ ├── Addition.kt │ │ ├── Division.kt │ │ ├── DoubleTypeSensitiveOperation.kt │ │ ├── Max.kt │ │ ├── Min.kt │ │ ├── Modulo.kt │ │ ├── Multiplication.kt │ │ ├── Subtraction.kt │ │ ├── compare │ │ │ ├── GreaterThan.kt │ │ │ ├── GreaterThanOrEqualTo.kt │ │ │ ├── LessThan.kt │ │ │ ├── LessThanOrEqualTo.kt │ │ │ └── RangeComparingOperation.kt │ │ └── unwrap │ │ │ ├── LenientUnwrapStrategy.kt │ │ │ └── StrictUnwrapStrategy.kt │ │ └── string │ │ ├── Cat.kt │ │ ├── StringUnwrapStrategy.kt │ │ └── Substr.kt │ └── commonTest │ ├── ProjectTestsConfig.kt │ └── kotlin │ ├── AssertionUtils.kt │ ├── CommonJsonLogicEngineTest.kt │ ├── JsonLogicEngineTest.kt │ ├── TestInput.kt │ ├── evaluation │ └── CommonLogicEvaluatorTest.kt │ └── operations │ ├── BooleanUnwrapStrategyTest.kt │ ├── ComparableUnwrapStrategyTest.kt │ ├── InTest.kt │ ├── LogTest.kt │ ├── array │ ├── FilterTest.kt │ ├── MapTest.kt │ ├── MergeTest.kt │ ├── ReduceTest.kt │ └── occurrence │ │ ├── AllTest.kt │ │ ├── NoneTest.kt │ │ └── SomeTest.kt │ ├── data │ ├── MissingSomeTest.kt │ ├── MissingTest.kt │ ├── VarTest.kt │ └── unwrap │ │ └── ValueFetchingUnwrapStrategyTest.kt │ ├── logic │ ├── AndTest.kt │ ├── DoubleNegationTest.kt │ ├── IfTest.kt │ ├── NegationTest.kt │ ├── OrTest.kt │ ├── equals │ │ ├── EqualsTest.kt │ │ ├── NotEqualsTest.kt │ │ └── strict │ │ │ ├── NotStrictEqualsTest.kt │ │ │ └── StrictEqualsTest.kt │ └── unwrap │ │ ├── EqualsUnwrapStrategyTest.kt │ │ ├── SingleNestedValueUnwrapStrategyTest.kt │ │ └── TruthyUnwrapStrategyTest.kt │ ├── numeric │ ├── AdditionTest.kt │ ├── DivisionTest.kt │ ├── MaxTest.kt │ ├── MinTest.kt │ ├── ModuloTest.kt │ ├── MultiplicationTest.kt │ ├── SubtractionTest.kt │ ├── compare │ │ ├── GreaterThanOrEqualToTest.kt │ │ ├── GreaterThanTest.kt │ │ ├── LessThanOrEqualToTest.kt │ │ └── LessThanTest.kt │ └── unwrap │ │ ├── LenientUnwrapStrategyTest.kt │ │ └── StrictUnwrapStrategyTest.kt │ └── string │ ├── CatTest.kt │ ├── StringUnwrapStrategyTest.kt │ └── SubstrTest.kt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── operations-api ├── build.gradle.kts └── src │ ├── commonMain │ └── kotlin │ │ ├── JsonLogicException.kt │ │ ├── LogicEvaluator.kt │ │ ├── operation │ │ ├── FunctionalLogicOperation.kt │ │ └── StandardLogicOperation.kt │ │ └── unwrap │ │ └── EvaluatingUnwrapper.kt │ └── commonTest │ └── kotlin │ └── unwrap │ └── EvaluatingUnwrapperTest.kt ├── operations-stdlib ├── build.gradle.kts └── src │ ├── commonMain │ └── kotlin │ │ ├── CurrentTimeMillis.kt │ │ ├── Drop.kt │ │ ├── OperationsProvider.kt │ │ ├── Reverse.kt │ │ ├── array │ │ ├── Distinct.kt │ │ ├── Find.kt │ │ ├── JoinToString.kt │ │ ├── Size.kt │ │ └── Sort.kt │ │ ├── encoding │ │ └── Encode.kt │ │ ├── format │ │ ├── DecimalFormat.kt │ │ └── DecimalFormatter.kt │ │ └── string │ │ ├── Capitalize.kt │ │ ├── IsBlank.kt │ │ ├── Length.kt │ │ ├── Lowercase.kt │ │ ├── Match.kt │ │ ├── Replace.kt │ │ ├── ReplaceMode.kt │ │ ├── Split.kt │ │ ├── StringUnwrapStrategy.kt │ │ ├── ToArray.kt │ │ ├── Trim.kt │ │ ├── Uppercase.kt │ │ └── compareToDate │ │ ├── ComparePrecision.kt │ │ ├── ComparePrecisionDateFormatter.kt │ │ └── CompareToDate.kt │ ├── commonTest │ └── kotlin │ │ ├── DropTest.kt │ │ ├── ReverseTest.kt │ │ ├── TestInput.kt │ │ ├── array │ │ ├── DistinctTest.kt │ │ ├── FindTest.kt │ │ ├── JoinToStringTest.kt │ │ ├── SizeTest.kt │ │ └── SortTest.kt │ │ └── string │ │ ├── CapitalizeTest.kt │ │ ├── IsBlankTest.kt │ │ ├── LengthTest.kt │ │ ├── LowercaseTest.kt │ │ ├── MatchTest.kt │ │ ├── ReplaceTest.kt │ │ ├── SplitTest.kt │ │ ├── StringUnwrapStrategyTest.kt │ │ ├── ToArrayTest.kt │ │ ├── TrimTest.kt │ │ ├── UppercaseTest.kt │ │ └── compareToDate │ │ ├── ComparePrecisionDateFormatterTests.kt │ │ └── CompareToDateTest.kt │ ├── iosMain │ └── kotlin │ │ ├── CurrentTimeMillis.kt │ │ ├── encoding │ │ └── Encode.kt │ │ └── format │ │ └── DecimalFormat.kt │ ├── iosTest │ └── kotlin │ │ ├── CurrentTimeMillisTests.kt │ │ ├── encoding │ │ └── EncodeTest.kt │ │ └── format │ │ └── DecimalFormatTest.kt │ ├── jvmMain │ └── kotlin │ │ ├── CurrentTimeMillis.kt │ │ ├── encoding │ │ └── Encode.kt │ │ └── format │ │ └── DecimalFormat.kt │ └── jvmTest │ └── kotlin │ ├── CurrentTimeMillisTests.kt │ ├── encoding │ └── EncodeTest.kt │ └── format │ └── DecimalFormatTest.kt ├── settings.gradle.kts ├── umbrella-framework ├── build.gradle.kts └── src │ └── iosMain │ └── kotlin │ └── CrashIntegration.kt └── utils ├── build.gradle.kts └── src ├── commonMain └── kotlin │ ├── AnyUtils.kt │ ├── BooleanUtils.kt │ ├── ListUtils.kt │ ├── StringUtils.kt │ └── type │ └── JsonLogicList.kt └── commonTest └── kotlin └── AnyUtilsTest.kt /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_size = 4 3 | indent_style = space 4 | max_line_length = 120 5 | tab_width = 4 6 | insert_final_newline = true 7 | 8 | [*.java] 9 | ij_continuation_indent_size = 4 10 | 11 | [{*.gson, *.gradle, *.groovy, *.gdsl, *.gy, *.gant}] 12 | ij_continuation_indent_size = 4 13 | 14 | [{*.kts, *.kt}] 15 | ij_continuation_indent_size = 4 16 | ij_kotlin_name_count_to_use_star_import = 20 17 | ij_kotlin_name_count_to_use_star_import_for_members = 20 18 | ij_kotlin_align_in_columns_case_branch = false 19 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## What: 2 | 3 | * 4 | 5 | ## Why: 6 | 7 | * 8 | 9 | ## Example: 10 | 11 | * 12 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build libraries 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | types: [opened, synchronize] 7 | 8 | jobs: 9 | build: 10 | name: Build libraries 11 | runs-on: macos-latest 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Set up JDK 11 19 | uses: actions/setup-java@v2 20 | with: 21 | distribution: "temurin" 22 | java-version: 11 23 | 24 | - name: Setup Gradle 25 | uses: gradle/gradle-build-action@v2 26 | 27 | - name: Check 28 | run: ./gradlew detektAll 29 | 30 | - name: Build project 31 | run: ./gradlew build 32 | 33 | - name: Build XCFramework 34 | run: ./gradlew assembleJsonLogicKMPXCFramework 35 | -------------------------------------------------------------------------------- /.github/workflows/pr_review.yml: -------------------------------------------------------------------------------- 1 | name: Detekt check 2 | on: pull_request 3 | 4 | jobs: 5 | detekt: 6 | runs-on: macos-latest 7 | steps: 8 | - name: Checkout repository 9 | uses: actions/checkout@v2 10 | with: 11 | fetch-depth: 0 12 | 13 | - name: Set up JDK 11 14 | uses: actions/setup-java@v2 15 | with: 16 | distribution: "temurin" 17 | java-version: 11 18 | 19 | - name: Setup Gradle 20 | uses: gradle/gradle-build-action@v2 21 | 22 | - name: Set up reviewdog 23 | uses: reviewdog/action-setup@v1 24 | 25 | - name: Run check 26 | run: ./gradlew detektAll -i 27 | 28 | - name: "Upload report" 29 | uses: actions/upload-artifact@v2 30 | with: 31 | name: DetektHTMLReport 32 | path: build/reports/detekt/detekt.html 33 | 34 | - name: Run reviewdog 35 | env: 36 | REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | run: cat build/reports/detekt/detekt.xml | reviewdog -f=checkstyle -reporter=github-pr-review 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Intellij Idea project files 2 | .idea 3 | *.iml 4 | *.ipr 5 | *.iws 6 | 7 | # gradle config 8 | .gradle 9 | 10 | # project binaries 11 | build 12 | out 13 | classes 14 | 15 | # mac os x 16 | .DS_Store 17 | 18 | # local properties 19 | local.properties -------------------------------------------------------------------------------- /JsonLogicKMP.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = "JsonLogicKMP" 3 | spec.version = "1.1.8" 4 | spec.summary = "Kotlin multiplatform JsonLogic" 5 | spec.description = <<-DESC 6 | * Kotlin multiplatform JsonLogic expressions evaluation engine 7 | DESC 8 | spec.homepage = "https://github.com/allegro/json-logic-kmp" 9 | spec.license = { :type => "The Apache License, Version 2.0", :file => "LICENSE" } 10 | spec.author = { "Allegro" => "opensource@allegro.pl" } 11 | 12 | spec.platform = :ios 13 | spec.ios.deployment_target = "13.0" 14 | 15 | spec.source = { :http => "#{spec.homepage}/releases/download/#{spec.version}/#{spec.name}.xcframework.zip", 16 | :sha256 => "27faea59924461ca71254b29d367b82b4033257c63fe399a0e23419ee81e6476" 17 | } 18 | 19 | spec.vendored_frameworks = "#{spec.name}.xcframework" 20 | end 21 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | import PackageDescription 4 | 5 | let version = "1.1.8" 6 | let checksum = "27faea59924461ca71254b29d367b82b4033257c63fe399a0e23419ee81e6476" 7 | 8 | let package = Package( 9 | name: "JsonLogicKMP", 10 | products: [ 11 | .library( 12 | name: "JsonLogicKMP", 13 | targets: ["JsonLogicKMP"]), 14 | ], 15 | targets: [ 16 | .binaryTarget( 17 | name: "JsonLogicKMP", 18 | url: "https://github.com/allegro/json-logic-kmp/releases/download/\(version)/JsonLogicKMP.xcframework.zip", 19 | checksum: "\(checksum)" 20 | ), 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import io.gitlab.arturbosch.detekt.Detekt 2 | 3 | @Suppress("DSL_SCOPE_VIOLATION") // TODO remove on fix: https://youtrack.jetbrains.com/issue/KTIJ-19369 4 | plugins { 5 | id("maven-publish") 6 | id("java-library") 7 | id("signing") 8 | alias(libs.plugins.detekt) 9 | alias(libs.plugins.nexus) 10 | id(Conventions.versioning) 11 | } 12 | 13 | allprojects { 14 | repositories { 15 | google() 16 | mavenCentral() 17 | } 18 | } 19 | 20 | val detektAll by tasks.registering(Detekt::class) { 21 | description = "Runs over whole code base without the starting overhead for each module." 22 | parallel = true 23 | buildUponDefaultConfig = true 24 | setSource(files(projectDir)) 25 | config.setFrom(files("$projectDir/config/detekt/detekt.yml")) 26 | include("**/*.kt") 27 | exclude("**/*.kts") 28 | exclude("resources/") 29 | exclude("build/") 30 | exclude("buildSrc/") 31 | reports { 32 | html.required.set(true) 33 | xml.required.set(true) 34 | txt.required.set(true) 35 | sarif.required.set(true) 36 | } 37 | } 38 | 39 | nexusPublishing { 40 | repositories { 41 | sonatype { 42 | username.set(System.getenv("SONATYPE_USERNAME")) 43 | password.set(System.getenv("SONATYPE_PASSWORD")) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | mavenCentral() 7 | } 8 | 9 | dependencies { 10 | api(libs.axion.release) 11 | } 12 | -------------------------------------------------------------------------------- /buildSrc/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | versionCatalogs { 3 | create("libs") { 4 | from(files("../gradle/libs.versions.toml")) 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/Dependencies.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("SpellCheckingInspection") 2 | 3 | object LibConfig { 4 | const val group = "pl.allegro.mobile" 5 | const val repositoryUrl = "https://github.com/allegro/json-logic-kmp" 6 | const val name = "JsonLogicKMP" 7 | const val artifactName = "json-logic" 8 | const val xcFrameworkName = "JsonLogicKMP" 9 | const val bundleId = "pl.allegro.mobile.JsonLogicKMP" 10 | } 11 | 12 | object Modules { 13 | const val core = ":core" 14 | const val operationsApi = ":operations-api" 15 | const val operationsStdlib = ":operations-stdlib" 16 | const val utils = ":utils" 17 | const val kotlinTest = "test" 18 | } 19 | 20 | object External { 21 | const val dateTime = "org.jetbrains.kotlinx:kotlinx-datetime:0.4.0" 22 | } 23 | 24 | object Conventions { 25 | const val versioning = "versioning-convention" 26 | const val junit = "junit-convention" 27 | const val publishing = "publishing-convention" 28 | } 29 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/MultiplatformPublishConfigurationUtils.kt: -------------------------------------------------------------------------------- 1 | import org.gradle.api.publish.maven.MavenPublication 2 | 3 | fun MavenPublication.setFullModuleArtifactId() { 4 | artifactId = "${LibConfig.artifactName}-$artifactId" 5 | } 6 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/junit-convention.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.gradle.api.tasks.testing.Test 2 | import org.gradle.kotlin.dsl.withType 3 | 4 | tasks.withType { 5 | useJUnitPlatform() 6 | } 7 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/publishing-convention.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.gradle.kotlin.dsl.`maven-publish` 2 | import org.gradle.kotlin.dsl.configure 3 | import org.gradle.kotlin.dsl.signing 4 | import org.gradle.kotlin.dsl.withType 5 | 6 | plugins { 7 | `maven-publish` 8 | signing 9 | id("versioning-convention") 10 | } 11 | 12 | val javadocJar: TaskProvider = tasks.register("javadocJar", Jar::class.java) { 13 | archiveClassifier.set("javadoc") 14 | } 15 | 16 | configure { 17 | publishing { 18 | publications.withType { 19 | setFullModuleArtifactId() 20 | artifact(javadocJar) 21 | 22 | pom { 23 | name.set(LibConfig.name) 24 | description.set("Kotlin multiplatform JsonLogic expressions evaluation engine") 25 | url.set(LibConfig.repositoryUrl) 26 | inceptionYear.set("2022") 27 | licenses { 28 | license { 29 | name.set("The Apache License, Version 2.0") 30 | url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") 31 | } 32 | } 33 | developers { 34 | developer { 35 | name.set("Marek Krogulski") 36 | email.set("marek.krogulski@allegro.pl") 37 | } 38 | } 39 | scm { 40 | connection.set("scm:svn:${LibConfig.repositoryUrl}") 41 | developerConnection.set("scm:git@github.com:allegro/json-logic-kmp.git") 42 | url.set(LibConfig.repositoryUrl) 43 | } 44 | } 45 | } 46 | } 47 | } 48 | 49 | System.getenv("GPG_KEY_ID")?.let { gpgKeyId -> 50 | signing { 51 | useInMemoryPgpKeys( 52 | gpgKeyId, 53 | System.getenv("GPG_PRIVATE_KEY"), 54 | System.getenv("GPG_PRIVATE_KEY_PASSWORD") 55 | ) 56 | sign(publishing.publications) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/versioning-convention.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | /* TODO fix when version catalog will be available for precompiled scripts: 3 | https://github.com/gradle/gradle/issues/15352, https://github.com/gradle/gradle/issues/15383 */ 4 | id("pl.allegro.tech.build.axion-release") 5 | } 6 | 7 | apply(from = "$rootDir/buildSrc/versionConfig.gradle") 8 | 9 | group = LibConfig.group 10 | version = scmVersion.version 11 | -------------------------------------------------------------------------------- /buildSrc/versionConfig.gradle: -------------------------------------------------------------------------------- 1 | scmVersion { 2 | tag { 3 | prefix = '' 4 | } 5 | versionCreator 'versionWithBranch' 6 | } 7 | -------------------------------------------------------------------------------- /core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @Suppress("DSL_SCOPE_VIOLATION") // TODO remove on fix: https://youtrack.jetbrains.com/issue/KTIJ-19369 2 | plugins { 3 | alias(libs.plugins.kotlin.multiplatform) 4 | alias(libs.plugins.kotest) 5 | id(Conventions.junit) 6 | id(Conventions.publishing) 7 | } 8 | 9 | kotlin { 10 | jvm { 11 | mavenPublication { setFullModuleArtifactId() } 12 | compilations.all { 13 | kotlinOptions.jvmTarget = JavaVersion.VERSION_11.majorVersion 14 | } 15 | } 16 | 17 | iosX64() 18 | iosArm64() 19 | iosSimulatorArm64() 20 | 21 | sourceSets { 22 | val commonMain by getting { 23 | dependencies { 24 | implementation(project(Modules.operationsApi)) 25 | implementation(project(Modules.utils)) 26 | } 27 | } 28 | 29 | val commonTest by getting { 30 | dependencies { 31 | implementation(kotlin(Modules.kotlinTest)) 32 | implementation(libs.bundles.common.kotest) 33 | implementation(project(Modules.utils)) 34 | } 35 | } 36 | val jvmTest by getting { 37 | dependencies { 38 | implementation(libs.kotest.jvm.junit5.runner) 39 | } 40 | } 41 | 42 | val iosX64Main by getting 43 | val iosArm64Main by getting 44 | val iosSimulatorArm64Main by getting 45 | val iosMain by creating { 46 | dependsOn(commonMain) 47 | iosX64Main.dependsOn(this) 48 | iosArm64Main.dependsOn(this) 49 | iosSimulatorArm64Main.dependsOn(this) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/CommonJsonLogicEngine.kt: -------------------------------------------------------------------------------- 1 | internal class CommonJsonLogicEngine(private val evaluator: LogicEvaluator) : JsonLogicEngine { 2 | override fun evaluate(expression: Map, data: Any?): JsonLogicResult = 3 | expression.takeIf { 4 | it.isNotEmpty() 5 | }?.let { 6 | safeEvaluate(expression, data) 7 | } ?: JsonLogicResult.Failure.EmptyExpression 8 | 9 | private fun safeEvaluate(expression: Map, data: Any?) = runCatching { 10 | evaluator.evaluateLogic(expression, data) 11 | }.fold( 12 | onSuccess = ::toJsonLogicResult, 13 | onFailure = { JsonLogicResult.Failure.MissingOperation } 14 | ) 15 | 16 | private fun toJsonLogicResult(evaluatedValue: Any?) = evaluatedValue?.let { notNullResult -> 17 | JsonLogicResult.Success(notNullResult.toNormalizedResult()) 18 | } ?: JsonLogicResult.Failure.NullResult 19 | 20 | private fun Any.toNormalizedResult() = 21 | if (this is Double && this.mod(1.0) == 0.0) { 22 | this.toLong() 23 | } else this 24 | } 25 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/JsonLogicResult.kt: -------------------------------------------------------------------------------- 1 | sealed class JsonLogicResult { 2 | data class Success(val value: Any) : JsonLogicResult() 3 | 4 | sealed class Failure : JsonLogicResult() { 5 | object NullResult : Failure() 6 | object EmptyExpression : Failure() 7 | object MissingOperation : Failure() 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/evaluation/CommonLogicEvaluator.kt: -------------------------------------------------------------------------------- 1 | package evaluation 2 | 3 | import JsonLogicException 4 | import LogicEvaluator 5 | import operation.StandardLogicOperation 6 | import utils.asList 7 | 8 | internal class CommonLogicEvaluator(private val operations: LogicOperations) : LogicEvaluator { 9 | override fun evaluateLogic(expression: Map, data: Any?): Any? = 10 | executeExpression(expression, data) 11 | 12 | private fun executeExpression(logic: Any?, data: Any?): Any? { 13 | return when { 14 | logic is List<*> -> logic.map { executeExpression(it, data) } 15 | logic !is Map<*, *> -> logic 16 | logic.isEmpty() -> data 17 | else -> executeOperation(logic, data) 18 | } 19 | } 20 | 21 | private fun executeOperation(logic: Map<*, *>, data: Any?): Any? { 22 | val operator = logic.keys.firstOrNull() 23 | val values = logic[operator] 24 | return if (operations.functionalOperations.keys.contains(operator)) { 25 | operations.functionalOperations[operator]?.evaluateLogic(values, data, this) 26 | } else { 27 | operations.standardOperations.getOperation(operator).evaluateLogic(when (values) { 28 | is List<*> -> values.map { executeExpression(it, data) } 29 | is Map<*, *> -> executeExpression(values, data) 30 | else -> executeExpression(values, data) 31 | }, data) 32 | } 33 | } 34 | 35 | private fun Map.getOperation(operator: Any?) = 36 | get(operator) ?: throw JsonLogicException("Operation $operator not found.") 37 | } 38 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/evaluation/LogicOperations.kt: -------------------------------------------------------------------------------- 1 | package evaluation 2 | 3 | import operation.FunctionalLogicOperation 4 | import operation.StandardLogicOperation 5 | 6 | internal data class LogicOperations( 7 | val standardOperations: Map = emptyMap(), 8 | val functionalOperations: Map = emptyMap() 9 | ) 10 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/BooleanUnwrapStrategy.kt: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | internal interface BooleanUnwrapStrategy { 4 | fun unwrapValueAsBoolean(wrappedValue: Any?): Boolean? = when (wrappedValue) { 5 | is Boolean -> wrappedValue 6 | is Number -> wrappedValue.toLong() > 0 7 | is String -> wrappedValue.toDoubleOrNull()?.toLong()?.let { it > 0 } 8 | else -> null 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/ComparableUnwrapStrategy.kt: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | internal interface ComparableUnwrapStrategy: BooleanUnwrapStrategy { 4 | fun unwrapAsComparable(first: Comparable<*>?, second: Comparable<*>?): List?>? = when { 5 | first is Number && second is Number -> listOf(first.toDouble(), second.toDouble()) 6 | first is String && second is Number -> listOf(first.toDoubleOrNull(), second.toDouble()) 7 | first is Number && second is String -> listOf(first.toDouble(), second.toDoubleOrNull()) 8 | first is Boolean || second is Boolean -> listOf(unwrapValueAsBoolean(first), unwrapValueAsBoolean(second)) 9 | else -> unwrapAsComparableWithTypeSensitivity(first, second) 10 | } 11 | 12 | fun unwrapAsComparableWithTypeSensitivity( 13 | first: Comparable<*>?, 14 | second: Comparable<*>? 15 | ): List?>? = when { 16 | (first != null && second != null && first::class == second::class) -> listOf(first, second) 17 | first == null && second == null -> listOf(first, second) 18 | else -> null 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/ComparingOperation.kt: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import utils.comparableList 4 | import utils.secondOrNull 5 | 6 | internal interface ComparingOperation : ComparableUnwrapStrategy { 7 | fun compareListOfTwo(values: List?, operator: (Int, Int) -> Boolean) = values?.comparableList 8 | ?.let { 9 | compare(it, operator) 10 | } ?: false 11 | 12 | private fun compare(values: List?>, operator: (Int, Int) -> Boolean): Boolean { 13 | return compareOrNull(values.firstOrNull(), values.secondOrNull())?.let { 14 | operator(it, 0) 15 | } ?: false 16 | } 17 | 18 | private fun compareOrNull( 19 | first: Comparable<*>?, 20 | second: Comparable<*>? 21 | ) = unwrapAsComparable(first, second)?.let { values -> 22 | when { 23 | values.all { value -> value == null } -> compareValues(values.firstOrNull(), values.secondOrNull()) 24 | values.any { value -> value == null } -> null 25 | else -> compareValues(values.firstOrNull(), values.secondOrNull()) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/In.kt: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import operation.StandardLogicOperation 4 | import utils.asList 5 | import utils.secondOrNull 6 | 7 | internal object In : StandardLogicOperation { 8 | override fun evaluateLogic(expression: Any?, data: Any?): Boolean? { 9 | val first = expression.asList.firstOrNull() 10 | return when (val second = expression.asList.secondOrNull()) { 11 | is String -> second.contains(first.toString()) 12 | is List<*> -> second.contains(first) 13 | else -> false 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/Log.kt: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import operation.StandardLogicOperation 4 | import utils.asList 5 | 6 | internal class Log(private val logger: ((Any?) -> Unit)? = null) : StandardLogicOperation { 7 | override fun evaluateLogic(expression: Any?, data: Any?): Any? { 8 | val loggedValue = expression.asList.firstOrNull() 9 | logger?.let { log -> log(loggedValue) } 10 | return loggedValue 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/array/ArrayOperation.kt: -------------------------------------------------------------------------------- 1 | package operations.array 2 | 3 | import LogicEvaluator 4 | import unwrap.EvaluatingUnwrapper 5 | import utils.getMappingOperationOrNull 6 | import utils.secondOrNull 7 | import kotlin.collections.Map 8 | 9 | internal interface ArrayOperation : EvaluatingUnwrapper { 10 | fun createOperationInput( 11 | expressionValues: List, 12 | operationData: Any?, 13 | evaluator: LogicEvaluator 14 | ): ArrayOperationInputData { 15 | val evaluatedOperationData = unwrapDataByEvaluation(expressionValues, operationData, evaluator) 16 | val mappingOperation = expressionValues.getMappingOperationOrNull() 17 | val operationDefault = getOperationDefault(mappingOperation, expressionValues) 18 | 19 | return ArrayOperationInputData(evaluatedOperationData, mappingOperation, operationDefault) 20 | } 21 | 22 | fun getOperationDefault(mappingOperation: Map?, expressionValues: List) = 23 | if (mappingOperation == null) { 24 | expressionValues.secondOrNull() 25 | } else null 26 | } 27 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/array/ArrayOperationInputData.kt: -------------------------------------------------------------------------------- 1 | package operations.array 2 | 3 | import kotlin.collections.Map 4 | 5 | internal data class ArrayOperationInputData( 6 | val operationData: List?, 7 | val mappingOperation: Map?, 8 | val operationDefault: Any?, 9 | ) 10 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/array/Filter.kt: -------------------------------------------------------------------------------- 1 | package operations.array 2 | 3 | import LogicEvaluator 4 | import operation.FunctionalLogicOperation 5 | import operations.logic.unwrap.TruthyUnwrapStrategy 6 | import kotlin.collections.Map 7 | 8 | internal object Filter : FunctionalLogicOperation, NoInitialValueOperation, TruthyUnwrapStrategy { 9 | override fun evaluateLogic(expression: Any?, data: Any?, evaluator: LogicEvaluator): Any? = 10 | invokeArrayOperation(expression, data, evaluator, ::filterOrEmptyList) 11 | 12 | private fun filterOrEmptyList( 13 | operationInput: ArrayOperationInputData, 14 | evaluator: LogicEvaluator 15 | ) = with(operationInput) { 16 | operationData.orEmpty().filter { evaluatedValue -> 17 | evaluator.filterValue(evaluatedValue, mappingOperation, operationDefault) 18 | } 19 | } 20 | 21 | private fun LogicEvaluator.filterValue( 22 | evaluatedValue: Any?, 23 | mappingOperation: Map?, 24 | operationDefault: Any? 25 | ) = unwrapValueAsBoolean((mappingOperation?.let { operation -> 26 | evaluateLogic(operation, evaluatedValue) 27 | } ?: operationDefault)) 28 | } 29 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/array/Map.kt: -------------------------------------------------------------------------------- 1 | package operations.array 2 | 3 | import LogicEvaluator 4 | import operation.FunctionalLogicOperation 5 | import kotlin.collections.Map 6 | 7 | internal object Map : FunctionalLogicOperation, NoInitialValueOperation { 8 | override fun evaluateLogic(expression: Any?, data: Any?, evaluator: LogicEvaluator): Any? = 9 | invokeArrayOperation(expression, data, evaluator, ::mapOrEmptyList) 10 | 11 | private fun mapOrEmptyList( 12 | operationInput: ArrayOperationInputData, 13 | evaluator: LogicEvaluator 14 | ) = with(operationInput) { 15 | operationData.orEmpty().map { evaluatedValue -> 16 | evaluator.mapValue(evaluatedValue, mappingOperation, operationDefault) 17 | } 18 | } 19 | 20 | private fun LogicEvaluator.mapValue( 21 | evaluatedValue: Any?, 22 | mappingOperation: Map?, 23 | operationDefault: Any? 24 | ) = mappingOperation?.let { operation -> 25 | evaluateLogic(operation, evaluatedValue) 26 | } ?: operationDefault 27 | } 28 | 29 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/array/Merge.kt: -------------------------------------------------------------------------------- 1 | package operations.array 2 | 3 | import operation.StandardLogicOperation 4 | import utils.asList 5 | 6 | internal object Merge : StandardLogicOperation { 7 | override fun evaluateLogic(expression: Any?, data: Any?): Any = expression.asList.mergeOrAdd() 8 | 9 | private fun List.mergeOrAdd(): List = flatMap { 10 | when (it) { 11 | is List<*> -> it 12 | else -> listOf(it) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/array/NoInitialValueOperation.kt: -------------------------------------------------------------------------------- 1 | package operations.array 2 | 3 | import LogicEvaluator 4 | import utils.asList 5 | 6 | internal interface NoInitialValueOperation : ArrayOperation { 7 | fun invokeArrayOperation( 8 | expression: Any?, 9 | operationData: Any?, 10 | evaluator: LogicEvaluator, 11 | arrayOperation: (ArrayOperationInputData, LogicEvaluator) -> Any? 12 | ) = expression.asList.let { expressionValues -> 13 | val input = createOperationInput(expressionValues, operationData, evaluator) 14 | arrayOperation(input, evaluator) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/array/Reduce.kt: -------------------------------------------------------------------------------- 1 | package operations.array 2 | 3 | import LogicEvaluator 4 | import operation.FunctionalLogicOperation 5 | import utils.asList 6 | import utils.thirdOrNull 7 | import kotlin.collections.Map 8 | 9 | internal object Reduce : FunctionalLogicOperation, ArrayOperation { 10 | private const val CURRENT_DATA_KEY = "current" 11 | private const val ACCUMULATOR_DATA_KEY = "accumulator" 12 | 13 | override fun evaluateLogic(expression: Any?, data: Any?, evaluator: LogicEvaluator): Any? = 14 | expression.asList.let { expressionValues -> 15 | val input = createOperationInput(expressionValues, data, evaluator) 16 | val initialValue = expressionValues.thirdOrNull() 17 | 18 | reduceOrInitial(input, initialValue, evaluator) 19 | } 20 | 21 | private fun reduceOrInitial( 22 | operationInput: ArrayOperationInputData, 23 | initialValue: Any?, 24 | evaluator: LogicEvaluator 25 | ) = with(operationInput) { 26 | operationData?.fold(initialValue) { accumulator, evaluatedValue -> 27 | evaluator.reduceValue(accumulator, evaluatedValue, mappingOperation) ?: return operationDefault 28 | } ?: initialValue 29 | } 30 | 31 | private fun LogicEvaluator.reduceValue( 32 | accumulator: Any?, 33 | evaluatedValue: Any?, 34 | mappingOperation: Map?, 35 | ) = mappingOperation?.let { operation -> 36 | evaluateLogic(operation, toReduceIterationData(accumulator, evaluatedValue)) 37 | } 38 | 39 | private fun toReduceIterationData(accumulator: Any?, current: Any?) = 40 | mapOf(ACCUMULATOR_DATA_KEY to accumulator, CURRENT_DATA_KEY to current) 41 | } 42 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/array/occurence/All.kt: -------------------------------------------------------------------------------- 1 | package operations.array.occurence 2 | 3 | import LogicEvaluator 4 | import operation.FunctionalLogicOperation 5 | 6 | internal object All : FunctionalLogicOperation, OccurrenceCheckOperation { 7 | override fun evaluateLogic(expression: Any?, data: Any?, evaluator: LogicEvaluator): Any? = 8 | checkOccurrence(expression, data, evaluator) 9 | 10 | override fun check( 11 | data: OccurrenceCheckInputData, 12 | evaluator: LogicEvaluator 13 | ) = with(data) { 14 | operationData.forEach { dataValue -> 15 | if (unwrapValueAsBoolean(evaluator.evaluateLogic(mappingOperation, dataValue)).not()) { 16 | return@with operationDefault 17 | } 18 | } 19 | return@with true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/array/occurence/None.kt: -------------------------------------------------------------------------------- 1 | package operations.array.occurence 2 | 3 | import LogicEvaluator 4 | import operation.FunctionalLogicOperation 5 | 6 | internal object None : FunctionalLogicOperation, OccurrenceCheckOperation { 7 | override fun evaluateLogic(expression: Any?, data: Any?, evaluator: LogicEvaluator): Any? = 8 | checkOccurrence(expression, data, evaluator) 9 | 10 | override fun getOperationDefault(mappingOperation: Map?, expressionValues: List) = true 11 | 12 | override fun check( 13 | data: OccurrenceCheckInputData, 14 | evaluator: LogicEvaluator 15 | ) = with(data) { 16 | operationData.forEach { dataValue -> 17 | if (unwrapValueAsBoolean(evaluator.evaluateLogic(mappingOperation, dataValue))) { 18 | return@with false 19 | } 20 | } 21 | return@with operationDefault 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/array/occurence/OccurrenceCheckInputData.kt: -------------------------------------------------------------------------------- 1 | package operations.array.occurence 2 | 3 | internal data class OccurrenceCheckInputData( 4 | val operationData: List, 5 | val mappingOperation: Map, 6 | val operationDefault: Any? 7 | ) 8 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/array/occurence/OccurrenceCheckOperation.kt: -------------------------------------------------------------------------------- 1 | package operations.array.occurence 2 | 3 | import LogicEvaluator 4 | import operations.array.NoInitialValueOperation 5 | import operations.array.ArrayOperationInputData 6 | import operations.logic.unwrap.TruthyUnwrapStrategy 7 | 8 | internal interface OccurrenceCheckOperation : NoInitialValueOperation, TruthyUnwrapStrategy { 9 | fun check(data: OccurrenceCheckInputData, evaluator: LogicEvaluator): Any? 10 | 11 | fun checkOccurrence(expression: Any?, data: Any?, evaluator: LogicEvaluator) = 12 | invokeArrayOperation(expression, data, evaluator) { input, logicEvaluator -> 13 | evaluateOrDefault(input, logicEvaluator, ::check) 14 | } 15 | 16 | private fun evaluateOrDefault( 17 | operationInput: ArrayOperationInputData, 18 | evaluator: LogicEvaluator, 19 | arrayOperation: (OccurrenceCheckInputData, LogicEvaluator) -> Any? 20 | ) = operationInput.toOccurrenceCheckInput()?.let { 21 | arrayOperation(it, evaluator) 22 | } ?: operationInput.operationDefault 23 | 24 | override fun getOperationDefault(mappingOperation: Map?, expressionValues: List) = false 25 | 26 | private fun ArrayOperationInputData.toOccurrenceCheckInput() = 27 | if (mappingOperation != null && operationData != null && operationData.isNotEmpty()) { 28 | OccurrenceCheckInputData(operationData, mappingOperation, operationDefault) 29 | } else null 30 | } 31 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/array/occurence/Some.kt: -------------------------------------------------------------------------------- 1 | package operations.array.occurence 2 | 3 | import LogicEvaluator 4 | import operation.FunctionalLogicOperation 5 | 6 | internal object Some : FunctionalLogicOperation, OccurrenceCheckOperation { 7 | override fun evaluateLogic(expression: Any?, data: Any?, evaluator: LogicEvaluator): Any? = 8 | checkOccurrence(expression, data, evaluator) 9 | 10 | override fun check( 11 | data: OccurrenceCheckInputData, 12 | evaluator: LogicEvaluator 13 | ) = with(data) { 14 | operationData.forEach { dataValue -> 15 | if (unwrapValueAsBoolean(evaluator.evaluateLogic(mappingOperation, dataValue))) { 16 | return@with true 17 | } 18 | } 19 | return@with operationDefault 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/data/Missing.kt: -------------------------------------------------------------------------------- 1 | package operations.data 2 | 3 | import operation.StandardLogicOperation 4 | import utils.asList 5 | 6 | internal object Missing : StandardLogicOperation { 7 | override fun evaluateLogic(expression: Any?, data: Any?): List { 8 | return expression.asList.mapNotNull { 9 | it.takeIf { Var.evaluateLogic(it, data).isNullOrEmptyString() } 10 | } 11 | } 12 | 13 | private fun Any?.isNullOrEmptyString() = this == null || (this is String && this.isEmpty()) 14 | } 15 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/data/MissingSome.kt: -------------------------------------------------------------------------------- 1 | package operations.data 2 | 3 | import operation.StandardLogicOperation 4 | import utils.longOrZero 5 | import utils.secondOrNull 6 | 7 | internal object MissingSome : StandardLogicOperation { 8 | override fun evaluateLogic(expression: Any?, data: Any?): Any { 9 | val min = (expression as? List?)?.firstOrNull()?.toString()?.longOrZero ?: 0 10 | val keys = ((expression as? List?)?.secondOrNull() as? List).orEmpty() 11 | val missing = Missing.evaluateLogic(keys, data) 12 | return missing.takeIf { keys.size - missing.size < min }.orEmpty() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/data/Var.kt: -------------------------------------------------------------------------------- 1 | package operations.data 2 | 3 | import operation.StandardLogicOperation 4 | import operations.data.unwrap.ValueFetchingUnwrapStrategy 5 | import utils.asList 6 | import utils.intOrZero 7 | import utils.secondOrNull 8 | 9 | internal object Var : StandardLogicOperation, ValueFetchingUnwrapStrategy { 10 | override fun evaluateLogic(expression: Any?, data: Any?): Any? = 11 | unwrapDataKeys(expression.asList)?.fetchValueOrDefault(expression, data) 12 | 13 | private fun List.fetchValueOrDefault(expression: Any?, data: Any?): Any? { 14 | val value = if (isNotEmpty()) { 15 | getIndexedValue(data, this) 16 | } else { 17 | data 18 | } 19 | 20 | return if (shouldUseDefaultValue(value, expression)) { 21 | (expression as? List<*>)?.secondOrNull() 22 | } else { 23 | value 24 | } 25 | } 26 | 27 | private fun getIndexedValue(value: Any?, indexParts: List): Any? { 28 | return when (value) { 29 | is List<*> -> { 30 | if (indexParts.size == 1) { 31 | value[indexParts.first().intOrZero] 32 | } else { 33 | getRecursive(indexParts, value) 34 | } 35 | } 36 | is Map<*, *> -> { 37 | val initial = value[indexParts.first()] 38 | indexParts.drop(1).fold(initial) { acc: Any?, indexPart: String -> 39 | (acc as? Map<*, *>)?.get(indexPart) 40 | } 41 | } 42 | else -> value 43 | } 44 | } 45 | 46 | private fun shouldUseDefaultValue(value: Any?, expression: Any?) = (value == expression || value == null) 47 | && expression is List<*> 48 | && expression.size > 1 49 | 50 | private fun getRecursive(indexes: List, data: List): Any? = indexes.firstOrNull()?.apply { 51 | val indexedData = data.getOrNull(intOrZero) 52 | return if (indexedData is List<*>) { 53 | getRecursive(indexes.subList(1, indexes.size), indexedData) 54 | } else { 55 | data.getOrNull(intOrZero) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/data/unwrap/ValueFetchingUnwrapStrategy.kt: -------------------------------------------------------------------------------- 1 | package operations.data.unwrap 2 | 3 | internal interface ValueFetchingUnwrapStrategy { 4 | fun unwrapDataKeys(expression: Any?): List? { 5 | val indexParts = if (expression is List<*>) { 6 | expression.unwrapNestedValue() 7 | } else { 8 | expression 9 | } 10 | 11 | return if (indexParts is List<*>) { 12 | null 13 | } else { 14 | indexParts?.toString()?.split(".").orEmpty() 15 | } 16 | } 17 | 18 | private fun List<*>.unwrapNestedValue(): Any? { 19 | val firstValue = firstOrNull() 20 | return when { 21 | firstValue is List<*> -> firstValue.unwrapNested() 22 | firstValue.isFetchWholeDataValue() -> null 23 | else -> firstValue 24 | } 25 | } 26 | 27 | private fun Any?.isFetchWholeDataValue() = listOf(null, "", emptyList()).contains(this) 28 | 29 | private fun List<*>.unwrapNested() = if (size > 1) { 30 | this 31 | } else { 32 | unwrapNestedValue() ?: this 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/logic/And.kt: -------------------------------------------------------------------------------- 1 | package operations.logic 2 | 3 | import operation.StandardLogicOperation 4 | import operations.logic.unwrap.TruthyUnwrapStrategy 5 | import utils.asList 6 | 7 | internal object And : StandardLogicOperation, TruthyUnwrapStrategy { 8 | override fun evaluateLogic(expression: Any?, data: Any?) = with(expression.asList) { 9 | if (all { it is Boolean }) { 10 | all { unwrapValueAsBoolean(it) } 11 | } else { 12 | firstOrNull { !unwrapValueAsBoolean(it) } ?: last() 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/logic/DoubleNegation.kt: -------------------------------------------------------------------------------- 1 | package operations.logic 2 | 3 | import operation.StandardLogicOperation 4 | import operations.logic.unwrap.TruthyUnwrapStrategy 5 | import utils.asList 6 | 7 | internal object DoubleNegation : StandardLogicOperation, TruthyUnwrapStrategy { 8 | override fun evaluateLogic( 9 | expression: Any?, 10 | data: Any? 11 | ): Boolean = unwrapValueAsBoolean(expression.asList.firstOrNull()) 12 | } 13 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/logic/If.kt: -------------------------------------------------------------------------------- 1 | package operations.logic 2 | 3 | import operation.StandardLogicOperation 4 | import operations.logic.unwrap.TruthyUnwrapStrategy 5 | import utils.asList 6 | import utils.secondOrNull 7 | import utils.thirdOrNull 8 | 9 | @Suppress("MagicNumber") 10 | internal object If : StandardLogicOperation, TruthyUnwrapStrategy { 11 | override fun evaluateLogic(expression: Any?, data: Any?): Any? = expression.asList.recursiveIf() 12 | 13 | private fun List.recursiveIf(): Any? = when (size) { 14 | 0 -> null 15 | 1 -> firstOrNull() 16 | 2 -> if (unwrapValueAsBoolean(firstOrNull())) secondOrNull() else null 17 | 3 -> if (unwrapValueAsBoolean(firstOrNull())) secondOrNull() else thirdOrNull() 18 | else -> if (unwrapValueAsBoolean(firstOrNull())) secondOrNull() else subList(2, size).recursiveIf() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/logic/Negation.kt: -------------------------------------------------------------------------------- 1 | package operations.logic 2 | 3 | import operation.StandardLogicOperation 4 | import operations.logic.unwrap.TruthyUnwrapStrategy 5 | import utils.asList 6 | 7 | internal object Negation : StandardLogicOperation, TruthyUnwrapStrategy { 8 | override fun evaluateLogic( 9 | expression: Any?, 10 | data: Any? 11 | ): Boolean = !unwrapValueAsBoolean(expression.asList.firstOrNull()) 12 | } 13 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/logic/Or.kt: -------------------------------------------------------------------------------- 1 | package operations.logic 2 | 3 | import operation.StandardLogicOperation 4 | import operations.logic.unwrap.TruthyUnwrapStrategy 5 | import utils.asList 6 | 7 | internal object Or : StandardLogicOperation, TruthyUnwrapStrategy { 8 | override fun evaluateLogic(expression: Any?, data: Any?) = with(expression.asList) { 9 | if (all { it is Boolean }) { 10 | firstOrNull { unwrapValueAsBoolean(it) } != null 11 | } else { 12 | (firstOrNull { unwrapValueAsBoolean(it) } ?: last()) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/logic/equals/Equals.kt: -------------------------------------------------------------------------------- 1 | package operations.logic.equals 2 | 3 | import operation.StandardLogicOperation 4 | 5 | internal object Equals : StandardLogicOperation, EqualsOperation { 6 | override fun evaluateLogic( 7 | expression: Any?, 8 | data: Any? 9 | ): Boolean = compare(expression) { first, second -> first == second } 10 | } 11 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/logic/equals/EqualsOperation.kt: -------------------------------------------------------------------------------- 1 | package operations.logic.equals 2 | 3 | import operations.ComparingOperation 4 | import operations.logic.unwrap.EqualsUnwrapStrategy 5 | import operations.logic.unwrap.SingleNestedValueUnwrapStrategy 6 | import utils.asList 7 | import utils.secondOrNull 8 | 9 | internal interface EqualsOperation : ComparingOperation, EqualsUnwrapStrategy, SingleNestedValueUnwrapStrategy { 10 | fun compare(values: Any?, operator: (Int, Int) -> Boolean): Boolean = 11 | with(values.asList) { 12 | val firstUnwrappedValue = unwrapSingleNestedValueOrDefault(firstOrNull()) 13 | val secondUnwrappedValue = unwrapSingleNestedValueOrDefault(secondOrNull()) 14 | val firstPossibleTrueValues = EqualsTableOfTruth[firstUnwrappedValue] 15 | val secondPossibleTrueValues = EqualsTableOfTruth[secondUnwrappedValue] 16 | 17 | if (firstPossibleTrueValues != null || secondPossibleTrueValues != null) { 18 | firstPossibleTrueValues?.contains(secondUnwrappedValue) ?: false 19 | || secondPossibleTrueValues?.contains(firstUnwrappedValue) ?: false 20 | } else { 21 | compareListOfTwo(map(::unwrapValue), operator) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/logic/equals/EqualsTableOfTruth.kt: -------------------------------------------------------------------------------- 1 | package operations.logic.equals 2 | 3 | import operations.logic.unwrap.SingleNestedValue 4 | 5 | @Suppress("SpreadOperator") 6 | internal object EqualsTableOfTruth{ 7 | private val tableOfTruth = mapOf( 8 | true to listOf(true, *getAllVariantsOf(1)), 9 | false to listOf( 10 | false, 11 | *getAllVariantsOf(0), 12 | "", 13 | emptyList(), 14 | SingleNestedValue(emptyList()), 15 | SingleNestedValue(""), 16 | SingleNestedValue(null) 17 | ), 18 | 1 to listOf(true, *getAllVariantsOf(1)), 19 | 0 to listOf( 20 | false, 21 | *getAllVariantsOf(0), 22 | "", 23 | emptyList(), 24 | SingleNestedValue(emptyList()), 25 | SingleNestedValue(""), 26 | SingleNestedValue(null) 27 | ), 28 | "true" to listOf("true"), 29 | "false" to listOf("false"), 30 | "1" to listOf(true, 1, 1.0, "1", SingleNestedValue(1), SingleNestedValue(1.0), SingleNestedValue("1")), 31 | "0" to listOf(false, 0, 0.0, "0", SingleNestedValue(0), SingleNestedValue(0.0), SingleNestedValue("0")), 32 | "" to listOf( 33 | false, 34 | 0, 35 | "", 36 | emptyList(), 37 | SingleNestedValue(emptyList()), 38 | SingleNestedValue(""), 39 | SingleNestedValue(null) 40 | ), 41 | null to listOf(null), 42 | emptyList() to listOf(false, 0, 0.0, ""), 43 | SingleNestedValue(null) to listOf(false, 0, 0.0, ""), 44 | SingleNestedValue("") to listOf(false, 0, 0.0, ""), 45 | SingleNestedValue(emptyList()) to listOf(false, 0, 0.0, ""), 46 | SingleNestedValue(0) to listOf(false, 0, 0.0, "0"), 47 | SingleNestedValue(1) to listOf(true, 1, 1.0, "1"), 48 | SingleNestedValue("1") to listOf(true, 1, 1.0, "1"), 49 | SingleNestedValue("0") to listOf(false, 0, 0.0, "0"), 50 | SingleNestedValue(0.0) to listOf(false, 0, 0.0, "0"), 51 | SingleNestedValue(1.0) to listOf(true, 1, 1.0, "1"), 52 | SingleNestedValue("1.0") to listOf(true, 1, 1.0), 53 | SingleNestedValue("0.0") to listOf(false, 0, 0.0), 54 | 1.0 to listOf(1.0, SingleNestedValue(1.0), SingleNestedValue(1), SingleNestedValue("1.0"), "1", 1, true, "1.0"), 55 | "1.0" to listOf("1.0", 1.0, 1, true), 56 | 0.0 to listOf(*getAllVariantsOf(0.0), false, emptyList(), SingleNestedValue(emptyList())), 57 | "0.0" to listOf("0.0", 0.0, 0, false), 58 | ) 59 | 60 | operator fun get(key: Any?): List? = tableOfTruth[key] 61 | 62 | private fun getAllVariantsOf(value: Int) = listOf(value).plusDoubles().plusStrings().plusNested().toTypedArray() 63 | private fun getAllVariantsOf(value: Double) = listOf(value).plusIntegers().plusStrings().plusNested().toTypedArray() 64 | 65 | private fun List.plusNested() = plus(map { SingleNestedValue(it) }) 66 | private fun List.plusDoubles() = plus(map { it.toDouble() }) 67 | private fun List.plusIntegers() = plus(map { it.toInt() }) 68 | private fun List.plusStrings() = plus(map { it.toString() }) 69 | } 70 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/logic/equals/NotEquals.kt: -------------------------------------------------------------------------------- 1 | package operations.logic.equals 2 | 3 | import operation.StandardLogicOperation 4 | 5 | internal object NotEquals : StandardLogicOperation, EqualsOperation { 6 | override fun evaluateLogic( 7 | expression: Any?, 8 | data: Any? 9 | ): Boolean = !compare(expression) { first, second -> first == second } 10 | } 11 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/logic/equals/strict/NotStrictEquals.kt: -------------------------------------------------------------------------------- 1 | package operations.logic.equals.strict 2 | 3 | import operation.StandardLogicOperation 4 | 5 | internal object NotStrictEquals : StandardLogicOperation, StrictEqualsOperation { 6 | override fun evaluateLogic(expression: Any?, data: Any?): Boolean = 7 | !compare(expression) { first, second -> first == second } 8 | } 9 | 10 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/logic/equals/strict/StrictEquals.kt: -------------------------------------------------------------------------------- 1 | package operations.logic.equals.strict 2 | 3 | import operation.StandardLogicOperation 4 | 5 | internal object StrictEquals : StandardLogicOperation, StrictEqualsOperation { 6 | override fun evaluateLogic( 7 | expression: Any?, 8 | data: Any? 9 | ): Boolean = compare(expression) { first, second -> first == second } 10 | } 11 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/logic/equals/strict/StrictEqualsOperation.kt: -------------------------------------------------------------------------------- 1 | package operations.logic.equals.strict 2 | 3 | import operations.logic.equals.EqualsOperation 4 | import utils.asList 5 | 6 | internal interface StrictEqualsOperation : EqualsOperation { 7 | override fun compare(values: Any?, operator: (Int, Int) -> Boolean): Boolean { 8 | return with(values.asList) { 9 | if (size != 1) { 10 | compareListOfTwo(map(::unwrapValue), operator) 11 | } else false 12 | } 13 | } 14 | 15 | override fun unwrapValue(wrappedValue: Any?): Any? = (wrappedValue as? Number)?.toDouble() ?: wrappedValue 16 | 17 | override fun unwrapAsComparable(first: Comparable<*>?, second: Comparable<*>?): List?>? = 18 | unwrapAsComparableWithTypeSensitivity(first, second) 19 | } 20 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/logic/unwrap/EqualsUnwrapStrategy.kt: -------------------------------------------------------------------------------- 1 | package operations.logic.unwrap 2 | 3 | import utils.asNumber 4 | import utils.isSingleNullList 5 | 6 | internal interface EqualsUnwrapStrategy { 7 | fun unwrapValue(wrappedValue: Any?): Any? = 8 | when (wrappedValue) { 9 | is Number -> wrappedValue.toDouble() 10 | is String -> wrappedValue.toDoubleOrNull() ?: wrappedValue 11 | is List<*> -> wrappedValue.unwrapList() ?: wrappedValue 12 | is Boolean -> wrappedValue.asNumber() 13 | else -> wrappedValue 14 | } 15 | 16 | private fun List<*>.unwrapList() = when { 17 | isSingleNullList() -> 0.0 18 | isEmpty() -> "" 19 | else -> unwrapNotBooleanSingleElement() 20 | } 21 | 22 | private fun List<*>.unwrapNotBooleanSingleElement() = takeIf { size == 1 && firstOrNull() !is Boolean } 23 | ?.let { unwrapValue(firstOrNull()) } 24 | } 25 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/logic/unwrap/SingleNestedValue.kt: -------------------------------------------------------------------------------- 1 | package operations.logic.unwrap 2 | 3 | internal data class SingleNestedValue(val value: Any?) 4 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/logic/unwrap/SingleNestedValueUnwrapStrategy.kt: -------------------------------------------------------------------------------- 1 | package operations.logic.unwrap 2 | 3 | internal interface SingleNestedValueUnwrapStrategy { 4 | fun unwrapSingleNestedValueOrDefault(value: Any?) = value.unwrapSingleNestedValue().let { 5 | if (it != value) { 6 | SingleNestedValue(normalizeNumberString(it)) 7 | } else normalizeNumberString(value) 8 | } 9 | 10 | private fun Any?.unwrapSingleNestedValue(): Any? = when { 11 | this is List<*> && this.size == 1 -> this.firstOrNull().unwrapSingleNestedValue() 12 | else -> this 13 | } 14 | 15 | private fun normalizeNumberString(value: Any?) = (value as? String).let { 16 | (it?.toIntOrNull() ?: it?.toDoubleOrNull())?.toString() ?: value 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/logic/unwrap/TruthyUnwrapStrategy.kt: -------------------------------------------------------------------------------- 1 | package operations.logic.unwrap 2 | 3 | internal interface TruthyUnwrapStrategy { 4 | fun unwrapValueAsBoolean(wrappedValue: Any?): Boolean = when (wrappedValue) { 5 | null -> false 6 | is Boolean -> wrappedValue 7 | is Number -> wrappedValue.toDouble() != 0.0 8 | is String -> wrappedValue.isNotEmpty() && wrappedValue != "[]" && wrappedValue != "null" 9 | is Collection<*> -> wrappedValue.isNotEmpty() 10 | is Array<*> -> wrappedValue.isNotEmpty() 11 | else -> true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/numeric/Addition.kt: -------------------------------------------------------------------------------- 1 | package operations.numeric 2 | 3 | import operation.StandardLogicOperation 4 | import operations.numeric.unwrap.StrictUnwrapStrategy 5 | 6 | internal object Addition : StandardLogicOperation, DoubleTypeSensitiveOperation, StrictUnwrapStrategy { 7 | override fun evaluateLogic( 8 | expression: Any?, 9 | data: Any? 10 | ): Any? = doubleResultOrNull(unwrapValue(expression)) { it.sum() } 11 | } 12 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/numeric/Division.kt: -------------------------------------------------------------------------------- 1 | package operations.numeric 2 | 3 | import operation.StandardLogicOperation 4 | import operations.numeric.unwrap.LenientUnwrapStrategy 5 | 6 | internal object Division : StandardLogicOperation, LenientUnwrapStrategy { 7 | override fun evaluateLogic(expression: Any?, data: Any?) = 8 | unwrapValueAsDouble(expression).takeIf { it.size >= 2 }?.let { 9 | val second = it[1] 10 | val first = it.first() 11 | if (first != null && second != null && second != 0.0) { 12 | first / second 13 | } else null 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/numeric/DoubleTypeSensitiveOperation.kt: -------------------------------------------------------------------------------- 1 | package operations.numeric 2 | 3 | import utils.asDoubleList 4 | 5 | internal interface DoubleTypeSensitiveOperation { 6 | fun doubleResultOrNull(expression: Any?, operation: (List) -> Double?): Double? { 7 | val elements = expression?.asDoubleList 8 | val notNullElements = elements?.filterNotNull() 9 | return if (notNullElements?.size == elements?.size) { 10 | elements?.filterNotNull()?.let { operation(it) } 11 | } else null 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/numeric/Max.kt: -------------------------------------------------------------------------------- 1 | package operations.numeric 2 | 3 | import operation.StandardLogicOperation 4 | 5 | internal object Max : StandardLogicOperation, DoubleTypeSensitiveOperation { 6 | override fun evaluateLogic(expression: Any?, data: Any?): Any? = doubleResultOrNull(expression) { it.maxOrNull() } 7 | } 8 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/numeric/Min.kt: -------------------------------------------------------------------------------- 1 | package operations.numeric 2 | 3 | import operation.StandardLogicOperation 4 | 5 | internal object Min : StandardLogicOperation, DoubleTypeSensitiveOperation { 6 | override fun evaluateLogic(expression: Any?, data: Any?): Any? = doubleResultOrNull(expression) { it.minOrNull() } 7 | } 8 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/numeric/Modulo.kt: -------------------------------------------------------------------------------- 1 | package operations.numeric 2 | 3 | import operation.StandardLogicOperation 4 | import operations.numeric.unwrap.LenientUnwrapStrategy 5 | 6 | internal object Modulo : StandardLogicOperation, LenientUnwrapStrategy { 7 | override fun evaluateLogic(expression: Any?, data: Any?) = 8 | unwrapValueAsDouble(expression).takeIf { it.size >= 2 }?.let { 9 | val second = it[1] 10 | val first = it.first() 11 | if (first != null && second != null && second != 0.0) { 12 | first % second 13 | } else null 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/numeric/Multiplication.kt: -------------------------------------------------------------------------------- 1 | package operations.numeric 2 | 3 | import operation.StandardLogicOperation 4 | import operations.numeric.unwrap.StrictUnwrapStrategy 5 | import utils.asList 6 | 7 | internal object Multiplication : StandardLogicOperation, DoubleTypeSensitiveOperation, StrictUnwrapStrategy { 8 | override fun evaluateLogic(expression: Any?, data: Any?): Any? { 9 | val values = expression.asList 10 | return when (values.size) { 11 | 0 -> null 12 | 1 -> values.first() 13 | else -> doubleResultOrNull(unwrapValue(expression)) { 14 | it.reduce { sum: Double, value: Double -> 15 | sum * value 16 | } 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/numeric/Subtraction.kt: -------------------------------------------------------------------------------- 1 | package operations.numeric 2 | 3 | import operation.StandardLogicOperation 4 | import operations.numeric.unwrap.LenientUnwrapStrategy 5 | 6 | internal object Subtraction : StandardLogicOperation, LenientUnwrapStrategy { 7 | override fun evaluateLogic(expression: Any?, data: Any?) = with(unwrapValueAsDouble(expression)) { 8 | when (size) { 9 | 0 -> null 10 | 1 -> first()?.unaryMinus() 11 | else -> minusOrNull(first(), get(1)) 12 | } 13 | } 14 | 15 | private fun minusOrNull(first: Double?, second: Double?) = if (first != null && second != null) { 16 | first - second 17 | } else null 18 | } 19 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/numeric/compare/GreaterThan.kt: -------------------------------------------------------------------------------- 1 | package operations.numeric.compare 2 | 3 | import operation.StandardLogicOperation 4 | import operations.ComparingOperation 5 | import utils.asList 6 | 7 | internal object GreaterThan : StandardLogicOperation, ComparingOperation { 8 | override fun evaluateLogic(expression: Any?, data: Any?): Any = 9 | compareListOfTwo(expression.asList) { first, second -> first > second } 10 | } 11 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/numeric/compare/GreaterThanOrEqualTo.kt: -------------------------------------------------------------------------------- 1 | package operations.numeric.compare 2 | 3 | import operation.StandardLogicOperation 4 | import operations.ComparingOperation 5 | import utils.asList 6 | 7 | internal object GreaterThanOrEqualTo : StandardLogicOperation, ComparingOperation { 8 | override fun evaluateLogic(expression: Any?, data: Any?): Any = 9 | compareListOfTwo(expression.asList) { first, second -> first >= second } 10 | } 11 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/numeric/compare/LessThan.kt: -------------------------------------------------------------------------------- 1 | package operations.numeric.compare 2 | 3 | import operation.StandardLogicOperation 4 | import utils.asList 5 | 6 | internal object LessThan : StandardLogicOperation, RangeComparingOperation { 7 | override fun evaluateLogic(expression: Any?, data: Any?): Any = 8 | compareOrBetween(expression.asList) { first, second -> first < second } 9 | } 10 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/numeric/compare/LessThanOrEqualTo.kt: -------------------------------------------------------------------------------- 1 | package operations.numeric.compare 2 | 3 | import operation.StandardLogicOperation 4 | import utils.asList 5 | 6 | internal object LessThanOrEqualTo : StandardLogicOperation, RangeComparingOperation { 7 | override fun evaluateLogic(expression: Any?, data: Any?): Any = 8 | compareOrBetween(expression.asList) { first, second -> first <= second } 9 | } 10 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/numeric/compare/RangeComparingOperation.kt: -------------------------------------------------------------------------------- 1 | package operations.numeric.compare 2 | 3 | import operations.ComparingOperation 4 | import utils.comparableList 5 | import utils.secondOrNull 6 | import utils.thirdOrNull 7 | 8 | internal interface RangeComparingOperation : ComparingOperation { 9 | fun compareOrBetween( 10 | values: List?, 11 | operator: (Int, Int) -> Boolean 12 | ) = values?.comparableList?.let { comparableList -> 13 | when { 14 | comparableList.size == 2 -> compareListOfTwo(comparableList, operator) 15 | comparableList.size > 2 -> comparableList.between(operator) 16 | else -> false 17 | } 18 | } ?: false 19 | 20 | private fun List?>.between(operator: (Int, Int) -> Boolean): Boolean { 21 | val firstEvaluation = compareListOfTwo(listOf(firstOrNull(), secondOrNull()), operator) 22 | val secondEvaluation = compareListOfTwo(listOf(secondOrNull(), thirdOrNull()), operator) 23 | return firstEvaluation && secondEvaluation 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/numeric/unwrap/LenientUnwrapStrategy.kt: -------------------------------------------------------------------------------- 1 | package operations.numeric.unwrap 2 | 3 | import utils.asList 4 | import utils.asNumber 5 | 6 | internal interface LenientUnwrapStrategy { 7 | fun unwrapValueAsDouble(wrappedValue: Any?): List = wrappedValue.asList.map(::unwrap) 8 | 9 | private fun unwrap(value: Any?): Double? = 10 | when (value) { 11 | is Number -> value.toDouble() 12 | is String -> value.toDoubleOrNull() 13 | is List<*> -> value.unwrap() 14 | is Boolean -> value.asNumber() 15 | null -> 0.0 16 | else -> null 17 | } 18 | 19 | private fun List<*>.unwrap() = when (size) { 20 | 0 -> 0.0 21 | 1 -> unwrap(first()) 22 | else -> null 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/numeric/unwrap/StrictUnwrapStrategy.kt: -------------------------------------------------------------------------------- 1 | package operations.numeric.unwrap 2 | 3 | import utils.asList 4 | 5 | internal interface StrictUnwrapStrategy { 6 | fun unwrapValue(wrappedValue: Any?): List = wrappedValue.asList.map(::unwrap) 7 | 8 | private tailrec fun unwrap(value: Any?): Any? = 9 | when (value) { 10 | is Number -> value.toDouble() 11 | is String -> value.toDoubleOrNull() 12 | is List<*> -> unwrap(value.firstOrNull()) 13 | else -> null 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/string/Cat.kt: -------------------------------------------------------------------------------- 1 | package operations.string 2 | 3 | import operation.StandardLogicOperation 4 | 5 | internal object Cat : StandardLogicOperation, StringUnwrapStrategy { 6 | override fun evaluateLogic(expression: Any?, data: Any?): Any? = unwrapValueAsString(expression).joinToString("") 7 | } 8 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/string/StringUnwrapStrategy.kt: -------------------------------------------------------------------------------- 1 | package operations.string 2 | 3 | import utils.asList 4 | import utils.toStringOrEmpty 5 | 6 | internal interface StringUnwrapStrategy { 7 | fun unwrapValueAsString(wrappedValue: Any?): List = wrappedValue.asList.map(::stringify) 8 | 9 | private fun stringify(value: Any?) = (value as? List<*>)?.flatMap { nestedValue -> 10 | nestedValue.flattenNestedLists() 11 | }?.joinToString(separator = ",") ?: value.formatAsString() 12 | 13 | private fun Any?.flattenNestedLists(): List = (this as? List<*>)?.flatMap { 14 | it.flattenNestedLists() 15 | } ?: listOf(formatAsString()) 16 | 17 | private fun Any?.formatAsString(): String = if (this is Number && toDouble() == toInt().toDouble()) { 18 | toInt().toString() 19 | } else toStringOrEmpty() 20 | } 21 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/operations/string/Substr.kt: -------------------------------------------------------------------------------- 1 | package operations.string 2 | 3 | import operation.StandardLogicOperation 4 | import utils.asList 5 | import utils.intOrZero 6 | import utils.secondOrNull 7 | import utils.thirdOrNull 8 | import kotlin.math.abs 9 | 10 | internal object Substr : StandardLogicOperation, StringUnwrapStrategy { 11 | override fun evaluateLogic(expression: Any?, data: Any?): String { 12 | return with(expression.asList) { 13 | val startIndex = secondOrNull().toString().intOrZero 14 | val charsCount = thirdOrNull().toString().intOrZero 15 | substringOrEmpty(startIndex, charsCount) 16 | } 17 | } 18 | 19 | private fun List.substringOrEmpty(startIndex: Int, charsCount: Int): String { 20 | val baseString = unwrapValueAsString(firstOrNull()).joinToString(",") 21 | return runCatching { 22 | when { 23 | size == 2 -> baseString.startIndexSubstring(startIndex) 24 | size > 2 -> baseString.fromStartIndexToEndIndex(startIndex, charsCount) 25 | else -> baseString 26 | } 27 | }.getOrNull().orEmpty() 28 | } 29 | 30 | private fun String.startIndexSubstring(startIndex: Int): String = when { 31 | startIndex >= 0 -> substring(startIndex) 32 | abs(startIndex) <= length -> substring(length + startIndex) 33 | else -> this 34 | } 35 | 36 | private fun String.fromStartIndexToEndIndex(startIndex: Int, charsCount: Int) = when { 37 | startIndex >= 0 && charsCount > 0 -> notNegativeArgsSubstring(startIndex, charsCount) 38 | startIndex >= 0 && charsCount < 0 -> substring(startIndex, length + charsCount) 39 | startIndex < 0 && charsCount < 0 -> negativeArgsSubstring(startIndex, charsCount) 40 | startIndex < 0 && charsCount > 0 -> negativeStartIndexSubString(startIndex, charsCount) 41 | else -> null 42 | } 43 | 44 | private fun String.notNegativeArgsSubstring(startIndex: Int, charsCount: Int): String { 45 | val outOfBoundsSafeCount = (startIndex + charsCount).constrainOutOfBoundsCharsCount(length) 46 | return substring(startIndex, outOfBoundsSafeCount) 47 | } 48 | 49 | private fun String.negativeArgsSubstring(startIndex: Int, charsCount: Int): String { 50 | val outOfBoundsSafeStartIndex = constrainNegativeStartIndex(startIndex) 51 | val outOfBoundsSafeCount = (length + charsCount).constrainOutOfBoundsCharsCount(length) 52 | return substring(outOfBoundsSafeStartIndex, outOfBoundsSafeCount) 53 | } 54 | 55 | private fun String.negativeStartIndexSubString(startIndex: Int, charsCount: Int): String { 56 | val outOfBoundsSafeStartIndex = constrainNegativeStartIndex(startIndex) 57 | val outOfBoundsSafeCount = (outOfBoundsSafeStartIndex + charsCount).constrainOutOfBoundsCharsCount(length) 58 | return substring(outOfBoundsSafeStartIndex, outOfBoundsSafeCount) 59 | } 60 | 61 | private fun String.constrainNegativeStartIndex(startIndex: Int) = (length + startIndex).takeIf { it >= 0 } ?: 0 62 | 63 | private fun Int.constrainOutOfBoundsCharsCount(sourceStringLength: Int) = 64 | takeIf { it <= sourceStringLength } ?: sourceStringLength 65 | } 66 | -------------------------------------------------------------------------------- /core/src/commonTest/ProjectTestsConfig.kt: -------------------------------------------------------------------------------- 1 | import io.kotest.core.config.AbstractProjectConfig 2 | import io.kotest.core.extensions.Extension 3 | import io.kotest.core.names.DuplicateTestNameMode 4 | 5 | object ProjectTestsConfig : AbstractProjectConfig() { 6 | override val duplicateTestNameMode = DuplicateTestNameMode.Silent 7 | } 8 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/AssertionUtils.kt: -------------------------------------------------------------------------------- 1 | import io.kotest.matchers.shouldBe 2 | 3 | infix fun JsonLogicResult.valueShouldBe(other: JsonLogicResult) { 4 | if(this is JsonLogicResult.Success && other is JsonLogicResult.Success) { 5 | value shouldBe other.value 6 | } else { 7 | this shouldBe other 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/CommonJsonLogicEngineTest.kt: -------------------------------------------------------------------------------- 1 | import io.kotest.core.spec.style.BehaviorSpec 2 | import io.kotest.matchers.shouldBe 3 | import io.kotest.matchers.types.shouldBeTypeOf 4 | 5 | class CommonJsonLogicEngineTest : BehaviorSpec({ 6 | val logicEngine = JsonLogicEngine.Builder().build() 7 | 8 | given("An expression with unknown operation") { 9 | val logicExpression = mapOf("unknown" to listOf(1, 2)) 10 | 11 | `when`("on evaluation") { 12 | val result = logicEngine.evaluate(logicExpression, null) 13 | 14 | then("returns missing operation failure result") { 15 | result shouldBe JsonLogicResult.Failure.MissingOperation 16 | } 17 | } 18 | } 19 | 20 | given("An empty logic expression") { 21 | val logicExpression = emptyMap() 22 | 23 | `when`("on evaluation") { 24 | val result = logicEngine.evaluate(logicExpression, null) 25 | 26 | then("returns empty expression failure result") { 27 | result shouldBe JsonLogicResult.Failure.EmptyExpression 28 | } 29 | } 30 | } 31 | 32 | given("A null result expression") { 33 | val logicExpression = mapOf("var" to null) 34 | 35 | `when`("on evaluation") { 36 | val result = logicEngine.evaluate(logicExpression, null) 37 | 38 | then("returns null failure result") { 39 | result shouldBe JsonLogicResult.Failure.NullResult 40 | } 41 | } 42 | } 43 | 44 | given("A not null result expression") { 45 | val logicExpression = mapOf("var" to "int") 46 | val data = mapOf("int" to 2) 47 | 48 | `when`("on evaluation") { 49 | val result = logicEngine.evaluate(logicExpression, data) 50 | 51 | then("returns success result") { 52 | result.shouldBeTypeOf() 53 | } 54 | } 55 | } 56 | }) 57 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/TestInput.kt: -------------------------------------------------------------------------------- 1 | data class TestInput( 2 | val expression: Map, 3 | val data: Any? = emptyMap(), 4 | val result: JsonLogicResult 5 | ) 6 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/evaluation/CommonLogicEvaluatorTest.kt: -------------------------------------------------------------------------------- 1 | package evaluation 2 | 3 | import io.kotest.assertions.throwables.shouldThrow 4 | import io.kotest.core.spec.style.BehaviorSpec 5 | import JsonLogicException 6 | 7 | class CommonLogicEvaluatorTest : BehaviorSpec({ 8 | val evaluator = CommonLogicEvaluator(LogicOperations()) 9 | 10 | given("An unknown operation") { 11 | val logicExpression = mapOf("+" to listOf(2, mapOf("unknown" to "3"))) 12 | 13 | then("throws an exception on evaluation") { 14 | shouldThrow { 15 | evaluator.evaluateLogic(logicExpression, null) 16 | } 17 | } 18 | } 19 | }) 20 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/operations/BooleanUnwrapStrategyTest.kt: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import io.kotest.core.spec.style.BehaviorSpec 4 | import io.kotest.matchers.shouldBe 5 | 6 | class BooleanUnwrapStrategyTest : BehaviorSpec({ 7 | val strategyImplementation: BooleanUnwrapStrategy = object : BooleanUnwrapStrategy {} 8 | 9 | given("A boolean") { 10 | val wrappedValue = true 11 | `when`("unwrapped") { 12 | val unwrapResult = strategyImplementation.unwrapValueAsBoolean(wrappedValue) 13 | then("should has the same value") { 14 | unwrapResult shouldBe true 15 | } 16 | } 17 | } 18 | 19 | given("A positive number") { 20 | val wrappedValue = 2.5 21 | `when`("unwrapped") { 22 | val unwrapResult = strategyImplementation.unwrapValueAsBoolean(wrappedValue) 23 | then("should be true") { 24 | unwrapResult shouldBe true 25 | } 26 | } 27 | } 28 | 29 | given("A positive number") { 30 | val wrappedValue = 0.5 31 | `when`("unwrapped") { 32 | val unwrapResult = strategyImplementation.unwrapValueAsBoolean(wrappedValue) 33 | then("should has its decimal value ignored and be false") { 34 | unwrapResult shouldBe false 35 | } 36 | } 37 | } 38 | 39 | given("A positive number string") { 40 | val wrappedValue = "0.5" 41 | `when`("unwrapped") { 42 | val unwrapResult = strategyImplementation.unwrapValueAsBoolean(wrappedValue) 43 | then("should has its decimal value ignored and be false") { 44 | unwrapResult shouldBe false 45 | } 46 | } 47 | } 48 | 49 | given("A positive number string") { 50 | val wrappedValue = "2.5" 51 | `when`("unwrapped") { 52 | val unwrapResult = strategyImplementation.unwrapValueAsBoolean(wrappedValue) 53 | then("should be true") { 54 | unwrapResult shouldBe true 55 | } 56 | } 57 | } 58 | 59 | given("A zero") { 60 | val wrappedValue = 0 61 | `when`("unwrapped") { 62 | val unwrapResult = strategyImplementation.unwrapValueAsBoolean(wrappedValue) 63 | then("should be false") { 64 | unwrapResult shouldBe false 65 | } 66 | } 67 | } 68 | 69 | given("A negative number") { 70 | val wrappedValue = -2.5 71 | `when`("unwrapped") { 72 | val unwrapResult = strategyImplementation.unwrapValueAsBoolean(wrappedValue) 73 | then("should be false") { 74 | unwrapResult shouldBe false 75 | } 76 | } 77 | } 78 | 79 | given("A negative number") { 80 | val wrappedValue = -0.5 81 | `when`("unwrapped") { 82 | val unwrapResult = strategyImplementation.unwrapValueAsBoolean(wrappedValue) 83 | then("should has its decimal value ignored and be false") { 84 | unwrapResult shouldBe false 85 | } 86 | } 87 | } 88 | 89 | given("A negative number string") { 90 | val wrappedValue = "-0.5" 91 | `when`("unwrapped") { 92 | val unwrapResult = strategyImplementation.unwrapValueAsBoolean(wrappedValue) 93 | then("should has its decimal value ignored and be false") { 94 | unwrapResult shouldBe false 95 | } 96 | } 97 | } 98 | 99 | given("Any string value") { 100 | val wrappedValue = "banana" 101 | `when`("unwrapped") { 102 | val unwrapResult = strategyImplementation.unwrapValueAsBoolean(wrappedValue) 103 | then("should by null") { 104 | unwrapResult shouldBe null 105 | } 106 | } 107 | } 108 | 109 | given("Any other type value") { 110 | val wrappedValue = Pair("banana", 1) 111 | `when`("unwrapped") { 112 | val unwrapResult = strategyImplementation.unwrapValueAsBoolean(wrappedValue) 113 | then("should by null") { 114 | unwrapResult shouldBe null 115 | } 116 | } 117 | } 118 | }) 119 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/operations/ComparableUnwrapStrategyTest.kt: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import io.kotest.core.spec.style.BehaviorSpec 4 | import io.kotest.matchers.shouldBe 5 | import type.JsonLogicList 6 | 7 | class ComparableUnwrapStrategyTest : BehaviorSpec({ 8 | val strategyImplementation: ComparableUnwrapStrategy = object : ComparableUnwrapStrategy {} 9 | 10 | given("Two numbers") { 11 | val firstWrappedValue = 2.0 12 | val secondWrappedValue = -3 13 | `when`("unwrapped") { 14 | val unwrapResult = strategyImplementation.unwrapAsComparable(firstWrappedValue, secondWrappedValue) 15 | then("should have the same double values") { 16 | unwrapResult shouldBe listOf(2.0, -3.0) 17 | } 18 | } 19 | } 20 | 21 | given("Number and any string") { 22 | val firstWrappedValue = 2.0 23 | val secondWrappedValue = "banana" 24 | `when`("unwrapped") { 25 | val unwrapResult = strategyImplementation.unwrapAsComparable(firstWrappedValue, secondWrappedValue) 26 | then("should be number and null") { 27 | unwrapResult shouldBe listOf(2.0, null) 28 | } 29 | } 30 | } 31 | 32 | given("Number and number string") { 33 | val firstWrappedValue = 2.0 34 | val secondWrappedValue = "-3" 35 | `when`("unwrapped") { 36 | val unwrapResult = strategyImplementation.unwrapAsComparable(firstWrappedValue, secondWrappedValue) 37 | then("should have the same double values") { 38 | unwrapResult shouldBe listOf(2.0, -3.0) 39 | } 40 | } 41 | } 42 | 43 | given("At least one boolean value") { 44 | val firstWrappedValue = true 45 | val secondWrappedValue = "banana" 46 | `when`("unwrapped") { 47 | val unwrapResult = strategyImplementation.unwrapAsComparable(firstWrappedValue, secondWrappedValue) 48 | then("should has the same boolean value") { 49 | unwrapResult shouldBe listOf(true, null) 50 | } 51 | } 52 | } 53 | 54 | given("Two non-null values of the same type") { 55 | val firstWrappedValue = "strawberry" 56 | val secondWrappedValue = "banana" 57 | `when`("unwrapped") { 58 | val unwrapResult = strategyImplementation.unwrapAsComparable(firstWrappedValue, secondWrappedValue) 59 | then("should have the same values") { 60 | unwrapResult shouldBe listOf("strawberry", "banana") 61 | } 62 | } 63 | } 64 | 65 | given("Two null values") { 66 | val firstWrappedValue = null 67 | val secondWrappedValue = null 68 | `when`("unwrapped") { 69 | val unwrapResult = strategyImplementation.unwrapAsComparable(firstWrappedValue, secondWrappedValue) 70 | then("should have the same values") { 71 | unwrapResult shouldBe listOf(null, null) 72 | } 73 | } 74 | } 75 | 76 | given("Two values of different types") { 77 | val firstWrappedValue = "banana" 78 | val secondWrappedValue = JsonLogicList(listOf("strawberry")) 79 | `when`("unwrapped") { 80 | val unwrapResult = strategyImplementation.unwrapAsComparable(firstWrappedValue, secondWrappedValue) 81 | then("should be null") { 82 | unwrapResult shouldBe null 83 | } 84 | } 85 | } 86 | 87 | given("Null and any non-null type") { 88 | val firstWrappedValue = null 89 | val secondWrappedValue = "banana" 90 | `when`("unwrapped") { 91 | val unwrapResult = strategyImplementation.unwrapAsComparable(firstWrappedValue, secondWrappedValue) 92 | then("should be null") { 93 | unwrapResult shouldBe null 94 | } 95 | } 96 | } 97 | }) 98 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/operations/InTest.kt: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import JsonLogicEngine 4 | import JsonLogicResult 5 | import TestInput 6 | import io.kotest.core.spec.style.FunSpec 7 | import io.kotest.datatest.withData 8 | import valueShouldBe 9 | 10 | class InTest : FunSpec({ 11 | val logicEngine = JsonLogicEngine.Builder().build() 12 | 13 | withData( 14 | nameFn = { input -> "Should evaluated ${input.expression} with given ${input.data} result in ${input.result}" }, 15 | ts = listOf( 16 | TestInput( 17 | expression = mapOf( 18 | "in" to listOf( 19 | mapOf("var" to "filling"), 20 | listOf("apple", "cherry") 21 | ) 22 | ), 23 | data = mapOf("filling" to "apple"), 24 | result = JsonLogicResult.Success(true) 25 | ), 26 | TestInput( 27 | expression = mapOf("in" to listOf("Bart", listOf("Bart", "Homer", "Lisa", "Marge", "Maggie"))), 28 | result = JsonLogicResult.Success(true) 29 | ), 30 | TestInput( 31 | expression = mapOf("in" to listOf("Milhouse", listOf("Bart", "Homer", "Lisa", "Marge", "Maggie"))), 32 | result = JsonLogicResult.Success(false) 33 | ), 34 | TestInput( 35 | expression = mapOf("in" to listOf("Spring", "Springfield")), 36 | result = JsonLogicResult.Success(true) 37 | ), 38 | TestInput( 39 | expression = mapOf("in" to listOf("i", "team")), 40 | result = JsonLogicResult.Success(false) 41 | ), 42 | TestInput( 43 | expression = mapOf("in" to listOf("t", listOf("team"))), 44 | result = JsonLogicResult.Success(false) 45 | ), 46 | TestInput( 47 | expression = mapOf("in" to listOf("1", 133)), 48 | result = JsonLogicResult.Success(false) 49 | ), 50 | TestInput( 51 | expression = mapOf("in" to listOf(1, "133")), 52 | result = JsonLogicResult.Success(true) 53 | ), 54 | TestInput( 55 | expression = mapOf("in" to listOf(1, 133)), 56 | result = JsonLogicResult.Success(false) 57 | ), 58 | TestInput( 59 | expression = mapOf("in" to listOf("t", true)), 60 | result = JsonLogicResult.Success(false) 61 | ), 62 | TestInput( 63 | expression = mapOf("in" to listOf(true, "true gold")), 64 | result = JsonLogicResult.Success(true) 65 | ), 66 | TestInput( 67 | expression = mapOf("in" to listOf(null, "banana")), 68 | result = JsonLogicResult.Success(false) 69 | ), 70 | TestInput( 71 | expression = mapOf("in" to listOf("n", null)), 72 | result = JsonLogicResult.Success(false) 73 | ), 74 | TestInput( 75 | expression = mapOf("in" to listOf(null, listOf(null, null))), 76 | result = JsonLogicResult.Success(true) 77 | ), 78 | TestInput( 79 | expression = mapOf("in" to listOf(null, listOf(listOf(null), "banana"))), 80 | result = JsonLogicResult.Success(false) 81 | ), 82 | TestInput( 83 | expression = mapOf("in" to listOf(null, listOf("", ""))), 84 | result = JsonLogicResult.Success(false) 85 | ), 86 | TestInput( 87 | expression = mapOf("in" to listOf("t", "true")), 88 | result = JsonLogicResult.Success(true) 89 | ), 90 | TestInput( 91 | expression = mapOf("in" to listOf("j", "apple", "juice")), 92 | result = JsonLogicResult.Success(false) 93 | ), 94 | TestInput( 95 | expression = mapOf("in" to listOf("Spring", "Springfield")), 96 | result = JsonLogicResult.Success(true) 97 | ), 98 | ) 99 | // given 100 | ) { testInput: TestInput -> 101 | // when 102 | val evaluationResult = logicEngine.evaluate(testInput.expression, testInput.data) 103 | 104 | // then 105 | evaluationResult valueShouldBe testInput.result 106 | } 107 | }) 108 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/operations/LogTest.kt: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import JsonLogicEngine 4 | import JsonLogicResult 5 | import io.kotest.core.spec.style.BehaviorSpec 6 | import io.kotest.matchers.shouldBe 7 | import io.kotest.matchers.types.shouldBeInstanceOf 8 | 9 | class LogTest : BehaviorSpec({ 10 | given("Log operation") { 11 | val logicEngine = JsonLogicEngine.Builder().build() 12 | 13 | `when`("invoked") { 14 | val logResult = logicEngine.evaluate(mapOf("log" to listOf(1)), null) 15 | 16 | then("should return the same value") { 17 | logResult.shouldBeInstanceOf() 18 | logResult.value shouldBe 1 19 | } 20 | } 21 | } 22 | 23 | given("Log operation") { 24 | var loggedValue: Any? = null 25 | val loggingCallback: (Any?) -> Unit = { loggedValue = it } 26 | val logicEngine = JsonLogicEngine.Builder().addLogger(loggingCallback).build() 27 | 28 | `when`("invoked") { 29 | logicEngine.evaluate(mapOf("log" to listOf(1)), null) 30 | 31 | then("should trigger logging callback") { 32 | loggedValue shouldBe 1 33 | } 34 | } 35 | } 36 | }) 37 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/operations/array/MergeTest.kt: -------------------------------------------------------------------------------- 1 | package operations.array 2 | 3 | import JsonLogicEngine 4 | import JsonLogicResult 5 | import TestInput 6 | import io.kotest.core.spec.style.FunSpec 7 | import io.kotest.datatest.withData 8 | import valueShouldBe 9 | 10 | class MergeTest : FunSpec({ 11 | val logicEngine = JsonLogicEngine.Builder().build() 12 | 13 | withData( 14 | nameFn = { input -> "Should evaluated ${input.expression} with given ${input.data} result in ${input.result}" }, 15 | ts = listOf( 16 | TestInput( 17 | expression = mapOf("merge" to null), 18 | result = JsonLogicResult.Success(listOf(null)) 19 | ), 20 | TestInput( 21 | expression = mapOf("merge" to listOf(1, listOf(2, listOf(3, 4)))), 22 | result = JsonLogicResult.Success(listOf(1, 2, listOf(3, 4))) 23 | ), 24 | TestInput( 25 | expression = mapOf("merge" to listOf(1, listOf(2))), 26 | result = JsonLogicResult.Success(listOf(1, 2)) 27 | ), 28 | TestInput( 29 | expression = mapOf("merge" to listOf(1, 2)), 30 | result = JsonLogicResult.Success(listOf(1, 2)) 31 | ), 32 | TestInput( 33 | expression = mapOf("merge" to 1), 34 | result = JsonLogicResult.Success(listOf(1)) 35 | ), 36 | TestInput( 37 | expression = mapOf("merge" to listOf(listOf(1), listOf(2, 3))), 38 | result = JsonLogicResult.Success(listOf(1, 2, 3)) 39 | ), 40 | TestInput( 41 | expression = mapOf("merge" to listOf(listOf(1, 2), listOf(3))), 42 | result = JsonLogicResult.Success(listOf(1, 2, 3)) 43 | ), 44 | TestInput( 45 | expression = mapOf("merge" to listOf(listOf(1), listOf(2), listOf(3))), 46 | result = JsonLogicResult.Success(listOf(1, 2, 3)) 47 | ), 48 | TestInput( 49 | expression = mapOf("merge" to listOf(listOf(1), listOf(2))), 50 | result = JsonLogicResult.Success(listOf(1, 2)) 51 | ), 52 | TestInput( 53 | expression = mapOf("merge" to listOf(listOf(1), emptyList())), 54 | result = JsonLogicResult.Success(listOf(1)) 55 | ), 56 | TestInput( 57 | expression = mapOf("merge" to listOf(listOf(1))), 58 | result = JsonLogicResult.Success(listOf(1)) 59 | ), 60 | TestInput( 61 | expression = mapOf("merge" to emptyList()), 62 | result = JsonLogicResult.Success(emptyList()) 63 | ), 64 | TestInput( 65 | expression = mapOf("merge" to listOf(1, 2, listOf(3, 4))), 66 | result = JsonLogicResult.Success(listOf(1, 2, 3, 4)) 67 | ), 68 | TestInput( 69 | expression = mapOf("merge" to listOf(listOf(1, 2), listOf(3, 4))), 70 | result = JsonLogicResult.Success(listOf(1, 2, 3, 4)) 71 | ) 72 | ) 73 | // given 74 | ) { testInput: TestInput -> 75 | // when 76 | val evaluationResult = logicEngine.evaluate(testInput.expression, testInput.data) 77 | 78 | // then 79 | evaluationResult valueShouldBe testInput.result 80 | } 81 | }) 82 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/operations/data/MissingSomeTest.kt: -------------------------------------------------------------------------------- 1 | package operations.data 2 | 3 | import JsonLogicEngine 4 | import JsonLogicResult 5 | import TestInput 6 | import io.kotest.core.spec.style.FunSpec 7 | import io.kotest.datatest.withData 8 | import valueShouldBe 9 | 10 | class MissingSomeTest : FunSpec({ 11 | val logicEngine = JsonLogicEngine.Builder().build() 12 | 13 | withData( 14 | nameFn = { input -> "Should evaluated ${input.expression} with given ${input.data} result in ${input.result}" }, 15 | ts = listOf( 16 | TestInput( 17 | expression = mapOf("missing_some" to listOf(1, listOf("a", "b"))), 18 | data = mapOf("a" to "apple"), 19 | result = JsonLogicResult.Success(emptyList()) 20 | ), 21 | TestInput( 22 | expression = mapOf("missing_some" to listOf(1, listOf("a", "b"))), 23 | data = mapOf("b" to "banana"), 24 | result = JsonLogicResult.Success(emptyList()) 25 | ), 26 | TestInput( 27 | expression = mapOf("missing_some" to listOf(1, listOf("a", "b"))), 28 | data = mapOf("a" to "apple", "b" to "banana"), 29 | result = JsonLogicResult.Success(emptyList()) 30 | ), 31 | TestInput( 32 | expression = mapOf("missing_some" to listOf(1, listOf("a", "b"))), 33 | data = mapOf("c" to "carrot"), 34 | result = JsonLogicResult.Success(listOf("a", "b")) 35 | ), 36 | TestInput( 37 | expression = mapOf("missing_some" to listOf(2, listOf("a", "b", "c"))), 38 | data = mapOf("a" to "apple", "b" to "banana"), 39 | result = JsonLogicResult.Success(emptyList()) 40 | ), 41 | TestInput( 42 | expression = mapOf("missing_some" to listOf(2, listOf("a", "b", "c"))), 43 | data = mapOf("a" to "apple", "c" to "carrot"), 44 | result = JsonLogicResult.Success(emptyList()) 45 | ), 46 | TestInput( 47 | expression = mapOf("missing_some" to listOf(2, listOf("a", "b", "c"))), 48 | data = mapOf("a" to "apple", "b" to "banana", "c" to "carrot"), 49 | result = JsonLogicResult.Success(emptyList()) 50 | ), 51 | TestInput( 52 | expression = mapOf("missing_some" to listOf(2, listOf("a", "b", "c"))), 53 | data = mapOf("a" to "apple", "d" to "durian"), 54 | result = JsonLogicResult.Success(listOf("b", "c")) 55 | ), 56 | TestInput( 57 | expression = mapOf("missing_some" to listOf(2, listOf("a", "b", "c"))), 58 | data = mapOf("d" to "durian", "e" to "eggplant"), 59 | result = JsonLogicResult.Success(listOf("a", "b", "c")) 60 | ), 61 | ) 62 | // given 63 | ) { testInput: TestInput -> 64 | // when 65 | val evaluationResult = logicEngine.evaluate(testInput.expression, testInput.data) 66 | 67 | // then 68 | evaluationResult valueShouldBe testInput.result 69 | } 70 | }) 71 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/operations/data/unwrap/ValueFetchingUnwrapStrategyTest.kt: -------------------------------------------------------------------------------- 1 | package operations.data.unwrap 2 | 3 | import io.kotest.core.spec.style.BehaviorSpec 4 | import io.kotest.matchers.shouldBe 5 | 6 | class ValueFetchingUnwrapStrategyTest : BehaviorSpec({ 7 | val strategyImplementation: ValueFetchingUnwrapStrategy = object : ValueFetchingUnwrapStrategy {} 8 | 9 | given("An empty list") { 10 | val wrappedValue = emptyList() 11 | `when`("unwrapped") { 12 | val unwrapResult = strategyImplementation.unwrapDataKeys(wrappedValue) 13 | then("should be the same") { 14 | unwrapResult shouldBe emptyList() 15 | } 16 | } 17 | } 18 | 19 | given("A nested empty list") { 20 | val wrappedValue = listOf(emptyList()) 21 | `when`("unwrapped") { 22 | val unwrapResult = strategyImplementation.unwrapDataKeys(wrappedValue) 23 | then("should be the same") { 24 | unwrapResult shouldBe null 25 | } 26 | } 27 | } 28 | 29 | given("A nested null") { 30 | val wrappedValue = listOf(listOf(null)) 31 | `when`("unwrapped") { 32 | val unwrapResult = strategyImplementation.unwrapDataKeys(wrappedValue) 33 | then("should be null") { 34 | unwrapResult shouldBe null 35 | } 36 | } 37 | } 38 | 39 | given("A null") { 40 | val wrappedValue = null 41 | `when`("unwrapped") { 42 | val unwrapResult = strategyImplementation.unwrapDataKeys(wrappedValue) 43 | then("should be an empty list") { 44 | unwrapResult shouldBe emptyList() 45 | } 46 | } 47 | } 48 | 49 | given("A multiple values list") { 50 | val wrappedValue = listOf(1, "") 51 | `when`("unwrapped") { 52 | val unwrapResult = strategyImplementation.unwrapDataKeys(wrappedValue) 53 | then("should be list with the first value in string") { 54 | unwrapResult shouldBe listOf("1") 55 | } 56 | } 57 | } 58 | 59 | given("A multiple values nested list") { 60 | val wrappedValue = listOf(listOf(1, "banana")) 61 | `when`("unwrapped") { 62 | val unwrapResult = strategyImplementation.unwrapDataKeys(wrappedValue) 63 | then("should be the same") { 64 | unwrapResult shouldBe null 65 | } 66 | } 67 | } 68 | 69 | given("A single item list") { 70 | val wrappedValue = listOf(1) 71 | `when`("unwrapped") { 72 | val unwrapResult = strategyImplementation.unwrapDataKeys(wrappedValue) 73 | then("should be the same with item in string") { 74 | unwrapResult shouldBe listOf("1") 75 | } 76 | } 77 | } 78 | 79 | given("An empty string list") { 80 | val wrappedValue = listOf("") 81 | `when`("unwrapped") { 82 | val unwrapResult = strategyImplementation.unwrapDataKeys(wrappedValue) 83 | then("should be an empty list") { 84 | unwrapResult shouldBe listOf() 85 | } 86 | } 87 | } 88 | 89 | given("A nested empty string list") { 90 | val wrappedValue = listOf(listOf("")) 91 | `when`("unwrapped") { 92 | val unwrapResult = strategyImplementation.unwrapDataKeys(wrappedValue) 93 | then("should be the same") { 94 | unwrapResult shouldBe null 95 | } 96 | } 97 | } 98 | }) 99 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/operations/logic/DoubleNegationTest.kt: -------------------------------------------------------------------------------- 1 | package operations.logic 2 | 3 | import JsonLogicEngine 4 | import JsonLogicResult 5 | import TestInput 6 | import io.kotest.core.spec.style.FunSpec 7 | import io.kotest.datatest.withData 8 | import valueShouldBe 9 | 10 | class DoubleNegationTest : FunSpec({ 11 | val logicEngine = JsonLogicEngine.Builder().build() 12 | 13 | withData( 14 | nameFn = { input -> "Should evaluated ${input.expression} with given ${input.data} result in ${input.result}" }, 15 | ts = listOf( 16 | TestInput(expression = mapOf("!!" to listOf(false)), result = JsonLogicResult.Success(false)), 17 | TestInput(expression = mapOf("!!" to listOf(true)), result = JsonLogicResult.Success(true)), 18 | TestInput(expression = mapOf("!!" to false), result = JsonLogicResult.Success(false)), 19 | TestInput(expression = mapOf("!!" to "false"), result = JsonLogicResult.Success(true)), 20 | TestInput(expression = mapOf("!!" to true), result = JsonLogicResult.Success(true)), 21 | TestInput(expression = mapOf("!!" to 0), result = JsonLogicResult.Success(false)), 22 | TestInput(expression = mapOf("!!" to 1), result = JsonLogicResult.Success(true)), 23 | TestInput( 24 | expression = mapOf("!!" to listOf(emptyList())), 25 | result = JsonLogicResult.Success(false) 26 | ), 27 | TestInput(expression = mapOf("!!" to listOf(0)), result = JsonLogicResult.Success(false)), 28 | TestInput(expression = mapOf("!!" to listOf("")), result = JsonLogicResult.Success(false)), 29 | TestInput(expression = mapOf("!!" to listOf("0")), result = JsonLogicResult.Success(true)), 30 | TestInput(expression = mapOf("!!" to listOf(null)), result = JsonLogicResult.Success(false)), 31 | TestInput( 32 | expression = mapOf("!!" to listOf("banana", null)), 33 | result = JsonLogicResult.Success(true) 34 | ), 35 | TestInput(expression = mapOf("!!" to listOf(13)), result = JsonLogicResult.Success(true)), 36 | TestInput( 37 | expression = mapOf("!!" to listOf(false, false)), 38 | result = JsonLogicResult.Success(false) 39 | ), 40 | TestInput(expression = mapOf("!!" to listOf(true, true)), result = JsonLogicResult.Success(true)), 41 | TestInput(expression = mapOf("!!" to listOf(true, null)), result = JsonLogicResult.Success(true)), 42 | TestInput(expression = mapOf("!!" to listOf(null, null)), result = JsonLogicResult.Success(false)), 43 | ) 44 | // given 45 | ) { testInput: TestInput -> 46 | // when 47 | val evaluationResult = logicEngine.evaluate(testInput.expression, testInput.data) 48 | 49 | // then 50 | evaluationResult valueShouldBe testInput.result 51 | } 52 | }) 53 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/operations/logic/NegationTest.kt: -------------------------------------------------------------------------------- 1 | package operations.logic 2 | 3 | import JsonLogicEngine 4 | import JsonLogicResult 5 | import TestInput 6 | import io.kotest.core.spec.style.FunSpec 7 | import io.kotest.datatest.withData 8 | import valueShouldBe 9 | 10 | class NegationTest : FunSpec({ 11 | val logicEngine = JsonLogicEngine.Builder().build() 12 | 13 | withData( 14 | nameFn = { input -> "Should evaluated ${input.expression} with given ${input.data} result in ${input.result}" }, 15 | ts = listOf( 16 | TestInput(expression = mapOf("!" to listOf(false)), result = JsonLogicResult.Success(true)), 17 | TestInput(expression = mapOf("!" to listOf(true)), result = JsonLogicResult.Success(false)), 18 | TestInput(expression = mapOf("!" to "false"), result = JsonLogicResult.Success(false)), 19 | TestInput(expression = mapOf("!" to false), result = JsonLogicResult.Success(true)), 20 | TestInput(expression = mapOf("!" to true), result = JsonLogicResult.Success(false)), 21 | TestInput(expression = mapOf("!" to 0), result = JsonLogicResult.Success(true)), 22 | TestInput(expression = mapOf("!" to 1), result = JsonLogicResult.Success(false)), 23 | TestInput( 24 | expression = mapOf("!" to listOf(emptyList())), 25 | result = JsonLogicResult.Success(true) 26 | ), 27 | TestInput(expression = mapOf("!" to listOf(0)), result = JsonLogicResult.Success(true)), 28 | TestInput(expression = mapOf("!" to listOf("")), result = JsonLogicResult.Success(true)), 29 | TestInput(expression = mapOf("!" to listOf("0")), result = JsonLogicResult.Success(false)), 30 | TestInput(expression = mapOf("!" to listOf(null)), result = JsonLogicResult.Success(true)), 31 | TestInput( 32 | expression = mapOf("!" to listOf("banana", null)), 33 | result = JsonLogicResult.Success(false) 34 | ), 35 | TestInput(expression = mapOf("!" to listOf(13)), result = JsonLogicResult.Success(false)), 36 | TestInput(expression = mapOf("!" to listOf(false, false)), result = JsonLogicResult.Success(true)), 37 | TestInput(expression = mapOf("!" to listOf(true, true)), result = JsonLogicResult.Success(false)), 38 | TestInput(expression = mapOf("!" to listOf(true, null)), result = JsonLogicResult.Success(false)), 39 | TestInput(expression = mapOf("!" to listOf(null, null)), result = JsonLogicResult.Success(true)), 40 | ) 41 | // given 42 | ) { testInput: TestInput -> 43 | // when 44 | val evaluationResult = logicEngine.evaluate(testInput.expression, testInput.data) 45 | 46 | // then 47 | evaluationResult valueShouldBe testInput.result 48 | } 49 | }) 50 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/operations/logic/unwrap/EqualsUnwrapStrategyTest.kt: -------------------------------------------------------------------------------- 1 | package operations.logic.unwrap 2 | 3 | import io.kotest.core.spec.style.BehaviorSpec 4 | import io.kotest.matchers.shouldBe 5 | 6 | class EqualsUnwrapStrategyTest : BehaviorSpec({ 7 | val strategyImplementation: EqualsUnwrapStrategy = object : EqualsUnwrapStrategy {} 8 | 9 | given("A number") { 10 | val wrappedValue = 20 11 | `when`("unwrapped") { 12 | val unwrapResult = strategyImplementation.unwrapValue(wrappedValue) 13 | then("should has the same decimal value") { 14 | unwrapResult shouldBe 20.0 15 | } 16 | } 17 | } 18 | 19 | given("A number string") { 20 | val wrappedValue = "12" 21 | `when`("unwrapped") { 22 | val unwrapResult = strategyImplementation.unwrapValue(wrappedValue) 23 | then("should has the decimal value") { 24 | unwrapResult shouldBe 12.0 25 | } 26 | } 27 | } 28 | 29 | given("Any string") { 30 | val wrappedValue = "banana" 31 | `when`("unwrapped") { 32 | val unwrapResult = strategyImplementation.unwrapValue(wrappedValue) 33 | then("should has the same value") { 34 | unwrapResult shouldBe "banana" 35 | } 36 | } 37 | } 38 | 39 | given("A list with single null") { 40 | val wrappedValue = listOf(null) 41 | `when`("unwrapped") { 42 | val unwrapResult = strategyImplementation.unwrapValue(wrappedValue) 43 | then("should be 0") { 44 | unwrapResult shouldBe 0.0 45 | } 46 | } 47 | } 48 | 49 | given("A list with single boolean") { 50 | val wrappedValue = listOf(true) 51 | `when`("unwrapped") { 52 | val unwrapResult = strategyImplementation.unwrapValue(wrappedValue) 53 | then("should be the same value") { 54 | unwrapResult shouldBe listOf(true) 55 | } 56 | } 57 | } 58 | 59 | given("A list with single value") { 60 | val wrappedValue = listOf(20) 61 | `when`("unwrapped") { 62 | val unwrapResult = strategyImplementation.unwrapValue(wrappedValue) 63 | then("should be flattened value") { 64 | unwrapResult shouldBe 20.0 65 | } 66 | } 67 | } 68 | 69 | given("A list with multiple values") { 70 | val wrappedValue = listOf(2, "banana") 71 | `when`("unwrapped") { 72 | val unwrapResult = strategyImplementation.unwrapValue(wrappedValue) 73 | then("should be the same") { 74 | unwrapResult shouldBe listOf(2, "banana") 75 | } 76 | } 77 | } 78 | 79 | given("An empty list") { 80 | val wrappedValue = emptyList() 81 | `when`("unwrapped") { 82 | val unwrapResult = strategyImplementation.unwrapValue(wrappedValue) 83 | then("should be an empty string") { 84 | unwrapResult shouldBe "" 85 | } 86 | } 87 | } 88 | 89 | given("A nested empty list") { 90 | val wrappedValue = listOf(emptyList()) 91 | `when`("unwrapped") { 92 | val unwrapResult = strategyImplementation.unwrapValue(wrappedValue) 93 | then("should be an empty string") { 94 | unwrapResult shouldBe "" 95 | } 96 | } 97 | } 98 | 99 | given("A true value") { 100 | val wrappedValue = true 101 | `when`("unwrapped") { 102 | val unwrapResult = strategyImplementation.unwrapValue(wrappedValue) 103 | then("should be 1") { 104 | unwrapResult shouldBe 1.0 105 | } 106 | } 107 | } 108 | 109 | given("A false value") { 110 | val wrappedValue = false 111 | `when`("unwrapped") { 112 | val unwrapResult = strategyImplementation.unwrapValue(wrappedValue) 113 | then("should be 0") { 114 | unwrapResult shouldBe 0.0 115 | } 116 | } 117 | } 118 | 119 | given("Any other type value") { 120 | val wrappedValue = Pair(1, 2) 121 | `when`("unwrapped") { 122 | val unwrapResult = strategyImplementation.unwrapValue(wrappedValue) 123 | then("should be the same") { 124 | unwrapResult shouldBe Pair(1, 2) 125 | } 126 | } 127 | } 128 | }) 129 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/operations/logic/unwrap/SingleNestedValueUnwrapStrategyTest.kt: -------------------------------------------------------------------------------- 1 | package operations.logic.unwrap 2 | 3 | import io.kotest.core.spec.style.BehaviorSpec 4 | import io.kotest.matchers.shouldBe 5 | 6 | class SingleNestedValueUnwrapStrategyTest : BehaviorSpec({ 7 | val strategyImplementation: SingleNestedValueUnwrapStrategy = object : SingleNestedValueUnwrapStrategy {} 8 | 9 | given("A non list item") { 10 | val wrappedValue = "banana" 11 | `when`("unwrapped") { 12 | val unwrapResult = strategyImplementation.unwrapSingleNestedValueOrDefault(wrappedValue) 13 | then("should be the same") { 14 | unwrapResult shouldBe "banana" 15 | } 16 | } 17 | } 18 | 19 | given("A single item list") { 20 | val wrappedValue = listOf("banana") 21 | `when`("unwrapped") { 22 | val unwrapResult = strategyImplementation.unwrapSingleNestedValueOrDefault(wrappedValue) 23 | then("should be SingleNestedValue with that item") { 24 | unwrapResult shouldBe SingleNestedValue("banana") 25 | } 26 | } 27 | } 28 | 29 | given("A multiple items list") { 30 | val wrappedValue = listOf("banana", "apple") 31 | `when`("unwrapped") { 32 | val unwrapResult = strategyImplementation.unwrapSingleNestedValueOrDefault(wrappedValue) 33 | then("should be the same") { 34 | unwrapResult shouldBe listOf("banana", "apple") 35 | } 36 | } 37 | } 38 | 39 | given("A nested empty list") { 40 | val wrappedValue = listOf(emptyList()) 41 | `when`("unwrapped") { 42 | val unwrapResult = strategyImplementation.unwrapSingleNestedValueOrDefault(wrappedValue) 43 | then("should be SingleNestedValue with empty list") { 44 | unwrapResult shouldBe SingleNestedValue(emptyList()) 45 | } 46 | } 47 | } 48 | 49 | given("A double nested null") { 50 | val wrappedValue = listOf(listOf(null)) 51 | `when`("unwrapped") { 52 | val unwrapResult = strategyImplementation.unwrapSingleNestedValueOrDefault(wrappedValue) 53 | then("should be SingleNestedValue with null") { 54 | unwrapResult shouldBe SingleNestedValue(null) 55 | } 56 | } 57 | } 58 | 59 | given("A double nested number") { 60 | val wrappedValue = listOf(listOf(1)) 61 | `when`("unwrapped") { 62 | val unwrapResult = strategyImplementation.unwrapSingleNestedValueOrDefault(wrappedValue) 63 | then("should be SingleNestedValue with that number") { 64 | unwrapResult shouldBe SingleNestedValue(1) 65 | } 66 | } 67 | } 68 | 69 | given("A double nested multiple items list") { 70 | val wrappedValue = listOf(listOf(listOf(1, 2))) 71 | `when`("unwrapped") { 72 | val unwrapResult = strategyImplementation.unwrapSingleNestedValueOrDefault(wrappedValue) 73 | then("should be SingleNestedValue with that multiple items list") { 74 | unwrapResult shouldBe SingleNestedValue(listOf(1, 2)) 75 | } 76 | } 77 | } 78 | }) 79 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/operations/logic/unwrap/TruthyUnwrapStrategyTest.kt: -------------------------------------------------------------------------------- 1 | package operations.logic.unwrap 2 | 3 | import io.kotest.core.spec.style.BehaviorSpec 4 | import io.kotest.matchers.shouldBe 5 | 6 | class TruthyUnwrapStrategyTest : BehaviorSpec({ 7 | val strategyImplementation: TruthyUnwrapStrategy = object : TruthyUnwrapStrategy {} 8 | 9 | given("A string zero") { 10 | val wrappedValue = "0" 11 | `when`("unwrapped") { 12 | val unwrapResult = strategyImplementation.unwrapValueAsBoolean(wrappedValue) 13 | then("should be true") { 14 | unwrapResult shouldBe true 15 | } 16 | } 17 | } 18 | 19 | given("A null") { 20 | val wrappedValue = null 21 | `when`("unwrapped") { 22 | val unwrapResult = strategyImplementation.unwrapValueAsBoolean(wrappedValue) 23 | then("should be false") { 24 | unwrapResult shouldBe false 25 | } 26 | } 27 | } 28 | 29 | given("Any non-empty string") { 30 | val wrappedValue = "anything" 31 | `when`("unwrapped") { 32 | val unwrapResult = strategyImplementation.unwrapValueAsBoolean(wrappedValue) 33 | then("should be true") { 34 | unwrapResult shouldBe true 35 | } 36 | } 37 | } 38 | 39 | given("An empty string") { 40 | val wrappedValue = "" 41 | `when`("unwrapped") { 42 | val unwrapResult = strategyImplementation.unwrapValueAsBoolean(wrappedValue) 43 | then("should be false") { 44 | unwrapResult shouldBe false 45 | } 46 | } 47 | } 48 | 49 | given("A blank string") { 50 | val wrappedValue = " " 51 | `when`("unwrapped") { 52 | val unwrapResult = strategyImplementation.unwrapValueAsBoolean(wrappedValue) 53 | then("should be true") { 54 | unwrapResult shouldBe true 55 | } 56 | } 57 | } 58 | 59 | given("A non-empty list") { 60 | val wrappedValue = listOf("banana", "strawberry") 61 | `when`("unwrapped") { 62 | val unwrapResult = strategyImplementation.unwrapValueAsBoolean(wrappedValue) 63 | then("should be true") { 64 | unwrapResult shouldBe true 65 | } 66 | } 67 | } 68 | 69 | given("An empty list") { 70 | val wrappedValue = emptyList() 71 | `when`("unwrapped") { 72 | val unwrapResult = strategyImplementation.unwrapValueAsBoolean(wrappedValue) 73 | then("should be false") { 74 | unwrapResult shouldBe false 75 | } 76 | } 77 | } 78 | 79 | given("Any non-zero number") { 80 | val wrappedValue = -1 81 | `when`("unwrapped") { 82 | val unwrapResult = strategyImplementation.unwrapValueAsBoolean(wrappedValue) 83 | then("should be true") { 84 | unwrapResult shouldBe true 85 | } 86 | } 87 | } 88 | 89 | given("A zero number") { 90 | val wrappedValue = 0 91 | `when`("unwrapped") { 92 | val unwrapResult = strategyImplementation.unwrapValueAsBoolean(wrappedValue) 93 | then("should be false") { 94 | unwrapResult shouldBe false 95 | } 96 | } 97 | } 98 | }) 99 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/operations/numeric/MaxTest.kt: -------------------------------------------------------------------------------- 1 | package operations.numeric 2 | 3 | import JsonLogicEngine 4 | import JsonLogicResult 5 | import TestInput 6 | import io.kotest.core.spec.style.FunSpec 7 | import io.kotest.datatest.withData 8 | import valueShouldBe 9 | 10 | class MaxTest : FunSpec({ 11 | val logicEngine = JsonLogicEngine.Builder().build() 12 | 13 | withData( 14 | nameFn = { input -> "Should evaluated ${input.expression} with given ${input.data} result in ${input.result}" }, 15 | ts = listOf( 16 | TestInput( 17 | expression = mapOf("max" to listOf(1, 2, 3)), 18 | result = JsonLogicResult.Success(3) 19 | ), 20 | TestInput( 21 | expression = mapOf("max" to listOf(1, 3, 3)), 22 | result = JsonLogicResult.Success(3) 23 | ), 24 | TestInput( 25 | expression = mapOf("max" to listOf("-1", -2, "-3")), 26 | result = JsonLogicResult.Success(-1) 27 | ), 28 | TestInput( 29 | expression = mapOf("max" to listOf(3, 2, 1)), 30 | result = JsonLogicResult.Success(3) 31 | ), 32 | TestInput( 33 | expression = mapOf("max" to listOf(1)), 34 | result = JsonLogicResult.Success(1) 35 | ), 36 | TestInput( 37 | expression = mapOf("max" to listOf(1, "2")), 38 | result = JsonLogicResult.Success(2) 39 | ), 40 | TestInput( 41 | expression = mapOf("max" to listOf(Int.MIN_VALUE - 1L, Int.MIN_VALUE, 0)), 42 | result = JsonLogicResult.Success(0) 43 | ), 44 | TestInput( 45 | expression = mapOf("max" to listOf(Int.MAX_VALUE + 1L, Int.MAX_VALUE, 0)), 46 | result = JsonLogicResult.Success(Int.MAX_VALUE + 1L) 47 | ), 48 | TestInput( 49 | expression = mapOf("max" to listOf(1, "banana")), 50 | result = JsonLogicResult.Failure.NullResult 51 | ), 52 | TestInput( 53 | expression = mapOf("max" to listOf(1, "banana", listOf(1, 2))), 54 | result = JsonLogicResult.Failure.NullResult 55 | ), 56 | ) 57 | // given 58 | ) { testInput: TestInput -> 59 | // when 60 | val evaluationResult = logicEngine.evaluate(testInput.expression, testInput.data) 61 | 62 | // then 63 | evaluationResult valueShouldBe testInput.result 64 | } 65 | }) 66 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/operations/numeric/MinTest.kt: -------------------------------------------------------------------------------- 1 | package operations.numeric 2 | 3 | import JsonLogicEngine 4 | import JsonLogicResult 5 | import TestInput 6 | import io.kotest.core.spec.style.FunSpec 7 | import io.kotest.datatest.withData 8 | import valueShouldBe 9 | 10 | class MinTest : FunSpec({ 11 | val logicEngine = JsonLogicEngine.Builder().build() 12 | 13 | withData( 14 | nameFn = { input -> "Should evaluated ${input.expression} with given ${input.data} result in ${input.result}" }, 15 | ts = listOf( 16 | TestInput( 17 | expression = mapOf("min" to listOf(1, 2, 3)), 18 | result = JsonLogicResult.Success(1) 19 | ), 20 | TestInput( 21 | expression = mapOf("min" to listOf("1", "0.2", 0.3)), 22 | result = JsonLogicResult.Success(0.2) 23 | ), 24 | TestInput( 25 | expression = mapOf("min" to listOf("-2", "0.2", 0.3)), 26 | result = JsonLogicResult.Success(-2) 27 | ), 28 | TestInput( 29 | expression = mapOf("min" to listOf(1, 3, 3)), 30 | result = JsonLogicResult.Success(1) 31 | ), 32 | TestInput( 33 | expression = mapOf("min" to listOf(3, 2, 1)), 34 | result = JsonLogicResult.Success(1) 35 | ), 36 | TestInput( 37 | expression = mapOf("min" to listOf(1)), 38 | result = JsonLogicResult.Success(1) 39 | ), 40 | TestInput( 41 | expression = mapOf("min" to listOf("1", 2)), 42 | result = JsonLogicResult.Success(1) 43 | ), 44 | TestInput( 45 | expression = mapOf("min" to listOf(Int.MIN_VALUE - 1L, Int.MIN_VALUE, 0)), 46 | result = JsonLogicResult.Success(Int.MIN_VALUE - 1L) 47 | ), 48 | TestInput( 49 | expression = mapOf("min" to listOf(Int.MAX_VALUE + 1L, Int.MAX_VALUE, 0)), 50 | result = JsonLogicResult.Success(0) 51 | ), 52 | TestInput( 53 | expression = mapOf("min" to listOf(1, "banana")), 54 | result = JsonLogicResult.Failure.NullResult 55 | ), 56 | TestInput( 57 | expression = mapOf("min" to listOf(1, "banana", listOf(1, 2))), 58 | result = JsonLogicResult.Failure.NullResult 59 | ), 60 | ) 61 | // given 62 | ) { testInput: TestInput -> 63 | // when 64 | val evaluationResult = logicEngine.evaluate(testInput.expression, testInput.data) 65 | 66 | // then 67 | evaluationResult valueShouldBe testInput.result 68 | } 69 | }) 70 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/operations/numeric/unwrap/StrictUnwrapStrategyTest.kt: -------------------------------------------------------------------------------- 1 | package operations.numeric.unwrap 2 | 3 | import io.kotest.core.spec.style.BehaviorSpec 4 | import io.kotest.matchers.collections.shouldHaveSize 5 | import io.kotest.matchers.shouldBe 6 | 7 | class StrictUnwrapStrategyTest : BehaviorSpec({ 8 | val strategyImplementation: StrictUnwrapStrategy = object : StrictUnwrapStrategy {} 9 | 10 | given("A false value") { 11 | val wrappedValue = listOf(false) 12 | `when`("unwrapped") { 13 | val unwrapResult = strategyImplementation.unwrapValue(wrappedValue) 14 | then("should be null") { 15 | unwrapResult shouldHaveSize 1 16 | unwrapResult.first() shouldBe null 17 | } 18 | } 19 | } 20 | 21 | given("A true value") { 22 | val wrappedValue = listOf(true) 23 | `when`("unwrapped") { 24 | val unwrapResult = strategyImplementation.unwrapValue(wrappedValue) 25 | then("should be null") { 26 | unwrapResult shouldHaveSize 1 27 | unwrapResult.first() shouldBe null 28 | } 29 | } 30 | } 31 | 32 | given("A null") { 33 | val wrappedValue = listOf(null) 34 | `when`("unwrapped") { 35 | val unwrapResult = strategyImplementation.unwrapValue(wrappedValue) 36 | then("should be null") { 37 | unwrapResult shouldHaveSize 1 38 | unwrapResult.first() shouldBe null 39 | } 40 | } 41 | } 42 | 43 | given("Any other value") { 44 | val wrappedValue = listOf(Pair("a", "apple")) 45 | `when`("unwrapped") { 46 | val unwrapResult = strategyImplementation.unwrapValue(wrappedValue) 47 | then("should return null") { 48 | unwrapResult shouldHaveSize 1 49 | unwrapResult.first() shouldBe null 50 | } 51 | } 52 | } 53 | 54 | given("A number string") { 55 | val wrappedValue = listOf("32.5") 56 | `when`("unwrapped") { 57 | val unwrapResult = strategyImplementation.unwrapValue(wrappedValue) 58 | then("should be equal to its number value") { 59 | unwrapResult shouldHaveSize 1 60 | unwrapResult.first() shouldBe 32.5 61 | } 62 | } 63 | } 64 | 65 | given("A not number string") { 66 | val wrappedValue = listOf("Not a number") 67 | `when`("unwrapped") { 68 | val unwrapResult = strategyImplementation.unwrapValue(wrappedValue) 69 | then("should be null") { 70 | unwrapResult shouldHaveSize 1 71 | unwrapResult.first() shouldBe null 72 | } 73 | } 74 | } 75 | 76 | given("An empty list") { 77 | val wrappedValue = listOf(emptyList()) 78 | `when`("unwrapped") { 79 | val unwrapResult = strategyImplementation.unwrapValue(wrappedValue) 80 | then("should be null") { 81 | unwrapResult shouldHaveSize 1 82 | unwrapResult.first() shouldBe null 83 | } 84 | } 85 | } 86 | 87 | given("A more than 2 element list") { 88 | val wrappedValue = listOf(listOf(1, 2, "Not a number")) 89 | `when`("unwrapped") { 90 | val unwrapResult = strategyImplementation.unwrapValue(wrappedValue) 91 | then("should be equal to its first element") { 92 | unwrapResult shouldHaveSize 1 93 | unwrapResult.first() shouldBe 1 94 | } 95 | } 96 | } 97 | 98 | given("A single element list") { 99 | val wrappedValue = listOf(listOf("20")) 100 | `when`("unwrapped") { 101 | val unwrapResult = strategyImplementation.unwrapValue(wrappedValue) 102 | then("should be equal to its element") { 103 | unwrapResult shouldHaveSize 1 104 | unwrapResult.first() shouldBe 20 105 | } 106 | } 107 | } 108 | 109 | given("A nested single element list") { 110 | val wrappedValue = listOf(listOf(listOf("20"))) 111 | `when`("unwrapped") { 112 | val unwrapResult = strategyImplementation.unwrapValue(wrappedValue) 113 | then("should be equal to its deepest element") { 114 | unwrapResult shouldHaveSize 1 115 | unwrapResult.first() shouldBe 20 116 | } 117 | } 118 | } 119 | }) 120 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/operations/string/StringUnwrapStrategyTest.kt: -------------------------------------------------------------------------------- 1 | package operations.string 2 | 3 | import io.kotest.core.spec.style.BehaviorSpec 4 | import io.kotest.matchers.collections.shouldHaveSize 5 | import io.kotest.matchers.shouldBe 6 | 7 | class StringUnwrapStrategyTest : BehaviorSpec({ 8 | val strategyImplementation: StringUnwrapStrategy = object : StringUnwrapStrategy {} 9 | 10 | given("A non-decimal double") { 11 | val wrappedValue = listOf(2.0) 12 | `when`("unwrapped") { 13 | val unwrapResult = strategyImplementation.unwrapValueAsString(wrappedValue) 14 | then("should be int string") { 15 | unwrapResult shouldHaveSize 1 16 | unwrapResult.first() shouldBe "2" 17 | } 18 | } 19 | } 20 | 21 | given("A double") { 22 | val wrappedValue = listOf(2.51) 23 | `when`("unwrapped") { 24 | val unwrapResult = strategyImplementation.unwrapValueAsString(wrappedValue) 25 | then("should be double string") { 26 | unwrapResult shouldHaveSize 1 27 | unwrapResult.first() shouldBe "2.51" 28 | } 29 | } 30 | } 31 | given("An empty list") { 32 | val wrappedValue = emptyList() 33 | `when`("unwrapped") { 34 | val unwrapResult = strategyImplementation.unwrapValueAsString(wrappedValue) 35 | then("should be empty list") { 36 | unwrapResult shouldBe emptyList() 37 | } 38 | } 39 | } 40 | 41 | given("A boolean value") { 42 | val wrappedValue = listOf(false) 43 | `when`("unwrapped") { 44 | val unwrapResult = strategyImplementation.unwrapValueAsString(wrappedValue) 45 | then("should be string") { 46 | unwrapResult shouldHaveSize 1 47 | unwrapResult.first() shouldBe "false" 48 | } 49 | } 50 | } 51 | 52 | given("A null") { 53 | val wrappedValue = listOf(null) 54 | `when`("unwrapped") { 55 | val unwrapResult = strategyImplementation.unwrapValueAsString(wrappedValue) 56 | then("should be empty string") { 57 | unwrapResult shouldHaveSize 1 58 | unwrapResult.first() shouldBe "" 59 | } 60 | } 61 | } 62 | 63 | given("A nested list items") { 64 | val wrappedValue = listOf(listOf(listOf("banana", "apple", "pineapple"))) 65 | `when`("unwrapped") { 66 | val unwrapResult = strategyImplementation.unwrapValueAsString(wrappedValue) 67 | then("should be separated with commas") { 68 | unwrapResult shouldHaveSize 1 69 | unwrapResult.first() shouldBe "banana,apple,pineapple" 70 | } 71 | } 72 | } 73 | 74 | given("A nested lists items") { 75 | val wrappedValue = listOf(true, listOf(listOf("banana")), listOf(2.0)) 76 | `when`("unwrapped") { 77 | val unwrapResult = strategyImplementation.unwrapValueAsString(wrappedValue) 78 | then("should be flattened") { 79 | unwrapResult shouldHaveSize 3 80 | unwrapResult shouldBe listOf("true", "banana", "2") 81 | } 82 | } 83 | } 84 | }) 85 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | kotlin.mpp.stability.nowarn=true 3 | org.gradle.jvmargs=-Xmx2560m 4 | org.gradle.daemon=true 5 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | axion = "1.14.0" 3 | detekt = "1.19.0" 4 | nexus = "1.0.0" 5 | kotest = "5.5.4" 6 | crashkios = "0.4.0" 7 | kotlin = "1.8.0" 8 | 9 | [libraries] 10 | axion-release = { module = "pl.allegro.tech.build:axion-release-plugin", version.ref = "axion" } 11 | crashkios = { module = "co.touchlab:crashkios", version.ref = "crashkios" } 12 | kotest-assertions-core = {module = "io.kotest:kotest-assertions-core", version.ref = "kotest"} 13 | kotest-framework-engine = {module = "io.kotest:kotest-framework-engine", version.ref = "kotest"} 14 | kotest-framework-dataset = {module = "io.kotest:kotest-framework-datatest", version.ref = "kotest"} 15 | kotest-jvm-junit5-runner = {module = "io.kotest:kotest-runner-junit5-jvm", version.ref = "kotest"} 16 | 17 | [bundles] 18 | common-kotest = ["kotest-assertions-core", "kotest-framework-engine", "kotest-framework-dataset"] 19 | 20 | [plugins] 21 | detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } 22 | nexus = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "nexus" } 23 | kotest = { id = "io.kotest.multiplatform", version.ref = "kotest" } 24 | kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 25 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allegro/json-logic-kmp/026afe67f6dda91f2500663ff5f157603d58f29e/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /operations-api/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @Suppress("DSL_SCOPE_VIOLATION") // TODO remove on fix: https://youtrack.jetbrains.com/issue/KTIJ-19369 2 | plugins { 3 | alias(libs.plugins.kotlin.multiplatform) 4 | alias(libs.plugins.kotest) 5 | id(Conventions.junit) 6 | id(Conventions.publishing) 7 | } 8 | 9 | kotlin { 10 | jvm { 11 | mavenPublication{ setFullModuleArtifactId() } 12 | compilations.all { 13 | kotlinOptions.jvmTarget = JavaVersion.VERSION_11.majorVersion 14 | } 15 | } 16 | 17 | iosX64() 18 | iosArm64() 19 | iosSimulatorArm64() 20 | 21 | sourceSets { 22 | val commonMain by getting { 23 | dependencies { 24 | implementation(project(Modules.utils)) 25 | } 26 | } 27 | val commonTest by getting { 28 | dependencies { 29 | implementation(kotlin(Modules.kotlinTest)) 30 | implementation(libs.bundles.common.kotest) 31 | } 32 | } 33 | val jvmTest by getting { 34 | dependencies { 35 | implementation(libs.kotest.jvm.junit5.runner) 36 | } 37 | } 38 | val iosX64Main by getting 39 | val iosArm64Main by getting 40 | val iosSimulatorArm64Main by getting 41 | val iosMain by creating { 42 | dependsOn(commonMain) 43 | iosX64Main.dependsOn(this) 44 | iosArm64Main.dependsOn(this) 45 | iosSimulatorArm64Main.dependsOn(this) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /operations-api/src/commonMain/kotlin/JsonLogicException.kt: -------------------------------------------------------------------------------- 1 | class JsonLogicException(override val message: String) : Exception(message) 2 | -------------------------------------------------------------------------------- /operations-api/src/commonMain/kotlin/LogicEvaluator.kt: -------------------------------------------------------------------------------- 1 | interface LogicEvaluator { 2 | fun evaluateLogic(expression: Map, data: Any?): Any? 3 | } 4 | -------------------------------------------------------------------------------- /operations-api/src/commonMain/kotlin/operation/FunctionalLogicOperation.kt: -------------------------------------------------------------------------------- 1 | package operation 2 | 3 | import LogicEvaluator 4 | 5 | interface FunctionalLogicOperation { 6 | fun evaluateLogic(expression: Any?, data: Any?, evaluator: LogicEvaluator): Any? 7 | } 8 | -------------------------------------------------------------------------------- /operations-api/src/commonMain/kotlin/operation/StandardLogicOperation.kt: -------------------------------------------------------------------------------- 1 | package operation 2 | 3 | interface StandardLogicOperation { 4 | fun evaluateLogic(expression: Any?, data: Any?): Any? 5 | } 6 | -------------------------------------------------------------------------------- /operations-api/src/commonMain/kotlin/unwrap/EvaluatingUnwrapper.kt: -------------------------------------------------------------------------------- 1 | package unwrap 2 | 3 | import LogicEvaluator 4 | import utils.isExpression 5 | 6 | interface EvaluatingUnwrapper { 7 | fun unwrapDataByEvaluation(expression: List, data: Any?, evaluator: LogicEvaluator) = 8 | (expression.firstOrNull().unwrapOperationData(data, evaluator) as? List) 9 | 10 | @Suppress("UNCHECKED_CAST") 11 | private fun Any?.unwrapOperationData(data: Any?, evaluator: LogicEvaluator): Any? = when { 12 | this is List<*> -> map { it.unwrapOperationData(data, evaluator) } 13 | isExpression() -> evaluator.evaluateLogic(this as Map, data) 14 | else -> this 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /operations-api/src/commonTest/kotlin/unwrap/EvaluatingUnwrapperTest.kt: -------------------------------------------------------------------------------- 1 | package unwrap 2 | 3 | import LogicEvaluator 4 | import io.kotest.core.spec.style.BehaviorSpec 5 | import io.kotest.matchers.shouldBe 6 | 7 | class EvaluatingUnwrapperTest : BehaviorSpec({ 8 | val strategy: EvaluatingUnwrapper = object : EvaluatingUnwrapper {} 9 | var logicEvaluator: LogicEvaluator = object : LogicEvaluator { 10 | override fun evaluateLogic(expression: Map, data: Any?): Any? { 11 | return data 12 | } 13 | } 14 | 15 | given("A simple values input") { 16 | val wrappedValue = listOf(listOf(1, 2, 3)) 17 | `when`("unwrapped") { 18 | val unwrapResult = strategy.unwrapDataByEvaluation(wrappedValue, null, logicEvaluator) 19 | then("should be extracted") { 20 | unwrapResult shouldBe listOf(1, 2, 3) 21 | } 22 | } 23 | } 24 | 25 | given("A nested values input") { 26 | val wrappedValue = listOf(listOf(1, 2, 3, listOf(listOf("banana")))) 27 | `when`("unwrapped") { 28 | val unwrapResult = strategy.unwrapDataByEvaluation(wrappedValue, null, logicEvaluator) 29 | then("should be extracted") { 30 | unwrapResult shouldBe listOf(1, 2, 3, listOf(listOf("banana"))) 31 | } 32 | } 33 | } 34 | 35 | given("An input with operation") { 36 | val wrappedValue = listOf(listOf(1, 2, 3, mapOf("var" to "integers"))) 37 | val operationData = mapOf("integers" to listOf(4, 5)) 38 | logicEvaluator = object : LogicEvaluator { 39 | override fun evaluateLogic(expression: Map, data: Any?): Any? { 40 | return listOf(4, 5) 41 | } 42 | } 43 | `when`("unwrapped") { 44 | val unwrapResult = strategy.unwrapDataByEvaluation(wrappedValue, operationData, logicEvaluator) 45 | then("should be evaluated and extracted") { 46 | unwrapResult shouldBe listOf(1, 2, 3, listOf(4, 5)) 47 | } 48 | } 49 | } 50 | }) 51 | -------------------------------------------------------------------------------- /operations-stdlib/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @Suppress("DSL_SCOPE_VIOLATION") // TODO remove on fix: https://youtrack.jetbrains.com/issue/KTIJ-19369 2 | plugins { 3 | alias(libs.plugins.kotlin.multiplatform) 4 | alias(libs.plugins.kotest) 5 | id(Conventions.junit) 6 | id(Conventions.publishing) 7 | } 8 | 9 | kotlin { 10 | jvm { 11 | mavenPublication { setFullModuleArtifactId() } 12 | compilations.all { 13 | kotlinOptions.jvmTarget = JavaVersion.VERSION_11.majorVersion 14 | } 15 | } 16 | 17 | iosX64() 18 | iosArm64() 19 | iosSimulatorArm64() 20 | 21 | sourceSets { 22 | val commonMain by getting { 23 | dependencies { 24 | implementation(project(Modules.operationsApi)) 25 | implementation(project(Modules.utils)) 26 | implementation(External.dateTime) 27 | } 28 | } 29 | val commonTest by getting { 30 | dependencies { 31 | implementation(kotlin(Modules.kotlinTest)) 32 | implementation(libs.bundles.common.kotest) 33 | implementation(project(Modules.core)) 34 | implementation(project(Modules.utils)) 35 | } 36 | } 37 | val jvmMain by getting 38 | val jvmTest by getting { 39 | dependsOn(commonTest) 40 | dependencies { 41 | implementation(libs.kotest.jvm.junit5.runner) 42 | } 43 | } 44 | val iosX64Main by getting 45 | val iosX64Test by getting 46 | val iosArm64Main by getting 47 | val iosArm64Test by getting 48 | val iosSimulatorArm64Main by getting 49 | val iosSimulatorArm64Test by getting 50 | val iosMain by creating { 51 | dependsOn(commonMain) 52 | iosX64Main.dependsOn(this) 53 | iosArm64Main.dependsOn(this) 54 | iosSimulatorArm64Main.dependsOn(this) 55 | } 56 | val iosTest by creating { 57 | dependsOn(commonTest) 58 | iosX64Test.dependsOn(this) 59 | iosArm64Test.dependsOn(this) 60 | iosSimulatorArm64Test.dependsOn(this) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonMain/kotlin/CurrentTimeMillis.kt: -------------------------------------------------------------------------------- 1 | import operation.StandardLogicOperation 2 | 3 | expect object CurrentTimeMillis: StandardLogicOperation { 4 | override fun evaluateLogic(expression: Any?, data: Any?): Any 5 | } 6 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonMain/kotlin/Drop.kt: -------------------------------------------------------------------------------- 1 | import operation.StandardLogicOperation 2 | import utils.asList 3 | import utils.secondOrNull 4 | import utils.thirdOrNull 5 | 6 | object Drop : StandardLogicOperation { 7 | override fun evaluateLogic(expression: Any?, data: Any?): Any? = 8 | with(expression.asList) { 9 | val dropCandidate = firstOrNull() 10 | val count = secondOrNull() 11 | val mode = (thirdOrNull() as? String).toDropMode() 12 | 13 | (count as? Int)?.let { dropCandidate.dropElements(it, mode) } 14 | } 15 | 16 | private fun String?.toDropMode() = when (this) { 17 | "first" -> DropMode.First 18 | "last" -> DropMode.Last 19 | else -> DropMode.Unknown 20 | } 21 | 22 | private fun Any?.dropElements(count: Int, mode: DropMode) = 23 | when (this) { 24 | is String -> modeBasedDrop(mode = mode, first = { drop(count) }, last = { dropLast(count) }) 25 | is List<*> -> modeBasedDrop(mode = mode, first = { drop(count) }, last = { dropLast(count) }) 26 | else -> null 27 | } 28 | 29 | private fun modeBasedDrop(mode: DropMode, first: (() -> Any?), last: (() -> Any?)) = 30 | when (mode) { 31 | DropMode.First -> first() 32 | DropMode.Last -> last() 33 | DropMode.Unknown -> null 34 | } 35 | } 36 | 37 | private sealed class DropMode { 38 | object First : DropMode() 39 | object Last : DropMode() 40 | object Unknown : DropMode() 41 | } 42 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonMain/kotlin/OperationsProvider.kt: -------------------------------------------------------------------------------- 1 | import array.Distinct 2 | import array.Find 3 | import array.JoinToString 4 | import array.Size 5 | import array.Sort 6 | import encoding.Encode 7 | import format.DecimalFormat 8 | import operation.FunctionalLogicOperation 9 | import operation.StandardLogicOperation 10 | import string.Capitalize 11 | import string.compareToDate.CompareToDate 12 | import string.IsBlank 13 | import string.Length 14 | import string.Lowercase 15 | import string.Replace 16 | import string.ToArray 17 | import string.Trim 18 | import string.Uppercase 19 | import string.Match 20 | import string.Split 21 | 22 | object OperationsProvider { 23 | val standardOperations: Map = mutableMapOf( 24 | // string 25 | "capitalize" to Capitalize, 26 | "isBlank" to IsBlank, 27 | "length" to Length, 28 | "lowercase" to Lowercase, 29 | "replace" to Replace, 30 | "uppercase" to Uppercase, 31 | "toArray" to ToArray, 32 | "decimalFormat" to DecimalFormat, 33 | "encode" to Encode, 34 | "match" to Match, 35 | "compareToDate" to CompareToDate, 36 | "split" to Split, 37 | 38 | // time 39 | "currentTime" to CurrentTimeMillis, 40 | 41 | // array 42 | "size" to Size, 43 | "sort" to Sort, 44 | "distinct" to Distinct, 45 | "joinToString" to JoinToString, 46 | 47 | "drop" to Drop, 48 | "reverse" to Reverse, 49 | "trim" to Trim 50 | ) 51 | 52 | val functionalOperations: Map = mutableMapOf( 53 | "find" to Find, 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonMain/kotlin/Reverse.kt: -------------------------------------------------------------------------------- 1 | import operation.StandardLogicOperation 2 | 3 | object Reverse : StandardLogicOperation { 4 | override fun evaluateLogic(expression: Any?, data: Any?): Any? { 5 | return when (expression) { 6 | is String -> expression.reversed() 7 | is List<*> -> expression.reversed() 8 | else -> null 9 | 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonMain/kotlin/array/Distinct.kt: -------------------------------------------------------------------------------- 1 | package array 2 | 3 | import operation.StandardLogicOperation 4 | import utils.asList 5 | import utils.isSingleNullList 6 | 7 | object Distinct : StandardLogicOperation { 8 | override fun evaluateLogic(expression: Any?, data: Any?): Any? = (expression as? List<*>)?.distinct() 9 | } 10 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonMain/kotlin/array/Find.kt: -------------------------------------------------------------------------------- 1 | package array 2 | 3 | import LogicEvaluator 4 | import operation.FunctionalLogicOperation 5 | import unwrap.EvaluatingUnwrapper 6 | import utils.asList 7 | import utils.getMappingOperationOrNull 8 | 9 | object Find : FunctionalLogicOperation, EvaluatingUnwrapper { 10 | override fun evaluateLogic(expression: Any?, data: Any?, evaluator: LogicEvaluator): Any? { 11 | return expression.asList.let { expressionValues -> 12 | val inputData = unwrapDataByEvaluation(expressionValues, data, evaluator) 13 | val predicateOperation = expressionValues.getMappingOperationOrNull() 14 | 15 | predicateOperation?.let { 16 | inputData?.find { evaluator.evaluateLogic(predicateOperation, it) == true } 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonMain/kotlin/array/JoinToString.kt: -------------------------------------------------------------------------------- 1 | package array 2 | 3 | import operation.StandardLogicOperation 4 | import utils.asList 5 | 6 | object JoinToString : StandardLogicOperation { 7 | private const val ELEMENTS_ARG_INDEX = 0 8 | private const val SEPARATOR_ARG_INDEX = 1 9 | private const val PREFIX_ARG_INDEX = 2 10 | private const val POSTFIX_ARG_INDEX = 3 11 | private const val LIMIT_ARG_INDEX = 4 12 | private const val TRUNCATED_ARG_INDEX = 5 13 | 14 | override fun evaluateLogic(expression: Any?, data: Any?): Any? = expression.asList.toOperationArguments()?.join() 15 | 16 | private fun List.toOperationArguments(): JoinToStringArguments? = runCatching { 17 | checkLimitArg()?.let { limit -> 18 | JoinToStringArguments( 19 | elementsToJoin = get(ELEMENTS_ARG_INDEX).asList, 20 | separator = get(SEPARATOR_ARG_INDEX) as String, 21 | prefix = get(PREFIX_ARG_INDEX) as String, 22 | postfix = get(POSTFIX_ARG_INDEX) as String, 23 | limit = limit, 24 | truncated = get(TRUNCATED_ARG_INDEX) as String 25 | ) 26 | } 27 | }.fold( 28 | onSuccess = { it }, 29 | onFailure = { null } 30 | ) 31 | 32 | private fun List.checkLimitArg() = (get(LIMIT_ARG_INDEX) as Number).takeIf { 33 | it.toDouble() == it.toInt().toDouble() 34 | }?.toInt() 35 | 36 | private fun JoinToStringArguments.join() = 37 | elementsToJoin.joinToString(separator, prefix, postfix, limit, truncated) 38 | } 39 | 40 | private data class JoinToStringArguments( 41 | val elementsToJoin: List, 42 | val separator: String, 43 | val prefix: String, 44 | val postfix: String, 45 | val limit: Int, 46 | val truncated: String 47 | ) 48 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonMain/kotlin/array/Size.kt: -------------------------------------------------------------------------------- 1 | package array 2 | 3 | import operation.StandardLogicOperation 4 | import utils.asList 5 | import utils.isSingleNullList 6 | 7 | object Size : StandardLogicOperation { 8 | override fun evaluateLogic(expression: Any?, data: Any?): Any? = (expression as? List<*>)?.size 9 | } 10 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonMain/kotlin/array/Sort.kt: -------------------------------------------------------------------------------- 1 | package array 2 | 3 | import operation.StandardLogicOperation 4 | import utils.asDoubleList 5 | import utils.asList 6 | import utils.secondOrNull 7 | 8 | object Sort : StandardLogicOperation { 9 | override fun evaluateLogic(expression: Any?, data: Any?): Any? = 10 | with(expression.asList) { 11 | (firstOrNull() as? List<*>)?.let { elementsToSort -> 12 | val sortingMode = (secondOrNull() as? String).toSortOrder() 13 | elementsToSort.sortByMode(sortingMode) 14 | } 15 | } 16 | 17 | private fun String?.toSortOrder() = when (this) { 18 | "desc" -> SortOrder.Descending 19 | "asc" -> SortOrder.Ascending 20 | else -> SortOrder.Unknown 21 | } 22 | 23 | private fun List.sortByMode(sortingMode: SortOrder) = when { 24 | containsOnlyElementsOfType() -> this.castAndSortComparable(sortingMode) 25 | containsOnlyElementsOfType() -> this.castAndSortComparable(sortingMode) 26 | containsOnlyElementsOfType() -> asDoubleList.filterNotNull().sortComparable(sortingMode) 27 | else -> null 28 | } 29 | 30 | private inline fun List?.containsOnlyElementsOfType() = 31 | this?.filterIsInstance()?.size == this?.size 32 | 33 | @Suppress("UNCHECKED_CAST") 34 | private inline fun > List<*>.castAndSortComparable(sortingMode: SortOrder) = 35 | (this as? List)?.sortComparable(sortingMode) 36 | 37 | private inline fun > List.sortComparable(sortingMode: SortOrder) = 38 | modeBasedSort(sortingMode = sortingMode, ascSort = { sorted() }, descSort = { sortedDescending() }) 39 | 40 | private fun modeBasedSort(sortingMode: SortOrder, ascSort: (() -> Any?), descSort: (() -> Any?)) = 41 | when (sortingMode) { 42 | SortOrder.Descending -> descSort() 43 | SortOrder.Ascending -> ascSort() 44 | SortOrder.Unknown -> null 45 | } 46 | } 47 | 48 | private sealed class SortOrder { 49 | object Descending : SortOrder() 50 | object Ascending : SortOrder() 51 | object Unknown : SortOrder() 52 | } 53 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonMain/kotlin/encoding/Encode.kt: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import operation.StandardLogicOperation 4 | 5 | expect object Encode: StandardLogicOperation { 6 | override fun evaluateLogic(expression: Any?, data: Any?): Any? 7 | } 8 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonMain/kotlin/format/DecimalFormat.kt: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import operation.StandardLogicOperation 4 | 5 | expect object DecimalFormat : StandardLogicOperation, DecimalFormatter { 6 | override fun evaluateLogic(expression: Any?, data: Any?): Any? 7 | } 8 | 9 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonMain/kotlin/format/DecimalFormatter.kt: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import utils.asList 4 | import utils.secondOrNull 5 | 6 | internal interface DecimalFormatter { 7 | fun formatDecimal( 8 | expression: Any?, 9 | data: Any?, 10 | formatFloatingPoint: (format: String, arg: Double) -> String 11 | ): String? { 12 | return with(expression.asList) { 13 | val format = firstOrNull().toString() 14 | val formattedArgument = secondOrNull().toString() 15 | 16 | runCatching { format.formatAsFloatingDecimal(formattedArgument, formatFloatingPoint) } 17 | .fold( 18 | onSuccess = { it }, 19 | onFailure = { null } 20 | ) 21 | } 22 | } 23 | 24 | private fun String.formatAsFloatingDecimal( 25 | formattedArgument: String, 26 | formatFloatingPoint: (String, Double) -> String 27 | ) = if (matches("%[\\d|.]*[f]".toRegex())) { 28 | formattedArgument.toDoubleOrNull()?.let { 29 | formatFloatingPoint(this, it) 30 | } 31 | } else { 32 | null 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonMain/kotlin/string/Capitalize.kt: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import operation.StandardLogicOperation 4 | 5 | object Capitalize : StandardLogicOperation, StringUnwrapStrategy { 6 | override fun evaluateLogic(expression: Any?, data: Any?): Any? = 7 | unwrapValueAsString(expression)?.replaceFirstChar { it.uppercase() } 8 | } 9 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonMain/kotlin/string/IsBlank.kt: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import operation.StandardLogicOperation 4 | 5 | object IsBlank : StandardLogicOperation, StringUnwrapStrategy { 6 | override fun evaluateLogic(expression: Any?, data: Any?): Any? = unwrapValueAsString(expression)?.isBlank() 7 | } 8 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonMain/kotlin/string/Length.kt: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import operation.StandardLogicOperation 4 | 5 | object Length : StandardLogicOperation, StringUnwrapStrategy { 6 | override fun evaluateLogic(expression: Any?, data: Any?): Any? = unwrapValueAsString(expression)?.length 7 | } 8 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonMain/kotlin/string/Lowercase.kt: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import operation.StandardLogicOperation 4 | 5 | object Lowercase : StandardLogicOperation, StringUnwrapStrategy { 6 | override fun evaluateLogic(expression: Any?, data: Any?): Any? = unwrapValueAsString(expression)?.lowercase() 7 | } 8 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonMain/kotlin/string/Match.kt: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import operation.StandardLogicOperation 4 | import utils.asList 5 | 6 | object Match : StandardLogicOperation, StringUnwrapStrategy { 7 | private const val TEXT_ARG_INDEX = 0 8 | private const val REGEX_PATTERN_ARG_INDEX = 1 9 | private const val REGEX_OPTIONS_ARG_INDEX = 2 10 | 11 | override fun evaluateLogic(expression: Any?, data: Any?): Any? = 12 | expression.asList.toOperationArguments()?.invokeRegex() 13 | 14 | private fun List.toOperationArguments(): MatchArguments? = runCatching { 15 | MatchArguments( 16 | text = get(TEXT_ARG_INDEX) as String, 17 | regexPattern = get(REGEX_PATTERN_ARG_INDEX) as String, 18 | regexOptions = get(REGEX_OPTIONS_ARG_INDEX) as List 19 | ) 20 | }.fold( 21 | onSuccess = { it }, 22 | onFailure = { null } 23 | ) 24 | 25 | private fun convertArrayToRegexOptions(options: List): Set { 26 | return options.map { RegexOption.valueOf(it as String) }.toSet() 27 | } 28 | 29 | private fun matchBasic(regexPattern: String, text: String): Boolean = regexPattern.toRegex().matches(text) 30 | 31 | private fun matchWithOptions(options: List, regexPattern: String, text: String): Boolean { 32 | val convertedOptions = convertArrayToRegexOptions(options) 33 | val regex = regexPattern.toRegex(convertedOptions) 34 | return if (convertedOptions.any { it == RegexOption.MULTILINE }) { 35 | val splittedString = text.split("\n") 36 | return splittedString.all { regex.matches(it) } 37 | } else { 38 | regex.matches(text) 39 | } 40 | } 41 | 42 | private fun MatchArguments.invokeRegex() = if (regexOptions.isNullOrEmpty()) { 43 | matchBasic(regexPattern, text) 44 | } else { 45 | matchWithOptions(regexOptions, regexPattern, text) 46 | } 47 | } 48 | 49 | private data class MatchArguments( 50 | val text: String, 51 | val regexPattern: String, 52 | val regexOptions: List, 53 | ) 54 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonMain/kotlin/string/Replace.kt: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import operation.StandardLogicOperation 4 | import kotlin.runCatching 5 | import utils.asList 6 | 7 | object Replace: StandardLogicOperation, StringUnwrapStrategy { 8 | private const val REPLACE_CANDIDATE_INDEX = 0 9 | private const val OLD_STRING_INDEX = 1 10 | private const val NEW_STRING_INDEX = 2 11 | private const val MODE_INDEX = 3 12 | 13 | override fun evaluateLogic(expression: Any?, data: Any?): Any? = 14 | expression.asList.runCatching { 15 | val replaceData = ReplaceData( 16 | get(REPLACE_CANDIDATE_INDEX) as String, 17 | get(OLD_STRING_INDEX) as String, 18 | get(NEW_STRING_INDEX) as String, 19 | ) 20 | val mode = ReplaceMode.from(get(MODE_INDEX) as String, replaceData) 21 | 22 | mode() 23 | }.fold( 24 | onSuccess = { it }, 25 | onFailure = { null } 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonMain/kotlin/string/ReplaceMode.kt: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | internal data class ReplaceData( 4 | val replaceCandidate: String, 5 | val oldString: String, 6 | val newString: String, 7 | ) 8 | 9 | internal sealed class ReplaceMode: ()->Any? { 10 | abstract val replaceData: ReplaceData 11 | 12 | companion object { 13 | fun from(mode: String, replaceData: ReplaceData) = when { 14 | mode == AllReplace.name -> { 15 | AllReplace(replaceData) 16 | } 17 | mode.toIntOrNull() != null -> { 18 | FewReplace(replaceData, mode.toInt()) 19 | } 20 | else -> throw IllegalArgumentException(mode) 21 | } 22 | } 23 | } 24 | 25 | private class AllReplace(override val replaceData: ReplaceData) : ReplaceMode() { 26 | override fun invoke() = 27 | replaceData.replaceCandidate.replace(replaceData.oldString, replaceData.newString) 28 | 29 | companion object { 30 | const val name = "all" 31 | } 32 | 33 | } 34 | 35 | private class FewReplace(override val replaceData: ReplaceData, val times: Int):ReplaceMode() { 36 | override fun invoke() = 37 | replaceData.replaceCandidate.replace(replaceData.oldString, replaceData.newString, times) 38 | } 39 | 40 | private fun String.replace(oldValue:String, newValue:String, times: Int) : String { 41 | return if (times > 0) { 42 | replaceFirst(oldValue, newValue) 43 | .replace(oldValue, newValue, times - 1) 44 | } else { 45 | this 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonMain/kotlin/string/Split.kt: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import operation.StandardLogicOperation 4 | import utils.asList 5 | 6 | object Split : StandardLogicOperation { 7 | private const val TEXT_ARG_INDEX = 0 8 | private const val DELIMITERS_ARG_INDEX = 1 9 | 10 | override fun evaluateLogic(expression: Any?, data: Any?): Any? = 11 | expression.asList.toOperationArguments()?.invokeSplit() 12 | 13 | private fun List.toOperationArguments(): SplitArguments? = runCatching { 14 | SplitArguments( 15 | text = get(TEXT_ARG_INDEX) as String, 16 | delimiters = get(DELIMITERS_ARG_INDEX) as List 17 | ) 18 | }.fold( 19 | onSuccess = { it }, 20 | onFailure = { null } 21 | ) 22 | 23 | private fun SplitArguments.invokeSplit(): List { 24 | val convertedDelimiters = delimiters 25 | .map { it as String } 26 | .toSet() 27 | .toTypedArray() 28 | return text.split(delimiters = convertedDelimiters) 29 | } 30 | } 31 | 32 | private data class SplitArguments( 33 | var text: String, 34 | val delimiters: List 35 | ) 36 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonMain/kotlin/string/StringUnwrapStrategy.kt: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import utils.asList 4 | 5 | internal interface StringUnwrapStrategy { 6 | fun unwrapValueAsString(wrappedValue: Any?): String? = 7 | with(wrappedValue.asList) { 8 | if (size <= 1) { 9 | (firstOrNull() as? String) 10 | } else null 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonMain/kotlin/string/ToArray.kt: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import operation.StandardLogicOperation 4 | import utils.asList 5 | 6 | object ToArray : StandardLogicOperation { 7 | override fun evaluateLogic(expression: Any?, data: Any?): Any? = 8 | (expression.asList.firstOrNull() as? String)?.split("")?.drop(1)?.dropLast(1) 9 | } 10 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonMain/kotlin/string/Trim.kt: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import operation.StandardLogicOperation 4 | import utils.asList 5 | 6 | object Trim : StandardLogicOperation, StringUnwrapStrategy { 7 | private const val TEXT_ARG_INDEX = 0 8 | private const val CHAR_ARG_INDEX = 1 9 | private const val MODE_ARG_INDEX = 2 10 | 11 | override fun evaluateLogic(expression: Any?, data: Any?): Any? = 12 | expression.asList.toOperationArguments()?.invokeTrim() 13 | 14 | private fun List.toOperationArguments(): TrimArguments? = runCatching { 15 | TrimArguments( 16 | text = get(TEXT_ARG_INDEX) as String, 17 | char = (get(CHAR_ARG_INDEX) as String).single(), 18 | mode = (get(MODE_ARG_INDEX) as String).toTrimMode() 19 | ) 20 | }.fold( 21 | onSuccess = { it }, 22 | onFailure = { null } 23 | ) 24 | 25 | private fun String?.toTrimMode() = when (this) { 26 | "start" -> TrimMode.Start 27 | "end" -> TrimMode.End 28 | "bothEnds" -> TrimMode.BothEnds 29 | else -> throw IllegalStateException("Invalid TrimMode value") 30 | } 31 | 32 | private fun TrimArguments.invokeTrim() = when (mode) { 33 | TrimMode.Start -> this.text.trimStart(char) 34 | TrimMode.End -> this.text.trimEnd(char) 35 | TrimMode.BothEnds -> this.text.trim(char) 36 | } 37 | } 38 | 39 | private data class TrimArguments( 40 | val text: String, 41 | val char: Char, 42 | val mode: TrimMode 43 | ) 44 | 45 | private sealed class TrimMode { 46 | object Start : TrimMode() 47 | object End : TrimMode() 48 | object BothEnds : TrimMode() 49 | } 50 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonMain/kotlin/string/Uppercase.kt: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import operation.StandardLogicOperation 4 | 5 | object Uppercase : StandardLogicOperation, StringUnwrapStrategy { 6 | override fun evaluateLogic(expression: Any?, data: Any?): Any? = unwrapValueAsString(expression)?.uppercase() 7 | } 8 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonMain/kotlin/string/compareToDate/ComparePrecision.kt: -------------------------------------------------------------------------------- 1 | package string.compareToDate 2 | 3 | enum class ComparePrecision { 4 | MILLISECOND, SECOND, MINUTE, HOUR, DAY, MONTH, YEAR 5 | } 6 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonMain/kotlin/string/compareToDate/ComparePrecisionDateFormatter.kt: -------------------------------------------------------------------------------- 1 | package string.compareToDate 2 | 3 | import kotlinx.datetime.Instant 4 | 5 | class ComparePrecisionDateFormatter { 6 | fun formatDate(isoDate: String, precision: ComparePrecision): Instant { 7 | return Instant.parse( 8 | isoDate.substring(0, toPosition(precision)) + toDateSuffix(precision) 9 | ) 10 | } 11 | 12 | private fun toDateSuffix(precision: ComparePrecision): String { 13 | return when (precision) { 14 | ComparePrecision.MILLISECOND -> empty 15 | ComparePrecision.SECOND -> secondsPrecision 16 | ComparePrecision.MINUTE -> minutesPrecision 17 | ComparePrecision.HOUR -> hoursPrecision 18 | ComparePrecision.DAY -> daysPrecision 19 | ComparePrecision.MONTH -> monthsPrecision 20 | ComparePrecision.YEAR -> yearsPrecision 21 | } 22 | } 23 | 24 | private fun toPosition(precision: ComparePrecision): Int { 25 | return when (precision) { 26 | ComparePrecision.MILLISECOND -> dateLength 27 | ComparePrecision.SECOND -> secondPosition 28 | ComparePrecision.MINUTE -> minutePosition 29 | ComparePrecision.HOUR -> hourPosition 30 | ComparePrecision.DAY -> dayPosition 31 | ComparePrecision.MONTH -> monthPosition 32 | ComparePrecision.YEAR -> yearPosition 33 | } 34 | } 35 | 36 | companion object { 37 | private const val secondPosition = 19 38 | private const val minutePosition = 16 39 | private const val hourPosition = 13 40 | private const val dayPosition = 10 41 | private const val monthPosition = 7 42 | private const val yearPosition = 4 43 | private const val dateLength = 24 44 | 45 | private const val empty = "" 46 | private const val secondsPrecision = ".001Z" 47 | private const val minutesPrecision = ":00$secondsPrecision" 48 | private const val hoursPrecision = ":00$minutesPrecision" 49 | 50 | private const val daysPrecision = "T00$hoursPrecision" 51 | private const val monthsPrecision = "-01$daysPrecision" 52 | private const val yearsPrecision = "-01$monthsPrecision" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonMain/kotlin/string/compareToDate/CompareToDate.kt: -------------------------------------------------------------------------------- 1 | package string.compareToDate 2 | 3 | import kotlinx.datetime.Instant 4 | import operation.StandardLogicOperation 5 | import string.StringUnwrapStrategy 6 | import utils.asList 7 | 8 | object CompareToDate : StandardLogicOperation, StringUnwrapStrategy { 9 | 10 | private const val DATE_INDEX = 0 11 | private const val COMPARING_DATE_INDEX = 1 12 | private const val PRECISION_COMPARING_INDEX = 2 13 | 14 | private val formatter = ComparePrecisionDateFormatter() 15 | override fun evaluateLogic(expression: Any?, data: Any?): Any? = 16 | expression.asList.toDateComparator()?.invokeCompare() 17 | 18 | private fun List.toDateComparator(): List? = runCatching { 19 | ComparePrecision.valueOf(get(PRECISION_COMPARING_INDEX) as String).let { precision -> 20 | listOf( 21 | formatter.formatDate((get(DATE_INDEX) as String), precision), 22 | formatter.formatDate((get(COMPARING_DATE_INDEX) as String), precision) 23 | ) 24 | } 25 | }.getOrNull() 26 | 27 | private fun List.invokeCompare(): Int? = runCatching { 28 | first().compareTo(last()) 29 | }.onSuccess { 30 | return when { 31 | it > 0 -> 1 32 | it < 0 -> -1 33 | else -> 0 34 | } 35 | }.getOrNull() 36 | } 37 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonTest/kotlin/ReverseTest.kt: -------------------------------------------------------------------------------- 1 | 2 | import JsonLogicResult.Failure 3 | import JsonLogicResult.Success 4 | import io.kotest.core.spec.style.FunSpec 5 | import io.kotest.datatest.withData 6 | import io.kotest.matchers.shouldBe 7 | 8 | class ReverseTest : FunSpec({ 9 | val operatorName = "reverse" 10 | val logicEngine = JsonLogicEngine.Builder() 11 | .addStandardOperation(operatorName, Reverse) 12 | .build() 13 | 14 | withData( 15 | nameFn = { input -> "Should evaluated ${input.expression} with given ${input.data} result in ${input.result}" }, 16 | ts = listOf( 17 | TestInput( 18 | expression = mapOf(operatorName to "apple"), 19 | result = Success("elppa") 20 | ), 21 | TestInput( 22 | expression = mapOf(operatorName to listOf("apple")), 23 | result = Success(listOf("apple")) 24 | ), 25 | TestInput( 26 | expression = mapOf(operatorName to "12345"), 27 | result = Success("54321") 28 | ), 29 | TestInput( 30 | expression = mapOf(operatorName to listOf(listOf(1, 2, 3, 4, 5))), 31 | result = Success(listOf(listOf(1, 2, 3, 4, 5))) 32 | ), 33 | TestInput( 34 | expression = mapOf(operatorName to listOf(listOf("element1", "element2", "element3"))), 35 | result = Success(listOf(listOf("element1", "element2", "element3"))) 36 | ), 37 | TestInput( 38 | expression = mapOf(operatorName to listOf("element1", "element2", "element3")), 39 | result = Success(listOf("element3", "element2", "element1")) 40 | ), 41 | TestInput( 42 | expression = mapOf(operatorName to mapOf("var" to "key")), 43 | data = mapOf("key" to listOf(1, 2, 3, 4, 5)), 44 | result = Success(listOf(5,4,3,2,1)) 45 | ), 46 | TestInput( 47 | expression = mapOf(operatorName to mapOf("var" to "key")), 48 | data = mapOf("key" to "12345"), 49 | result = Success("54321") 50 | ), 51 | TestInput( 52 | expression = mapOf(operatorName to mapOf("var" to "key")), 53 | result = Failure.NullResult 54 | ), 55 | TestInput( 56 | expression = mapOf(operatorName to 1.3), 57 | result = Failure.NullResult 58 | ), 59 | TestInput( 60 | expression = mapOf(operatorName to null), 61 | result = Failure.NullResult 62 | ), 63 | TestInput( 64 | expression = mapOf(operatorName to true), 65 | result = Failure.NullResult 66 | ), 67 | TestInput( 68 | expression = mapOf(operatorName to emptyList()), 69 | result = Success(emptyList()) 70 | ), 71 | ) 72 | // given 73 | ) { testInput: TestInput -> 74 | // when 75 | val evaluationResult = logicEngine.evaluate(testInput.expression, testInput.data) 76 | 77 | // then 78 | evaluationResult shouldBe testInput.result 79 | } 80 | }) 81 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonTest/kotlin/TestInput.kt: -------------------------------------------------------------------------------- 1 | class TestInput( 2 | val expression: Map, 3 | val data: Any? = emptyMap(), 4 | val result: JsonLogicResult 5 | ) 6 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonTest/kotlin/string/CapitalizeTest.kt: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import JsonLogicResult.Failure 4 | import JsonLogicResult.Success 5 | import TestInput 6 | import io.kotest.core.spec.style.FunSpec 7 | import io.kotest.datatest.withData 8 | import io.kotest.matchers.shouldBe 9 | 10 | class CapitalizeTest : FunSpec({ 11 | val operatorName = "capitalize" 12 | val logicEngine = JsonLogicEngine.Builder().addStandardOperation(operatorName, Capitalize).build() 13 | 14 | withData( 15 | nameFn = { input -> "Should evaluated ${input.expression} with given ${input.data} result in ${input.result}" }, 16 | ts = listOf( 17 | TestInput( 18 | expression = mapOf(operatorName to "banana"), 19 | result = Success("Banana") 20 | ), 21 | TestInput( 22 | expression = mapOf(operatorName to ""), 23 | result = Success("") 24 | ), 25 | TestInput( 26 | expression = mapOf(operatorName to " "), 27 | result = Success(" ") 28 | ), 29 | TestInput( 30 | expression = mapOf(operatorName to "123"), 31 | result = Success("123") 32 | ), 33 | TestInput( 34 | expression = mapOf(operatorName to "Test"), 35 | result = Success("Test") 36 | ), 37 | TestInput( 38 | expression = mapOf(operatorName to "TEST ME!"), 39 | result = Success("TEST ME!") 40 | ), 41 | TestInput( 42 | expression = mapOf(operatorName to mapOf("var" to "key")), 43 | data = mapOf("key" to "apple"), 44 | result = Success("Apple") 45 | ), 46 | TestInput( 47 | expression = mapOf(operatorName to mapOf("var" to "key")), 48 | result = Failure.NullResult 49 | ), 50 | TestInput( 51 | expression = mapOf(operatorName to 1.3), 52 | result = Failure.NullResult 53 | ), 54 | TestInput( 55 | expression = mapOf(operatorName to null), 56 | result = Failure.NullResult 57 | ), 58 | TestInput( 59 | expression = mapOf(operatorName to true), 60 | result = Failure.NullResult 61 | ), 62 | TestInput( 63 | expression = mapOf(operatorName to emptyList()), 64 | result = Failure.NullResult 65 | ), 66 | ) 67 | // given 68 | ) { testInput: TestInput -> 69 | // when 70 | val evaluationResult = logicEngine.evaluate(testInput.expression, testInput.data) 71 | 72 | // then 73 | evaluationResult shouldBe testInput.result 74 | } 75 | }) 76 | 77 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonTest/kotlin/string/IsBlankTest.kt: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import JsonLogicEngine 4 | import JsonLogicResult.Failure 5 | import JsonLogicResult.Success 6 | import TestInput 7 | import io.kotest.core.spec.style.FunSpec 8 | import io.kotest.datatest.withData 9 | import io.kotest.matchers.shouldBe 10 | 11 | class IsBlankTest : FunSpec({ 12 | val operatorName = "isBlank" 13 | val logicEngine = JsonLogicEngine.Builder() 14 | .addStandardOperation(operatorName, IsBlank) 15 | .build() 16 | 17 | withData( 18 | nameFn = { input -> "Should evaluated ${input.expression} with given ${input.data} result in ${input.result}" }, 19 | ts = listOf( 20 | TestInput( 21 | expression = mapOf(operatorName to "apple"), 22 | result = Success(false) 23 | ), 24 | TestInput( 25 | expression = mapOf(operatorName to "12345"), 26 | result = Success(false) 27 | ), 28 | TestInput( 29 | expression = mapOf(operatorName to ""), 30 | result = Success(true) 31 | ), 32 | TestInput( 33 | expression = mapOf(operatorName to " "), 34 | result = Success(true) 35 | ), 36 | TestInput( 37 | expression = mapOf(operatorName to " apple"), 38 | result = Success(false) 39 | ), 40 | TestInput( 41 | expression = mapOf(operatorName to "apple "), 42 | result = Success(false) 43 | ), 44 | TestInput( 45 | expression = mapOf(operatorName to mapOf("var" to "key")), 46 | data = mapOf("key" to "APPLE"), 47 | result = Success(false) 48 | ), 49 | TestInput( 50 | expression = mapOf(operatorName to mapOf("var" to "key")), 51 | data = mapOf("key" to ""), 52 | result = Success(true) 53 | ), 54 | TestInput( 55 | expression = mapOf(operatorName to mapOf("var" to "key")), 56 | result = Failure.NullResult 57 | ), 58 | TestInput( 59 | expression = mapOf(operatorName to 1.3), 60 | result = Failure.NullResult 61 | ), 62 | TestInput( 63 | expression = mapOf(operatorName to null), 64 | result = Failure.NullResult 65 | ), 66 | TestInput( 67 | expression = mapOf(operatorName to true), 68 | result = Failure.NullResult 69 | ), 70 | TestInput( 71 | expression = mapOf(operatorName to emptyList()), 72 | result = Failure.NullResult 73 | ), 74 | ) 75 | // given 76 | ) { testInput: TestInput -> 77 | // when 78 | val evaluationResult = logicEngine.evaluate(testInput.expression, testInput.data) 79 | 80 | // then 81 | evaluationResult shouldBe testInput.result 82 | } 83 | }) 84 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonTest/kotlin/string/LengthTest.kt: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import JsonLogicEngine 4 | import TestInput 5 | import io.kotest.core.spec.style.FunSpec 6 | import io.kotest.datatest.withData 7 | import io.kotest.matchers.shouldBe 8 | 9 | class LengthTest : FunSpec({ 10 | val logicEngine = JsonLogicEngine.Builder() 11 | .addStandardOperation("length", Length).build() 12 | // given 13 | withData( 14 | nameFn = { input -> "Should evaluated ${input.expression} with given ${input.data} result in ${input.result}" }, 15 | ts = listOf( 16 | TestInput( 17 | expression = mapOf("length" to "test"), 18 | result = JsonLogicResult.Success(4) 19 | ), 20 | TestInput( 21 | expression = mapOf( 22 | "length" to mapOf("var" to "fruits") 23 | ), 24 | data = mapOf("fruits" to listOf("apple")), 25 | result = JsonLogicResult.Success(5) 26 | ), 27 | TestInput( 28 | expression = mapOf("length" to ""), 29 | result = JsonLogicResult.Success(0) 30 | ), 31 | TestInput( 32 | expression = mapOf( 33 | "length" to "this is very very very very very very very very very very very " + 34 | " very very very very very very very very very very very very very very very very very" + 35 | " very very very very very very very very very very very very very very very very very" + 36 | " very very very very very very very very very very very very very very very long text" 37 | ), 38 | result = JsonLogicResult.Success(336) 39 | ), 40 | TestInput( 41 | expression = mapOf( 42 | "length" to mapOf( 43 | "if" to listOf( 44 | mapOf("<" to listOf(mapOf("var" to "temp"), 0)), "freezing" 45 | ) 46 | ) 47 | ), 48 | data = mapOf("temp" to -20), 49 | result = JsonLogicResult.Success(8) 50 | ), 51 | TestInput( 52 | expression = mapOf( 53 | "length" to mapOf( 54 | "if" to listOf( 55 | mapOf("<" to listOf(mapOf("var" to "temp"), 0)), "freezing", 56 | mapOf("<" to listOf(mapOf("var" to "temp"), 100)), "liquid", 57 | "gas" 58 | ) 59 | ) 60 | ), 61 | data = mapOf("temp" to 55), 62 | result = JsonLogicResult.Success(6) 63 | ), 64 | TestInput( 65 | expression = mapOf("length" to 123455), 66 | result = JsonLogicResult.Failure.NullResult 67 | ), 68 | TestInput( 69 | expression = mapOf("length" to -123455), 70 | result = JsonLogicResult.Failure.NullResult 71 | ), 72 | TestInput( 73 | expression = mapOf("length" to 0), 74 | result = JsonLogicResult.Failure.NullResult 75 | ), 76 | TestInput( 77 | expression = mapOf("length" to 10.0), 78 | result = JsonLogicResult.Failure.NullResult 79 | ), 80 | 81 | TestInput( 82 | expression = mapOf("length" to listOf("test", "test2")), 83 | result = JsonLogicResult.Failure.NullResult 84 | ), 85 | TestInput( 86 | expression = mapOf("length" to mapOf("test" to "test2")), 87 | result = JsonLogicResult.Failure.MissingOperation 88 | ), 89 | ) 90 | ) { testInput: TestInput -> 91 | // when 92 | val evaluationResult = logicEngine.evaluate(testInput.expression, testInput.data) 93 | // then 94 | evaluationResult shouldBe testInput.result 95 | } 96 | }) 97 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonTest/kotlin/string/LowercaseTest.kt: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import JsonLogicResult.Failure 4 | import JsonLogicResult.Success 5 | import TestInput 6 | import io.kotest.core.spec.style.FunSpec 7 | import io.kotest.datatest.withData 8 | import io.kotest.matchers.shouldBe 9 | 10 | class LowercaseTest : FunSpec({ 11 | val operatorName = "lowercase" 12 | val logicEngine = JsonLogicEngine.Builder().addStandardOperation(operatorName, Lowercase).build() 13 | 14 | withData( 15 | nameFn = { input -> "Should evaluated ${input.expression} with given ${input.data} result in ${input.result}" }, 16 | ts = listOf( 17 | TestInput( 18 | expression = mapOf(operatorName to "banana"), 19 | result = Success("banana") 20 | ), 21 | TestInput( 22 | expression = mapOf(operatorName to ""), 23 | result = Success("") 24 | ), 25 | TestInput( 26 | expression = mapOf(operatorName to " "), 27 | result = Success(" ") 28 | ), 29 | TestInput( 30 | expression = mapOf(operatorName to "123"), 31 | result = Success("123") 32 | ), 33 | TestInput( 34 | expression = mapOf(operatorName to "Test"), 35 | result = Success("test") 36 | ), 37 | TestInput( 38 | expression = mapOf(operatorName to "TEST ME!"), 39 | result = Success("test me!") 40 | ), 41 | TestInput( 42 | expression = mapOf(operatorName to mapOf("var" to "key")), 43 | data = mapOf("key" to "APPLE"), 44 | result = Success("apple") 45 | ), 46 | TestInput( 47 | expression = mapOf(operatorName to mapOf("var" to "key")), 48 | result = Failure.NullResult 49 | ), 50 | TestInput( 51 | expression = mapOf(operatorName to 1.3), 52 | result = Failure.NullResult 53 | ), 54 | TestInput( 55 | expression = mapOf(operatorName to null), 56 | result = Failure.NullResult 57 | ), 58 | TestInput( 59 | expression = mapOf(operatorName to true), 60 | result = Failure.NullResult 61 | ), 62 | TestInput( 63 | expression = mapOf(operatorName to emptyList()), 64 | result = Failure.NullResult 65 | ), 66 | ) 67 | // given 68 | ) { testInput: TestInput -> 69 | // when 70 | val evaluationResult = logicEngine.evaluate(testInput.expression, testInput.data) 71 | 72 | // then 73 | evaluationResult shouldBe testInput.result 74 | } 75 | }) 76 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonTest/kotlin/string/SplitTest.kt: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import TestInput 4 | import io.kotest.core.spec.style.FunSpec 5 | import io.kotest.datatest.withData 6 | import io.kotest.matchers.shouldBe 7 | import JsonLogicResult.Success 8 | import JsonLogicResult.Failure 9 | 10 | class SplitTest : FunSpec({ 11 | val operatorName = "split" 12 | val logicEngine = JsonLogicEngine.Builder().addStandardOperation(operatorName, Split).build() 13 | 14 | withData( 15 | nameFn = { input -> "Should evaluated ${input.expression} with given ${input.data} result in ${input.result}" }, 16 | ts = listOf( 17 | TestInput( 18 | expression = mapOf(operatorName to listOf("one,two,three", listOf(","))), 19 | result = Success(listOf("one", "two", "three")) 20 | ), 21 | TestInput( 22 | expression = mapOf(operatorName to listOf("one, two, three", listOf(","))), 23 | result = Success(listOf("one", " two", " three")) 24 | ), 25 | TestInput( 26 | expression = mapOf(operatorName to listOf("one, two, three", listOf(", "))), 27 | result = Success(listOf("one", "two", "three")) 28 | ), 29 | TestInput( 30 | expression = mapOf(operatorName to listOf("/location/to/file", listOf("/"))), 31 | result = Success(listOf("", "location", "to", "file")) 32 | ), 33 | TestInput( 34 | expression = mapOf(operatorName to listOf("location\\to\\file", listOf("\\"))), 35 | result = Success(listOf("location", "to", "file")) 36 | ), 37 | TestInput( 38 | expression = mapOf(operatorName to listOf("oneAtwoAthree", listOf("A"))), 39 | result = Success(listOf("one", "two", "three")) 40 | ), 41 | TestInput( 42 | expression = mapOf(operatorName to listOf("The quick brown fox jumps over the lazy dog", listOf(" "))), 43 | result = Success(listOf("The", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog")) 44 | ), 45 | TestInput( 46 | expression = mapOf(operatorName to listOf("https://domain.net/query/search", listOf("/"))), 47 | result = Success(listOf("https:", "", "domain.net", "query", "search")) 48 | ), 49 | TestInput( 50 | expression = mapOf(operatorName to listOf("Hello", listOf(""))), 51 | result = Success(listOf("", "H", "e", "l", "l", "o", "")) 52 | ), 53 | TestInput( 54 | expression = mapOf(operatorName to listOf("", listOf(""))), 55 | result = Success(listOf("", "")) 56 | ), 57 | TestInput( 58 | expression = mapOf(operatorName to listOf(null, null)), 59 | result = Failure.NullResult 60 | ), 61 | TestInput( 62 | expression = mapOf(operatorName to listOf(null, ",")), 63 | result = Failure.NullResult 64 | ), 65 | TestInput( 66 | expression = mapOf(operatorName to listOf("Hello", "")), 67 | result = Failure.NullResult 68 | ), 69 | TestInput( 70 | expression = mapOf(operatorName to listOf("Hello", 1)), 71 | result = Failure.NullResult 72 | ), 73 | TestInput( 74 | expression = mapOf(operatorName to listOf("Hello", true)), 75 | result = Failure.NullResult 76 | ), 77 | TestInput( 78 | expression = mapOf(operatorName to listOf("Hello", listOf(0, 1))), 79 | result = Failure.MissingOperation 80 | ), 81 | ) 82 | // given 83 | ) { testInput: TestInput -> 84 | // when 85 | val evaluationResult = logicEngine.evaluate(testInput.expression, testInput.data) 86 | 87 | // then 88 | evaluationResult shouldBe testInput.result 89 | } 90 | }) 91 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonTest/kotlin/string/StringUnwrapStrategyTest.kt: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import io.kotest.core.spec.style.BehaviorSpec 4 | import io.kotest.matchers.shouldBe 5 | 6 | class StringUnwrapStrategyTest : BehaviorSpec({ 7 | val strategyImplementation: StringUnwrapStrategy = object : StringUnwrapStrategy {} 8 | 9 | given("A list with one string") { 10 | val wrappedValue = listOf("test") 11 | `when`("unwrapped") { 12 | val unwrapResult = strategyImplementation.unwrapValueAsString(wrappedValue) 13 | then("should be string") { 14 | unwrapResult shouldBe "test" 15 | } 16 | } 17 | } 18 | 19 | given("A string") { 20 | val wrappedValue = "apple" 21 | `when`("unwrapped") { 22 | val unwrapResult = strategyImplementation.unwrapValueAsString(wrappedValue) 23 | then("should be string") { 24 | unwrapResult shouldBe "apple" 25 | } 26 | } 27 | } 28 | 29 | given("A list of strings") { 30 | val wrappedValue = listOf("apple", "banana") 31 | `when`("unwrapped") { 32 | val unwrapResult = strategyImplementation.unwrapValueAsString(wrappedValue) 33 | then("should not be unwrapped") { 34 | unwrapResult shouldBe null 35 | } 36 | } 37 | } 38 | 39 | given("A non-decimal double") { 40 | val wrappedValue = listOf(2.0) 41 | `when`("unwrapped") { 42 | val unwrapResult = strategyImplementation.unwrapValueAsString(wrappedValue) 43 | then("should not be unwrapped") { 44 | unwrapResult shouldBe null 45 | } 46 | } 47 | } 48 | 49 | given("A double") { 50 | val wrappedValue = listOf(2.51) 51 | `when`("unwrapped") { 52 | val unwrapResult = strategyImplementation.unwrapValueAsString(wrappedValue) 53 | then("should not be unwrapped") { 54 | unwrapResult shouldBe null 55 | } 56 | } 57 | } 58 | given("An empty list") { 59 | val wrappedValue = emptyList() 60 | `when`("unwrapped") { 61 | val unwrapResult = strategyImplementation.unwrapValueAsString(wrappedValue) 62 | then("should not be unwrapped") { 63 | unwrapResult shouldBe null 64 | } 65 | } 66 | } 67 | 68 | given("A boolean value") { 69 | val wrappedValue = listOf(false) 70 | `when`("unwrapped") { 71 | val unwrapResult = strategyImplementation.unwrapValueAsString(wrappedValue) 72 | then("should not be unwrapped") { 73 | unwrapResult shouldBe null 74 | } 75 | } 76 | } 77 | 78 | given("A null") { 79 | val wrappedValue = listOf(null) 80 | `when`("unwrapped") { 81 | val unwrapResult = strategyImplementation.unwrapValueAsString(wrappedValue) 82 | then("should not be unwrapped") { 83 | unwrapResult shouldBe null 84 | } 85 | } 86 | } 87 | 88 | given("A nested list items") { 89 | val wrappedValue = listOf(listOf(listOf("banana", "apple", "pineapple"))) 90 | `when`("unwrapped") { 91 | val unwrapResult = strategyImplementation.unwrapValueAsString(wrappedValue) 92 | then("should not be unwrapped") { 93 | unwrapResult shouldBe null 94 | } 95 | } 96 | } 97 | 98 | given("A nested lists items") { 99 | val wrappedValue = listOf(true, listOf(listOf("banana")), listOf(2.0)) 100 | `when`("unwrapped") { 101 | val unwrapResult = strategyImplementation.unwrapValueAsString(wrappedValue) 102 | then("should not be unwrapped") { 103 | unwrapResult shouldBe null 104 | } 105 | } 106 | } 107 | }) 108 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonTest/kotlin/string/ToArrayTest.kt: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import TestInput 4 | import io.kotest.core.spec.style.FunSpec 5 | import io.kotest.datatest.withData 6 | import io.kotest.matchers.shouldBe 7 | import JsonLogicResult.Success 8 | import JsonLogicResult.Failure 9 | 10 | class ToArrayTest : FunSpec({ 11 | val operatorName = "toArray" 12 | val logicEngine = JsonLogicEngine.Builder().addStandardOperation(operatorName, ToArray).build() 13 | 14 | withData( 15 | nameFn = { input -> "Should evaluated ${input.expression} with given ${input.data} result in ${input.result}" }, 16 | ts = listOf( 17 | TestInput( 18 | expression = mapOf(operatorName to "banana"), 19 | result = Success(listOf("b", "a", "n", "a", "n", "a")) 20 | ), 21 | TestInput( 22 | expression = mapOf(operatorName to listOf("banana")), 23 | result = Success(listOf("b", "a", "n", "a", "n", "a")) 24 | ), 25 | TestInput( 26 | expression = mapOf(operatorName to ""), 27 | result = Success(emptyList()) 28 | ), 29 | TestInput( 30 | expression = mapOf(operatorName to listOf("")), 31 | result = Success(emptyList()) 32 | ), 33 | TestInput( 34 | expression = mapOf(operatorName to " "), 35 | result = Success(listOf(" ")) 36 | ), 37 | TestInput( 38 | expression = mapOf(operatorName to listOf(" ")), 39 | result = Success(listOf(" ")) 40 | ), 41 | TestInput( 42 | expression = mapOf(operatorName to "123"), 43 | result = Success(listOf("1", "2", "3")) 44 | ), 45 | TestInput( 46 | expression = mapOf(operatorName to 123), 47 | result = Failure.NullResult 48 | ), 49 | TestInput( 50 | expression = mapOf(operatorName to mapOf("var" to "key")), 51 | data = mapOf("key" to "APPLE"), 52 | result = Success(listOf("A", "P", "P", "L", "E")) 53 | ), 54 | TestInput( 55 | expression = mapOf(operatorName to mapOf("var" to "key")), 56 | result = Failure.NullResult 57 | ), 58 | TestInput( 59 | expression = mapOf(operatorName to 1.3), 60 | result = Failure.NullResult 61 | ), 62 | TestInput( 63 | expression = mapOf(operatorName to listOf("0.00001")), 64 | result = Success(listOf("0", ".", "0", "0", "0", "0", "1")) 65 | ), 66 | TestInput( 67 | expression = mapOf(operatorName to null), 68 | result = Failure.NullResult 69 | ), 70 | TestInput( 71 | expression = mapOf(operatorName to listOf(null)), 72 | result = Failure.NullResult 73 | ), 74 | TestInput( 75 | expression = mapOf(operatorName to true), 76 | result = Failure.NullResult 77 | ), 78 | TestInput( 79 | expression = mapOf(operatorName to listOf(false)), 80 | result = Failure.NullResult 81 | ), 82 | TestInput( 83 | expression = mapOf(operatorName to emptyList()), 84 | result = Failure.NullResult 85 | ), 86 | TestInput( 87 | expression = mapOf(operatorName to listOf(emptyList())), 88 | result = Failure.NullResult 89 | ), 90 | ) 91 | // given 92 | ) { testInput: TestInput -> 93 | // when 94 | val evaluationResult = logicEngine.evaluate(testInput.expression, testInput.data) 95 | 96 | // then 97 | evaluationResult shouldBe testInput.result 98 | } 99 | }) 100 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonTest/kotlin/string/UppercaseTest.kt: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import JsonLogicResult.Failure 4 | import JsonLogicResult.Success 5 | import TestInput 6 | import io.kotest.core.spec.style.FunSpec 7 | import io.kotest.datatest.withData 8 | import io.kotest.matchers.shouldBe 9 | 10 | class UppercaseTest : FunSpec({ 11 | val operatorName = "uppercase" 12 | val logicEngine = JsonLogicEngine.Builder().addStandardOperation(operatorName, Uppercase).build() 13 | 14 | withData( 15 | nameFn = { input -> "Should evaluated ${input.expression} with given ${input.data} result in ${input.result}" }, 16 | ts = listOf( 17 | TestInput( 18 | expression = mapOf(operatorName to "banana"), 19 | result = Success("BANANA") 20 | ), 21 | TestInput( 22 | expression = mapOf(operatorName to ""), 23 | result = Success("") 24 | ), 25 | TestInput( 26 | expression = mapOf(operatorName to " "), 27 | result = Success(" ") 28 | ), 29 | TestInput( 30 | expression = mapOf(operatorName to "123"), 31 | result = Success("123") 32 | ), 33 | TestInput( 34 | expression = mapOf(operatorName to "Test"), 35 | result = Success("TEST") 36 | ), 37 | TestInput( 38 | expression = mapOf(operatorName to "TEST ME!"), 39 | result = Success("TEST ME!") 40 | ), 41 | TestInput( 42 | expression = mapOf(operatorName to mapOf("var" to "key")), 43 | data = mapOf("key" to "apple"), 44 | result = Success("APPLE") 45 | ), 46 | TestInput( 47 | expression = mapOf(operatorName to mapOf("var" to "key")), 48 | result = Failure.NullResult 49 | ), 50 | TestInput( 51 | expression = mapOf(operatorName to 1.3), 52 | result = Failure.NullResult 53 | ), 54 | TestInput( 55 | expression = mapOf(operatorName to null), 56 | result = Failure.NullResult 57 | ), 58 | TestInput( 59 | expression = mapOf(operatorName to true), 60 | result = Failure.NullResult 61 | ), 62 | TestInput( 63 | expression = mapOf(operatorName to emptyList()), 64 | result = Failure.NullResult 65 | ), 66 | ) 67 | // given 68 | ) { testInput: TestInput -> 69 | // when 70 | val evaluationResult = logicEngine.evaluate(testInput.expression, testInput.data) 71 | 72 | // then 73 | evaluationResult shouldBe testInput.result 74 | } 75 | }) 76 | -------------------------------------------------------------------------------- /operations-stdlib/src/commonTest/kotlin/string/compareToDate/ComparePrecisionDateFormatterTests.kt: -------------------------------------------------------------------------------- 1 | package string.compareToDate 2 | 3 | import io.kotest.core.spec.style.FunSpec 4 | import io.kotest.datatest.withData 5 | import io.kotest.matchers.shouldBe 6 | 7 | class ComparePrecisionDateFormatterTests : FunSpec({ 8 | val sut = ComparePrecisionDateFormatter() 9 | withData( 10 | nameFn = { input -> 11 | "Should evaluated ${input.precision} with given ${input.precision} result in ${input.result}" 12 | }, ts = listOf( 13 | ComparePrecisionDateFormatterTestInput( 14 | "2023-04-18T12:34:56.123Z", ComparePrecision.MILLISECOND, "2023-04-18T12:34:56.123Z" 15 | ), 16 | ComparePrecisionDateFormatterTestInput( 17 | "2023-04-18T12:34:56.123Z", ComparePrecision.SECOND, "2023-04-18T12:34:56.001Z" 18 | ), 19 | ComparePrecisionDateFormatterTestInput( 20 | "2023-04-18T12:34:56.123Z", ComparePrecision.MINUTE, "2023-04-18T12:34:00.001Z" 21 | ), 22 | ComparePrecisionDateFormatterTestInput( 23 | "2023-04-18T12:34:56.123Z", ComparePrecision.HOUR, "2023-04-18T12:00:00.001Z" 24 | ), 25 | ComparePrecisionDateFormatterTestInput( 26 | "2023-04-18T12:34:56.123Z", ComparePrecision.DAY, "2023-04-18T00:00:00.001Z" 27 | ), 28 | ComparePrecisionDateFormatterTestInput( 29 | "2023-04-18T12:34:56.123Z", ComparePrecision.MONTH, "2023-04-01T00:00:00.001Z" 30 | ), 31 | ComparePrecisionDateFormatterTestInput( 32 | "2023-04-18T12:34:56.123Z", ComparePrecision.YEAR, "2023-01-01T00:00:00.001Z" 33 | ), 34 | ) 35 | 36 | ) { testInput: ComparePrecisionDateFormatterTestInput -> 37 | val result = sut.formatDate(testInput.baseDate, testInput.precision) 38 | result.toString() shouldBe testInput.result 39 | } 40 | }) 41 | 42 | private class ComparePrecisionDateFormatterTestInput( 43 | val baseDate: String, val precision: ComparePrecision, val result: String 44 | ) 45 | -------------------------------------------------------------------------------- /operations-stdlib/src/iosMain/kotlin/CurrentTimeMillis.kt: -------------------------------------------------------------------------------- 1 | import operation.StandardLogicOperation 2 | import platform.Foundation.NSDate 3 | import platform.Foundation.timeIntervalSince1970 4 | 5 | actual object CurrentTimeMillis : StandardLogicOperation { 6 | private const val MILLIS_IN_SECOND: Int = 1_000 7 | 8 | actual override fun evaluateLogic(expression: Any?, data: Any?): Any { 9 | /* 10 | `timeIntervalSince1970()` return Double value in seconds with fraction part. 11 | We have to multiply this by 1000 to move fractions value 12 | */ 13 | val currentTimestampInMillis = { NSDate().timeIntervalSince1970() * MILLIS_IN_SECOND } 14 | 15 | // We have to convert Double to Long to remove fraction part. 16 | return currentTimestampInMillis().toLong() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /operations-stdlib/src/iosMain/kotlin/encoding/Encode.kt: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import operation.StandardLogicOperation 4 | import platform.Foundation.NSCharacterSet 5 | import string.StringUnwrapStrategy 6 | import platform.Foundation.NSString 7 | import platform.Foundation.stringByAddingPercentEncodingWithAllowedCharacters 8 | import platform.Foundation.create 9 | 10 | actual object Encode : StandardLogicOperation, StringUnwrapStrategy { 11 | actual override fun evaluateLogic(expression: Any?, data: Any?): Any? { 12 | return unwrapValueAsString(expression)?.let { 13 | NSString.create(string = it) 14 | .stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.alphanumericCharacterSet) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /operations-stdlib/src/iosMain/kotlin/format/DecimalFormat.kt: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import kotlinx.cinterop.cstr 4 | import operation.StandardLogicOperation 5 | import platform.Foundation.NSString 6 | import platform.Foundation.stringWithFormat 7 | 8 | actual object DecimalFormat : StandardLogicOperation, DecimalFormatter { 9 | actual override fun evaluateLogic(expression: Any?, data: Any?): Any? { 10 | return formatDecimal(expression, data) { format: String, arg: Double -> 11 | NSString.stringWithFormat(format, arg) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /operations-stdlib/src/iosTest/kotlin/CurrentTimeMillisTests.kt: -------------------------------------------------------------------------------- 1 | import io.kotest.core.spec.style.FunSpec 2 | import io.kotest.matchers.should 3 | import io.kotest.matchers.types.beInstanceOf 4 | 5 | class CurrentTimeMillisTests : FunSpec() { 6 | init { 7 | val operatorName = "currentTime" 8 | val logicEngine = JsonLogicEngine.Builder() 9 | .addStandardOperation(operatorName, CurrentTimeMillis) 10 | .build() 11 | 12 | test("CurrentTimeMillis.evaluateLogic should be Long type") { 13 | val result = logicEngine.evaluate(mapOf(operatorName to emptyList()), null) 14 | 15 | result should beInstanceOf() 16 | (result as JsonLogicResult.Success).value should beInstanceOf() 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /operations-stdlib/src/iosTest/kotlin/encoding/EncodeTest.kt: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import TestInput 4 | import io.kotest.core.spec.style.FunSpec 5 | import io.kotest.datatest.withData 6 | import io.kotest.matchers.shouldBe 7 | 8 | class EncodeTest: FunSpec({ 9 | val operatorName = "encode" 10 | val logicEngine = JsonLogicEngine.Builder() 11 | .addStandardOperation(operatorName, Encode) 12 | .build() 13 | 14 | withData( 15 | nameFn = { input -> "Should evaluate encode operation into ${input.result}" }, 16 | ts = listOf( 17 | TestInput( 18 | expression = mapOf( 19 | operatorName to listOf("https://test.pl?id=1234&4321") 20 | ), 21 | result = JsonLogicResult.Success("https%3A%2F%2Ftest%2Epl%3Fid%3D1234%264321") 22 | ), 23 | TestInput( 24 | expression = mapOf( 25 | operatorName to listOf("Foo&Bar") 26 | ), 27 | result = JsonLogicResult.Success("Foo%26Bar") 28 | ), 29 | TestInput( 30 | expression = mapOf( 31 | operatorName to listOf("Foo#Bar") 32 | ), 33 | result = JsonLogicResult.Success("Foo%23Bar") 34 | ), 35 | TestInput( 36 | expression = mapOf( 37 | operatorName to listOf("https://test.pl?name=%C5%BC%C3%B3%C5%82%C4%85d%C5%BA") 38 | ), 39 | result = JsonLogicResult.Success( 40 | "https%3A%2F%2Ftest%2Epl%3Fname%3D%25C5%25BC%25C3%25B3%25C5%2582%25C4%2585d%25C5%25BA" 41 | ) 42 | ), 43 | TestInput( 44 | expression = mapOf( 45 | operatorName to listOf("zażółć gęślą jaźń") 46 | ), 47 | result = JsonLogicResult.Success( 48 | "za%C5%BC%C3%B3%C5%82%C4%87%20g%C4%99%C5%9Bl%C4%85%20ja%C5%BA%C5%84" 49 | ) 50 | ), 51 | TestInput( 52 | expression = mapOf( 53 | operatorName to listOf("kalendář") 54 | ), 55 | result = JsonLogicResult.Success("kalend%C3%A1%C5%99") 56 | ), 57 | TestInput( 58 | expression = mapOf( 59 | operatorName to listOf("Доброго ранку") 60 | ), 61 | result = JsonLogicResult.Success( 62 | "%D0%94%D0%BE%D0%B1%D1%80%D0%BE%D0%B3%D0%BE%20%D1%80%D0%B0%D0%BD%D0%BA%D1%83" 63 | ) 64 | ) 65 | ) 66 | // given 67 | ) { testInput: TestInput -> 68 | // when 69 | val evaluationResult = logicEngine.evaluate(testInput.expression, testInput.data) 70 | 71 | // then 72 | evaluationResult shouldBe testInput.result 73 | } 74 | }) 75 | -------------------------------------------------------------------------------- /operations-stdlib/src/iosTest/kotlin/format/DecimalFormatTest.kt: -------------------------------------------------------------------------------- 1 | ../../../jvmTest/kotlin/format/DecimalFormatTest.kt -------------------------------------------------------------------------------- /operations-stdlib/src/jvmMain/kotlin/CurrentTimeMillis.kt: -------------------------------------------------------------------------------- 1 | import operation.StandardLogicOperation 2 | 3 | actual object CurrentTimeMillis : StandardLogicOperation { 4 | actual override fun evaluateLogic(expression: Any?, data: Any?): Any = System.currentTimeMillis() 5 | } 6 | -------------------------------------------------------------------------------- /operations-stdlib/src/jvmMain/kotlin/encoding/Encode.kt: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import operation.StandardLogicOperation 4 | import string.StringUnwrapStrategy 5 | import java.net.URLEncoder 6 | 7 | actual object Encode : StandardLogicOperation, StringUnwrapStrategy { 8 | actual override fun evaluateLogic(expression: Any?, data: Any?): Any? { 9 | return unwrapValueAsString(expression)?.let { URLEncoder.encode(it, Charsets.UTF_8.name()) } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /operations-stdlib/src/jvmMain/kotlin/format/DecimalFormat.kt: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import operation.StandardLogicOperation 4 | import java.lang.String.format 5 | 6 | actual object DecimalFormat : StandardLogicOperation, DecimalFormatter { 7 | actual override fun evaluateLogic(expression: Any?, data: Any?): Any? { 8 | return formatDecimal(expression, data) { formatSequence: String, arg: Double -> 9 | format(formatSequence, arg) 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /operations-stdlib/src/jvmTest/kotlin/CurrentTimeMillisTests.kt: -------------------------------------------------------------------------------- 1 | import io.kotest.core.spec.style.FunSpec 2 | import io.kotest.matchers.should 3 | import io.kotest.matchers.types.beInstanceOf 4 | 5 | class CurrentTimeMillisTests : FunSpec() { 6 | init { 7 | val operatorName = "currentTime" 8 | val logicEngine = JsonLogicEngine.Builder() 9 | .addStandardOperation(operatorName, CurrentTimeMillis) 10 | .build() 11 | 12 | test("CurrentTimeMillis.evaluateLogic should be Long type") { 13 | val result = logicEngine.evaluate(mapOf(operatorName to emptyList()), null) 14 | 15 | result should beInstanceOf() 16 | (result as JsonLogicResult.Success).value should beInstanceOf() 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /operations-stdlib/src/jvmTest/kotlin/encoding/EncodeTest.kt: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import TestInput 4 | import io.kotest.core.spec.style.FunSpec 5 | import io.kotest.datatest.withData 6 | import io.kotest.matchers.shouldBe 7 | 8 | class EncodeTest: FunSpec({ 9 | val operatorName = "encode" 10 | val logicEngine = JsonLogicEngine.Builder() 11 | .addStandardOperation(operatorName, Encode) 12 | .build() 13 | 14 | withData( 15 | nameFn = { input -> "Should evaluate encode operation into ${input.result}" }, 16 | ts = listOf( 17 | TestInput( 18 | expression = mapOf( 19 | operatorName to listOf("https://test.pl?=1234&4321") 20 | ), 21 | result = JsonLogicResult.Success("https%3A%2F%2Ftest.pl%3F%3D1234%264321") 22 | ), 23 | TestInput( 24 | expression = mapOf( 25 | operatorName to listOf("Foo&Bar") 26 | ), 27 | result = JsonLogicResult.Success("Foo%26Bar") 28 | ), 29 | TestInput( 30 | expression = mapOf( 31 | operatorName to listOf("Foo#Bar") 32 | ), 33 | result = JsonLogicResult.Success("Foo%23Bar") 34 | ), 35 | TestInput( 36 | expression = mapOf( 37 | operatorName to listOf("https://test.pl?name=%C5%BC%C3%B3%C5%82%C4%85d%C5%BA") 38 | ), 39 | result = JsonLogicResult.Success( 40 | "https%3A%2F%2Ftest.pl%3Fname%3D%25C5%25BC%25C3%25B3%25C5%2582%25C4%2585d%25C5%25BA" 41 | ) 42 | ), 43 | TestInput( 44 | expression = mapOf( 45 | operatorName to listOf("zażółć gęślą jaźń") 46 | ), 47 | result = JsonLogicResult.Success( 48 | "za%C5%BC%C3%B3%C5%82%C4%87+g%C4%99%C5%9Bl%C4%85+ja%C5%BA%C5%84" 49 | ) 50 | ), 51 | TestInput( 52 | expression = mapOf( 53 | operatorName to listOf("kalendář") 54 | ), 55 | result = JsonLogicResult.Success("kalend%C3%A1%C5%99") 56 | ), 57 | TestInput( 58 | expression = mapOf( 59 | operatorName to listOf("Доброго ранку") 60 | ), 61 | result = JsonLogicResult.Success( 62 | "%D0%94%D0%BE%D0%B1%D1%80%D0%BE%D0%B3%D0%BE+%D1%80%D0%B0%D0%BD%D0%BA%D1%83" 63 | ) 64 | ) 65 | ) 66 | // given 67 | ) { testInput: TestInput -> 68 | // when 69 | val evaluationResult = logicEngine.evaluate(testInput.expression, testInput.data) 70 | 71 | // then 72 | evaluationResult shouldBe testInput.result 73 | } 74 | }) 75 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | gradlePluginPortal() 5 | mavenCentral() 6 | } 7 | } 8 | rootProject.name = "json-logic-kmp" 9 | 10 | include("core", "operations-api", "operations-stdlib", "utils", "umbrella-framework") 11 | -------------------------------------------------------------------------------- /umbrella-framework/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework 2 | 3 | @Suppress("DSL_SCOPE_VIOLATION") // TODO remove on fix: https://youtrack.jetbrains.com/issue/KTIJ-19369 4 | plugins { 5 | alias(libs.plugins.kotlin.multiplatform) 6 | alias(libs.plugins.kotest) 7 | id(Conventions.junit) 8 | id(Conventions.publishing) 9 | } 10 | 11 | kotlin { 12 | jvm { 13 | mavenPublication{ setFullModuleArtifactId() } 14 | compilations.all { 15 | kotlinOptions.jvmTarget = JavaVersion.VERSION_11.majorVersion 16 | } 17 | } 18 | 19 | val xcFramework = XCFramework(LibConfig.xcFrameworkName) 20 | listOf( 21 | iosX64(), 22 | iosArm64(), 23 | iosSimulatorArm64() 24 | ).forEach { 25 | it.binaries.framework { 26 | binaryOption("bundleId", LibConfig.bundleId) 27 | baseName = LibConfig.xcFrameworkName 28 | isStatic = true 29 | export(project(Modules.core)) 30 | export(project(Modules.operationsApi)) 31 | export(project(Modules.operationsStdlib)) 32 | xcFramework.add(this) 33 | } 34 | } 35 | 36 | sourceSets { 37 | val commonMain by getting { 38 | dependencies { 39 | api(project(Modules.core)) 40 | api(project(Modules.operationsApi)) 41 | api(project(Modules.operationsStdlib)) 42 | implementation(project(Modules.utils)) 43 | } 44 | } 45 | 46 | val commonTest by getting { 47 | dependencies { 48 | implementation(kotlin(Modules.kotlinTest)) 49 | implementation(libs.bundles.common.kotest) 50 | implementation(project(Modules.utils)) 51 | } 52 | } 53 | val jvmTest by getting { 54 | dependencies { 55 | implementation(libs.kotest.jvm.junit5.runner) 56 | } 57 | } 58 | 59 | val iosX64Main by getting 60 | val iosArm64Main by getting 61 | val iosSimulatorArm64Main by getting 62 | val iosMain by creating { 63 | dependsOn(commonMain) 64 | iosX64Main.dependsOn(this) 65 | iosArm64Main.dependsOn(this) 66 | iosSimulatorArm64Main.dependsOn(this) 67 | 68 | dependencies { 69 | implementation(libs.crashkios) 70 | } 71 | } 72 | } 73 | } 74 | 75 | 76 | -------------------------------------------------------------------------------- /umbrella-framework/src/iosMain/kotlin/CrashIntegration.kt: -------------------------------------------------------------------------------- 1 | import co.touchlab.crashkios.CrashHandler 2 | import co.touchlab.crashkios.setupCrashHandler 3 | 4 | fun crashInit(handler: CrashHandler) { 5 | setupCrashHandler(handler) 6 | } 7 | -------------------------------------------------------------------------------- /utils/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @Suppress("DSL_SCOPE_VIOLATION") // TODO remove on fix: https://youtrack.jetbrains.com/issue/KTIJ-19369 2 | plugins { 3 | alias(libs.plugins.kotlin.multiplatform) 4 | alias(libs.plugins.kotest) 5 | id(Conventions.junit) 6 | id(Conventions.publishing) 7 | } 8 | 9 | kotlin { 10 | jvm { 11 | compilations.all { 12 | kotlinOptions.jvmTarget = JavaVersion.VERSION_11.majorVersion 13 | } 14 | } 15 | 16 | iosX64() 17 | iosArm64() 18 | iosSimulatorArm64() 19 | 20 | sourceSets { 21 | val commonMain by getting 22 | val commonTest by getting { 23 | dependencies { 24 | implementation(kotlin(Modules.kotlinTest)) 25 | implementation(libs.bundles.common.kotest) 26 | } 27 | } 28 | val jvmTest by getting { 29 | dependencies { 30 | implementation(libs.kotest.jvm.junit5.runner) 31 | } 32 | } 33 | val iosX64Main by getting 34 | val iosArm64Main by getting 35 | val iosSimulatorArm64Main by getting 36 | val iosMain by creating { 37 | dependsOn(commonMain) 38 | iosX64Main.dependsOn(this) 39 | iosArm64Main.dependsOn(this) 40 | iosSimulatorArm64Main.dependsOn(this) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /utils/src/commonMain/kotlin/AnyUtils.kt: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import type.JsonLogicList 4 | 5 | val Any?.asList: List 6 | get() = (this as? List<*>)?.let { 7 | JsonLogicList(it) 8 | } ?: JsonLogicList(listOf(this)) 9 | 10 | val List.comparableList: List?> 11 | get() = asList.map { it.asComparable } 12 | 13 | private val Any?.asComparable: Comparable<*>? 14 | get() = when (this) { 15 | is Comparable<*> -> this 16 | is List<*> -> JsonLogicList(this) 17 | else -> null 18 | } 19 | 20 | val Any?.asDoubleList: List 21 | get() = asList.doubleList 22 | 23 | private val List.doubleList: List 24 | get() = map { 25 | when (it) { 26 | is Number -> it.toDouble() 27 | is String -> it.toDoubleOrNull() 28 | else -> null 29 | } 30 | } 31 | 32 | fun Any?.toStringOrEmpty() = this?.let { toString() }.orEmpty() 33 | 34 | fun Any?.isSingleNullList() = this is List<*> && size == 1 && first() == null 35 | 36 | fun Any?.isExpression() = (this as? Map<*, *>)?.let { 37 | it.isNotEmpty() && it.keys.all { key -> key is String } 38 | } ?: false 39 | -------------------------------------------------------------------------------- /utils/src/commonMain/kotlin/BooleanUtils.kt: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | fun Boolean.asNumber() = if (this) 1.0 else 0.0 4 | -------------------------------------------------------------------------------- /utils/src/commonMain/kotlin/ListUtils.kt: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | fun List.secondOrNull() = getOrNull(1) 4 | fun List.thirdOrNull() = getOrNull(2) 5 | 6 | @Suppress("UNCHECKED_CAST") 7 | fun List.getMappingOperationOrNull() = secondOrNull().takeIf { it.isExpression() } as? Map 8 | -------------------------------------------------------------------------------- /utils/src/commonMain/kotlin/StringUtils.kt: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | val String.intOrZero: Int 4 | get() = doubleOrZero.toInt() 5 | 6 | val String.longOrZero: Long 7 | get() = doubleOrZero.toLong() 8 | 9 | val String.doubleOrZero: Double 10 | get() = toDoubleOrNull() ?: 0.0 11 | 12 | -------------------------------------------------------------------------------- /utils/src/commonMain/kotlin/type/JsonLogicList.kt: -------------------------------------------------------------------------------- 1 | package type 2 | 3 | class JsonLogicList(private val items: List) : List by items, Comparable> { 4 | override fun compareTo(other: List): Int { 5 | return compareValues(items.toString(), other.toString()) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /utils/src/commonTest/kotlin/AnyUtilsTest.kt: -------------------------------------------------------------------------------- 1 | import io.kotest.core.spec.style.BehaviorSpec 2 | import io.kotest.matchers.shouldBe 3 | import utils.isExpression 4 | 5 | class AnyUtilsTest : BehaviorSpec({ 6 | given("A list") { 7 | val wrappedValue = listOf("banana", 1) 8 | `when`("checked") { 9 | val unwrapResult = wrappedValue.isExpression() 10 | then("should not be treated as an expression") { 11 | unwrapResult shouldBe false 12 | } 13 | } 14 | } 15 | 16 | given("A single value") { 17 | val wrappedValue = "fake map" 18 | `when`("checked") { 19 | val unwrapResult = wrappedValue.isExpression() 20 | then("should not be treated as an expression") { 21 | unwrapResult shouldBe false 22 | } 23 | } 24 | } 25 | 26 | given("An empty map") { 27 | val wrappedValue = emptyMap() 28 | `when`("checked") { 29 | val unwrapResult = wrappedValue.isExpression() 30 | then("should not be treated as an expression") { 31 | unwrapResult shouldBe false 32 | } 33 | } 34 | } 35 | 36 | given("A map with keys other than strings") { 37 | val wrappedValue = mapOf(1 to "var") 38 | `when`("checked") { 39 | val unwrapResult = wrappedValue.isExpression() 40 | then("should not be treated as an expression") { 41 | unwrapResult shouldBe false 42 | } 43 | } 44 | } 45 | 46 | given("A map with string keys") { 47 | val wrappedValue = mapOf("var" to "a.b") 48 | `when`("checked") { 49 | val unwrapResult = wrappedValue.isExpression() 50 | then("should be treated as an expression") { 51 | unwrapResult shouldBe true 52 | } 53 | } 54 | } 55 | }) 56 | --------------------------------------------------------------------------------