Portable validations for Kotlin

Overview

Build Status Maven Central

Portable validations for Kotlin

  • Type-safe DSL
  • 🔗 Multi-platform support (JVM, JS)
  • 🐥 Zero dependencies

Installation

For multiplatform projects:

kotlin {
    sourceSets {
        commonMain {
            dependencies {
                implementation("io.konform:konform:0.3.0")
            }
        }
    }
}

For jvm-only projects add:

dependencies {
    implementation("io.konform:konform:0.3.0")
}

Use

Suppose you have a data class like this:

data class UserProfile(
    val fullName: String,
    val age: Int?
)

Using the Konform type-safe DSL you can quickly write up a validation

val validateUser = Validation<UserProfile> {
    UserProfile::fullName {
        minLength(2)
        maxLength(100)
    }

    UserProfile::age ifPresent {
        minimum(0)
        maximum(150)
    }
}

and apply it to your data

val invalidUser = UserProfile("A", -1)
val validationResult = validateUser(invalidUser)

since the validation fails the validationResult will be of type Invalid and you can get a list of validation errors by indexed access:

validationResult[UserProfile::fullName]
// yields listOf("must have at least 2 characters")

validationResult[UserProfile::age]
// yields listOf("must be at least '0'")

or you can get all validation errors with details as a list:

validationResult.errors
// yields listOf(
//     ValidationError(dataPath=.fullName, message=must have at least 2 characters),
//     ValidationError(dataPath=.age, message=must be at least '0'
// )

In case the validation went through successfully you get a result of type Valid with the validated value in the value field.

val validUser = UserProfile("Alice", 25)
val validationResult = validateUser(validUser)
// yields Valid(UserProfile("Alice", 25))

Advanced use

You can define validations for nested classes and use them for new validations

val ageCheck = Validation<UserProfile> {
    UserProfile::age required {
        minimum(18)
    }
}

val validateUser = Validation<UserProfile> {
    UserProfile::fullName {
        minLength(2)
        maxLength(100)
    }
    
    run(ageCheck)
}

It is also possible to validate nested data classes and properties that are collections (List, Map, etc...)

data class Person(val name: String, val email: String?, val age: Int)

data class Event(
    val organizer: Person,
    val attendees: List<Person>,
    val ticketPrices: Map<String, Double?>
)

val validateEvent = Validation<Event> {
    Event::organizer {
        // even though the email is nullable you can force it to be set in the validation
        Person::email required {
            pattern("[email protected]") hint "Organizers must have a BigCorp email address"
        }
    }

    // validation on the attendees list
    Event::attendees {
        maxItems(100)
    }

    // validation on individual attendees
    Event::attendees onEach {
        Person::name {
            minLength(2)
        }
        Person::age {
            minimum(18) hint "Attendees must be 18 years or older"
        }
        // Email is optional but if it is set it must be valid
        Person::email ifPresent {
            pattern(".+@.+\..+") hint "Please provide a valid email address (optional)"
        }
    }

    // validation on the ticketPrices Map as a whole
    Event::ticketPrices {
        minItems(1) hint "Provide at least one ticket price"
    }

    // validations for the individual entries
    Event::ticketPrices onEach {
        // Tickets may be free in which case they are null
        Entry<String, Double?>::value ifPresent {
            minimum(0.01)
        }
    }
}

Errors in the ValidationResult can also be accessed using the index access method. In case of Iterables and Arrays you use the numerical index and in case of Maps you use the key as string.

// get the error messages for the first attendees age if any
result[Event::attendees, 0, Person::age]

// get the error messages for the free ticket if any
result[Event::ticketPrices, "free"]

Other validation libraries written in Kotlin

Integration with testing libraries

  • Kotest provides various matchers for use with Konform. They can be used in your tests to assert that a given object is validated successfully or fails validation with specific error messages. See usage documentation here.
Author

Niklas Lochschmidt

License

MIT License

Comments
  • `onEach` working for nullable iterables

    `onEach` working for nullable iterables

    Currently, one cannot apply onEach validation if iterable field is nullable, for example val items: List<MyClass>? There should be some way to do it either by making it possible to combine ifPresent with onEach or by making onEach treat a null iterable in the same way as an empty iterable. There could also be new method like onEachIfPresent

    opened by wtomi 6
  • Getting all errors for a result

    Getting all errors for a result

    Currently a ValidationResult<T> can be used to get errors for a specific field:

    result[UserProfile::fullName]
    

    Is there any way to get a map of all errors for a given validation (not a separate list for every field)?

    enhancement 
    opened by oripwk 6
  • Reimplementation of konform

    Reimplementation of konform

    This is PR for solving multiple issues at once (so it will be messy for a white). I will write detailed resume of proposed changes and issues, that should be affected/fixed by this PR. Also API's are not final and are subject for discussion.

    data class Person(val name: String, val email: String?, val age: Int, val parent: Person? = null)
    
    data class Event(
        val organizer: Person,
        val attendees: List<Person>,
        val ticketPrices: Map<String, Double?>
    )
    
    val validateEvent = Validation<Event, ValidationError> {
        Event::organizer {
            // even though the email is nullable you can force it to be set in the validation
            require(Person::email) {
                pattern("[email protected]") { Error("Organizers must have a BigCorp email address") }
            }
        }
    
        // validation on the attendees list
        Event::attendees {
            maxItems(100)
        }
    
        // validation on individual attendees
        Event::attendees onEach {
            // If any validation fails – do not check rest of them
            eager {
                Person::name {
                    minLength(2)
                }
                // Age affects internal checks
                Person::age affects { age ->
                    if (age < 18) {
                        require(Person::parent)
                    }
                }
                // Email is optional but if it is set it must be valid
                Person::email ifPresent {
                    pattern(".+@.+\..+") { Error("Please provide a valid email address (optional)") }
                }
            }
        }
    
        // validation on the ticketPrices Map as a whole
        Event::ticketPrices {
            minItems(1) { Error("Provide at least one ticket price") }
        }
    
        // validations for the individual entries
        Event::ticketPrices onEach {
            // Tickets may be free in which case they are null
            Entry<String, Double?>::value ifPresent {
                minimum(0.01)
            }
        }
    }
    

    For now these issues should be affected by this PR:

    • #54 – Added free standing ifPresent and required blocks
    • #53 – *Items were splitted to separate functions by type
    • #52 – properties argument added to validate method, affects helper also introduced to mark dependencies between fields
    • #51 – Error type introduced to generic signature Validation<T, E>
    • #48 – Should be achievable through custom errors type (but requires some additional code)
    • #36 – required now accepts constructError argument to provide custom errors (but looses infix)
    • #29 – affects provides access to property value inside validation Block
    • #20 – Pair<Person::name, Person::familyName>> affects { ... } could be used (test for this needed, I guess)
    • #8 – eager modifier added to break validation on first error

    • [ ] Port JSONSchemaStyleConstraintsTest.kt and ValidationBuilderTest.kt to new implementation
    • [ ] Revisit ErrorWithPath class
    • [ ] Port Indexed access to errors back
    opened by floatdrop 5
  • Metadata errors when using in a multi-platform project commonMain

    Metadata errors when using in a multi-platform project commonMain

    When including 0.2.0 in a multiplatform project, it is unusable in the commonMain source set due to some sort of metadata publishing error.

    I have an example repo set up to replicate this error here: https://github.com/adamsar/kotlin-dependency-bug

    opened by adamsar 5
  • Add 0.1.0 to MavenCentral

    Add 0.1.0 to MavenCentral

    Hello!

    We have installed version 0.1.0 in legacy project and want to make a simple switch from Bintray to MavenCentral, but MavenCentral does not have version 0.1.0

    Could You pulblish it?

    opened by esbobkov 4
  • Remove internal access modifier to make errors Map accessible from outside

    Remove internal access modifier to make errors Map accessible from outside

    Hey! This fixes #3. As there was no real result from the issue comments and I think everyone would be fine to at least have access to the errors, the easiest way would be to just expose the errors map to everyone.

    Thanks, Kenneth

    opened by KennethWussmann 4
  • Exposing Validation Metadata

    Exposing Validation Metadata

    Hey, I am working on an OpenAPI spec generator for Ktor https://github.com/bkbnio/kompendium and would love to build in Konform validation, but I am running into a problem. A method for generating validation metadata is eluding me.

    Because all implementations of Validation are internal, and afaik the actual constraints don't get exposed to the end user, it feels like I am in a bit of a bind as far as pulling that info goes.

    So I have a couple questions

    1. Am I just missing something? Is there an easy way to pull constraint metadata for a declared validation
    2. If not, are there any major concerns with exposing the list of constraints applied to a validation?
    opened by unredundant 3
  • Add support for range validation

    Add support for range validation

    Inspired by this reddit comment.

    Instead of writing

    minimum(0)
    maximum(150)
    

    the user could instead use

    inRange(0..5)
    between(0, 5)
    betweenExclusive(0, 5)
    
    wontfix 
    opened by timrobertsdev 3
  • Allow access to subvalidations

    Allow access to subvalidations

    It should be possible to access subvalidations of a complete validation for example to use it on input fields.

    Probably something along the lines of

    val validation = Validation<DTO> { DTO::firstName.has.minLength(2) }
    val firstNameValidation: Validation<String> = validation[DTO::firstName]
    
    enhancement wontfix 
    opened by nlochschmidt 3
  • How to validate single values?

    How to validate single values?

    For example, how to create a validation for a String or LocalDate? without having them part of some other object

    I was able to achieve by dropping to addConstraint:

    va validation = Validation<String?> {
        addConstraint("must be 4 digits") { it != null && it.length == 4 }
    }
    

    but wasn't unable to utilize required, ifPresent or any of the existing validators (minLength, ...etc).

    Is that supported?

    opened by amr 2
  • Upgrade to Kotlin 1.6

    Upgrade to Kotlin 1.6

    I decided to make konform backwards compatible until Kotlin 1.4 and to stop having the stdlib as a transitive dependency.

    This means that someone using Kotlin 1.4 should be able to use Konform without getting the Kotlin Stdlib version 1.6 forced upon them.

    Related to https://github.com/konform-kt/konform/issues/32#issuecomment-1143194843

    opened by nlochschmidt 2
  • Update plugin org.jetbrains.kotlin.multiplatform to v1.7.21

    Update plugin org.jetbrains.kotlin.multiplatform to v1.7.21

    Mend Renovate

    This PR contains the following updates:

    | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | org.jetbrains.kotlin.multiplatform (source) | 1.7.10 -> 1.7.21 | age | adoption | passing | confidence |


    Release Notes

    JetBrains/kotlin

    v1.7.21

    Compiler
    • KT-54463 Delegating to a field with a platform type causes java.lang.NoSuchFieldError: value$delegate
    • KT-54509 Ir Interpreter: unable to evaluate string concatenation with "this" as argument
    • KT-54004 Builder type inference does not work correctly with variable assignment and breaks run-time
    • KT-54393 Change in behavior from 1.7.10 to 1.7.20 for java field override.
    • KT-54615 JVM: Internal error in file lowering: java.lang.AssertionError: Error occurred while optimizing an expression
    • KT-54581 JVM: "VerifyError: Bad type on operand stack" with generic inline function and when inside try-catch block
    • KT-53146 JVM IR: unnecessary checkcast of null leads to NoClassDefFoundError if the type isn't available at runtime
    • KT-54600 NPE on passing nullable Kotlin lambda as Java's generic SAM interface with super type bound
    • KT-54707 "VerifyError: Bad type on operand stack" in inline call chain on a nullable array value
    • KT-54650 Binary incompatible ABI change in Kotlin 1.7.20
    • KT-54802 "VerifyError: Bad type on operand stack" for inline functions on arrays
    Native. Runtime. Memory
    • KT-54498 Deprecation message of 'FreezingIsDeprecated' is not really helpful
    Tools. Gradle. Multiplatform
    • KT-54387 Remove MPP alpha stability warning
    • KT-48436 False positive "The Kotlin source set androidAndroidTestRelease was configured but not added to any Kotlin compilation"
    Tools. JPS
    • KT-45474 False positive NO_ELSE_IN_WHEN on sealed class with incremental compilation

    v1.7.20

    Analysis API
    • KT-52667 FIR IDE: fun interfaces (SAM interfaces) are not properly resolved
    • KT-52136 FIR: Implicit type declaration from the other module cannot be used for overloading
    Analysis API. FE1.0
    • KT-51962 Analysis API: Finish Analysis API for FE1.0
    Analysis API. FIR
    • KT-52779 FIR IDE: Import Optimizer cannot handle generic type qualifiers
    • KT-50236 Fix OOB modification trackers for non-Kotlin code
    • KT-51240 Analysis API: KtAnalysisSession for a specific module cannot create a symbol for PSI that cannot be seen from that module.
    • KT-50868 Analysis API: decompiled type aliases are not resolved
    Compiler
    • KT-53739 Builder inference, extension hides members
    • KT-53733 Kotlin/Native: update source documentation for the new default memory manager
    • KT-53667 Compiler crashes on attempt to alloc a string on the stack in new MM
    • KT-53480 Internal error in file lowering: java.lang.ClassNotFoundException: com.android.systemui.R$string
    • KT-52843 Compose: NPE at Parameters.getParameterByDeclarationSlot if inline function with default arguments takes a lambda which captures value class represented by Long
    • KT-53475 Kotlin/Native for iOS: "IllegalArgumentException: Sequence has more than one element"
    New Features
    • KT-52495 Support until operator in back-ends
    • KT-52420 Implement resolve of until operator
    • KT-52419 Implement until operator in the parser
    • KT-33755 Kotlin/Native: Provide a way to customize a bundle Identifier of a generated framework
    • KT-51665 FIR: implement label resolve for "typed this" case
    • KT-52361 Report warning on potentially empty intersection types
    Performance Improvements
    • KT-47816 Disable script discovery for non-script environments
    • KT-48635 JVM IR: Double/Float values are boxed when comparing for equality in equals method of data/value classes
    • KT-23397 Optimize out field for property delegate when it's safe (JVM)
    Fixes
    • KT-53272 Backend Internal error: Exception during IR lowering / No such value argument slot: 2
    • KT-53124 Receiver type mismatch when combining extension properties, type projections, Java sources, and F-bounded type-variables
    • KT-51868 JVM / IR: Inconsistent behaviour between lambda expression and SAM interface conversion for the same interface
    • KT-36770 Prohibit unsafe calls with expected @NotNull T and given Kotlin generic parameter with nullable bound
    • KT-52974 "IllegalStateException: Symbol with IrSimpleFunctionSymbolImpl is unbound" compiling native targets of MPP project
    • KT-53007 JVM: "Bad invokespecial instruction: current class isn't assignable to reference class" when call superclass of outer class method from inner class
    • KT-53019 K2: cannot cast callable reference to Function1 in runtime
    • KT-53031 K2 compiler crashes with IllegalStateException: No type in ProtoBuf.ValueParameter
    • KT-29168 Prohibit upper bounds violation with generic typealias using not all type parameters as arguments for underlying type in supertypes
    • KT-52432 Using the IDE compiled with K2 (useFir) throws VerifyError exception
    • KT-52327 False negative: TYPECHECKER_HAS_RUN_INTO_RECURSIVE_PROBLEM isn't reported
    • KT-49682 Support JVM IR in KAPT stub generation
    • KT-24643 Prohibit using a type parameter declared for an extension property inside delegate
    • KT-51972 FIR, Gradle: "Symbol is invisible" compilation error with enabled Kotlin Lombok compiler plugin
    • KT-52011 [FIR] All-open compiler plugin isn't supported
    • KT-51950 JVM IR: "IndexOutOfBoundsException: Cannot pop operand off an empty stack" with crossinline lambdas and interface delegation
    • KT-52540 Native: kotlin.NotImplementedError with Arrow library
    • KT-48031 "IllegalStateException: Type variable TypeVariable(T) should not be fixed!"
    • KT-47708 RequiresOptIn check does not flag experimental method usage in SAM lambda expressions
    • KT-52913 JVM / IR: "IllegalArgumentException: Inline class types should have the same representation" when trying to down cast cast a value class
    • KT-50771 IR partial linkage: Removed abstract callable members are not supported
    • KT-52994 Enable generic inline classes as experimental feature
    • KT-52742 CYCLE_IN_ANNOTATION_PARAMETER_ERROR false positive on annotations with default values
    • KT-52743 Non-null generic functions throws NPE when assigned to val
    • KT-52745 Frontend / K2: "IncompatibleClassChangeError: class A$B overrides final method A.length()I" caused by delegation in a sealed class
    • KT-52832 Tree-generator's method FirExpression::isFirType returns true and false for different field names; it should always be true
    • KT-52403 IncompatibleClassChangeError when inlining suspend funs
    • KT-50107 Missed USAGE_IS_NOT_INLINABLE diagnostic: Leaking inline lambda parameter through extension receiver
    • KT-47965 Missed USAGE_IS_NOT_INLINABLE diagnostic on inline lambda parameter usage as receiver of .let call
    • KT-25787 No error on crossinline usage of receiver parameter of functional type in an inline function
    • KT-52762 Frontend / K2: Named arguments for Java classes lead to "Cannot find a parameter with this name"
    • KT-52680 K2: overload resolution ambiguity if this is casted in a different method
    • KT-52676 K2: Unsupported compile-time value IrGetFieldImpl instead of IrConst in AnnotationCodegen for constant from Java
    • KT-50293 False positive: USELESS_CAST on stub types
    • KT-52175 WRONG_ANNOTATION_TARGET for annotation that used inside if
    • KT-52338 "IncompatibleClassChangeError: Expected non-static field" with Kotlin class with same-named companion object property as base Java class field
    • KT-49507 JVM: "IllegalAccessError: class X tried to access private field" with same-named Kotlin property and Java base class field
    • KT-44512 FIR DFA: incorrect smartcast after null assignment inside a lambda
    • KT-49200 FIR/FE 1.0: different behavior with multiple matching star imports
    • KT-52718 declaringClass deprecation message mentions the wrong replacement in 1.7
    • KT-52190 FIR2IR: Unexpected IrErrorTypeImpl type for put method inside buildMap
    • KT-52197 Incorrect inference of var type inside lambda that passed to extension function with type parameters that defined inside this lambda
    • KT-52057 Unsupported compile-time value STRING_CONCATENATION and GET_FIELD in annotation arguments
    • KT-47823 JVM IR: "IllegalArgumentException: Inline class types should have the same representation" with break usage in the loop range
    • KT-51883 Kotlin 1.6.20 generates "-" in type names around lambdas and inline extension function with reified type which breaks Apache Beam
    • KT-52684 Syntax error regression on complicated combination of LT and GTEQ
    • KT-52417 Reflection: Can't reflect on type parameters captured by SAM converted lambda
    • KT-46797 JVM IR: suspendImpl has no generic signature, breaking reified types in anonymous object supertypes when using the type token pattern
    • KT-51464 FIR: Unable to infer type in coroutines flow code
    • KT-52163 JVM IR: Double.compareTo(Int) compiled to integer comparison
    • KT-41980 FIR: erroneous scope during annotation resolve
    • KT-47159 KtPsiUtils.areParenthesesUseless() is returning a false positive on expressions for interface delegation
    • KT-51418 Substitute anonymous type's supertypes
    • KT-35544 kotlin.TypeCastException has no message on Native
    • KT-52386 StackOverflowError during Kotlin/Native gradle build
    • KT-52592 NPE from KProperty.getExtensionDelegate on property delegated to another property
    • KT-52551 Delegating to object property reference does not invoke object's initializer
    • KT-51704 Contracts: "AssertionError: Arguments and parameters size mismatch" with companion object
    • KT-25527 False positive UNUSED_VALUE for delegated property/variable
    • KT-51002 [FIR] Hidden declaration hides visible one
    • KT-51008 [FIR] Star import does not work for nested calssifiers of java class
    • KT-52407 FIR: Star import has lower priority than built-in import
    • KT-52431 Reported error instead of warning due to empty intersection type found
    • KT-49394 Bad message and suggestion: The feature "unit conversion" is disabled
    • KT-51168 FIR: Inference error with Java interop and captured types
    • KT-49961 "AssertionError: Left expression was not processed: BINARY_EXPRESSION" when analyzing dangling [bracketed] expression inside elvis
    • KT-50108 Difference in fun interface conversion behavior for uninitialized not-null function values
    • KT-51889 Calls to super-classes constructors with context receivers fail on runtime
    • KT-51228 [FIR] Unresolved reference on callable reference on implicit this with smartcast
    • KT-52364 False positive for INFERRED_TYPE_VARIABLE_INTO_EMPTY_INTERSECTION
    • KT-52237 JVM / IR: "IllegalArgumentException: No argument for parameter VALUE_PARAMETER CONTINUATION_CLASS" when implementing Map interface on class with suspending functions
    • KT-50832 Method references to suspend inline functions are processed incorrectly
    • KT-52194 False positive "Class 'my.packge.MyClass' is compiled by a pre-release version of Kotlin and cannot be loaded by this version of the compiler" but builds fine
    • KT-47203 JVM Debugger: Parameter value doesn't change for tailrec function
    • KT-52131 False positive variable unused warning when calling inline function in finally block
    • KT-51738 Debugger: stepping over in inline function with multiple inline lambda invocations is incorrect
    • KT-52198 Losing reference to the value of an outer variable (Ref$ObjectRef) when using suspend inline with suspendCancellableCoroutine
    • KT-50994 FIR: AssertionError during inference of delegated properties with implicit types
    • KT-51757 FIR does not see various JS/Native specific declarations in common modules
    • KT-51201 FIR: ARGUMENT_TYPE_MISMATCH diagnostic contains generic parameter instead of the actual type
    • KT-48444 FIR: type argument rejected for annotation
    • KT-51754 JVM: Local variable table invalid for do-while with continue
    • KT-51936 Breakpoint not hit on last line of suspend function on Android
    • KT-27333 Forbid @Synchronized annotation on suspend functions
    • KT-51530 "StackOverflowError: CoroutineTransformerMethodVisitor.spillVariables" with data class in Flow
    • KT-51460 FIR: Protected property inaccessible from inner class
    • KT-53947 IllegalStateException: No mapping for symbol: VALUE_PARAMETER INSTANCE_RECEIVER
    • KT-51234 Context receivers can be duplicated in function declaration
    • KT-51576 Context receivers: "AssertionError: Callers must check that current token is IDENTIFIER followed with '@​'" with at character
    • KT-49479 JVM / IR: "IllegalStateException: IrErrorType" during IR lowering with non-trivial recursion calls
    • KT-52270 NullPointerException caused by braces on if-else returning a method reference inside lambda
    • KT-47621 False negative INVISIBLE_MEMBER on call to inaccessible protected synthetic property setter
    • KT-37796 NI: "ISE: Error type encountered" when inferring type of a property that delegates to itself
    • KT-45430 PSI2IR: "org.jetbrains.kotlin.psi2ir.generators.ErrorExpressionException: null: KtCallExpression: toString()" with recursive function call in "also" block in nontrivial context
    • KT-52691 K2: Expected FirResolvedTypeRef with ConeKotlinType but was FirImplicitTypeRefImpl with intertwined functional interfaces
    • KT-52822 Fix contract for KtElement.getReference()
    • KT-50223 IndexOutOfBoundsException from ClassicTypeSystemContext$DefaultImpls.getParameter during call resolution
    • KT-51963 Change Maven version to 1.7.255
    • KT-47664 Incorrect type checking in the case of generic types
    • KT-48765 NI: Inferred type does not respect the bound of type variable
    • KT-51243 False positive error "No required context receiver" inside contextual lambda
    • KT-43541 TYPE_MISMATCH for member function which is not occur for top level function during unsafe cast
    • KT-51016 [FIR] False positive OVERLOAD_RESOLUTION_AMBIGUITY between two extensions on different supertypes
    • KT-50155 FIR: support contextual receivers
    • KT-38637 Catch NoSuchFieldException in optimized when expression over enum
    Docs & Examples
    • KT-49896 Kotlin/JS: improve -Xir-property-lazy-initialization description due to making it true by default
    IDE
    • KTIJ-22286 Kotlin JPS project created via wizard does not contain Kotlin libraries in case of not-released version
    • KTIJ-22065 IDE notification motivating Kotlin users to use EAP
    • KTIJ-22209 Configure Kotlin on 221 idea adds 1.6.10 Kotlin (despite the fact that IDE plugin is 1.7.10)
    • KTIJ-22171 Fix test BuiltInDecompilerConsistencyTest
    • KTIJ-22016 Empty .kt file and build.gradle.kts can trigger an error while searching for a facade light class
    • KT-52571 MPP Tasks on import are not up-to-date after subsequent launches
    • KT-47777 ISE thrown from KtLightClassForFacadeImpl.Companion.createForFacadeNoCache has wrong message.
    IDE. FIR
    • KT-52360 FIR IDE: Make the fix of isInheritor method better
    • KT-51786 FIR IDE: IllegalStateException exception in Inspections' infrastructure
    • KT-52331 Analysis API: ArrayIndexOutOfBoundsException exception in Diagnostics' infrastructure
    IDE. Code Style, Formatting
    • KTIJ-21346 Incorrect formatting for functions with context receivers and visibility modifiers
    IDE. Completion
    • KTIJ-21910 FIR IDE: Fix completion tests started failing after visibility checker refinement
    IDE. Decompiler, Indexing, Stubs
    • KTIJ-21243 ContextReceivers: "UpToDateStubIndexMismatch: PSI and index do not match" plugin exception on library with context receivers usage attempt
    IDE. Gradle Integration
    • KT-47627 IDE import fails with com.intellij.util.lang.PathClassLoader error for runCommonizer Gradle task on 212, 213 IDEAs
    • KTIJ-21638 MPP: IntelliJ can not resolve MPP references in common-code
    • KT-52216 HMPP / KTOR: False positive "TYPE_MISMATCH" with Throwable descendant
    IDE. Inspections and Intentions
    • KTIJ-22540 Invalid "remove unnecessary parentheses" when delegating a functional interface to a SAM in brackets
    IDE. J2K
    • KTIJ-21665 J2K generates non compiling code when lifting return and one branch is broken before binary operator
    IDE. JS
    • KTIJ-22337 Wizard: Kotlin/Js for browser: cssSupport DSL should be updated
    IDE. K2
    • KTIJ-21672 FIR IDE: Method reference on generic class breaks resolve
    • KTIJ-21714 FIR IDE: Inherently imported type from another module is not properly resolved
    IDE. Script
    • KT-52525 Update scripts handling in source roots
    IDE. Misc
    • KTIJ-21699 Refactoring: move out parts of the plugin useful for both FE10 and K2
    JavaScript
    New Features
    • KT-39423 KJS: Optionally generate a method to handle optional parameters for function in typescript
    • KT-42282 KJS IR: add an ability to run separate tests
    Performance Improvements
    • KT-50270 KJS IR: Unnecessary getter and setter calls when accessing class members
    Fixes
    • KT-51133 Kotlin/JS - IR: even simple lambdas generate a lot of useless boilerplate
    • KT-51123 Provide a way to add comments to generated JS
    • KT-48493 KJS / IR: Invalid d.ts for inner classes inside objects
    • KT-52553 KJS / IR: diamond hierarchy with super.toString produces stack overflow in runtime
    • KT-23252 JS: Unit materialization on declaration and assignment
    • KT-51128 Kotlin/JS - IR generate huge count of useless blocks
    • KT-50778 KJS/IR: Inline class has no field when building production distribution
    • KT-50157 KSJ IR: Applying identity equality operator to Chars always returns false
    • KT-38262 Javascript generation (and Typescript) fails on 'then', 'catch' and 'finally' (and others?) claiming they are reserved names
    • KT-51066 KJS / IR: suspend lambda parameter of value class is undefined
    • KT-51102 KJS/IR: Assertion failed at translateCallArguments(jsAstUtils.kt:343)
    • KT-51878 KJS / Legacy: Unit is not materialized in an overridden method, but it should be
    Language Design
    • KT-47986 Implicit inferring a type variable into an upper bound in the builder inference context
    • KT-49264 Deprecate infix function calls of functions named "suspend" with dangling function literal
    • KT-25636 Native: Object is frozen by default problem
    • KT-49303 Implement support for basic compile-time evaluation
    Libraries
    • KT-52932 Open-ended ranges in the standard library
    • KT-52910 Provide visit extension functions for java.nio.file.Path
    • KT-48232 Multiplatform function for computing cubic root
    • KT-52778 The documentation for the Duration does not indicate any differences from the ISO-8601
    • KT-52618 ThreadLocalRandom is not a good source of randomness on Android before SDK 34, so don't use it for Kotlin Random
    Native
    • KT-53346 MPP project with kotlinx-serialization-json:1.4.0-RC is not built
    Native. C Export
    • KT-45468 Kotlin/Native: Bitcode verification error when trying to export a cached klib to a static or dynamic library
    Native. C and ObjC Import
    • KT-53373 Native: @​ExportObjCClass doesn't work with the new memory manager
    • KT-49034 Kotlin/Native: cnames.structs.Foo resolves into wrong declaration
    • KT-26478 Objective-C object's class name is null in ClassCastException's message
    Native. ObjC Export
    • KT-51593 Include more information in Objective-C header documentation
    • KT-33117 Improve customizing Info.plist in produced frameworks
    • KT-52681 Native: @end; for Objective-C is generated with an unnecessary semicolon
    Native. Platforms
    • KT-52226 Kotlin/Native: Add support for cross-compilation of MIPS targets from macOS and Windows hosts
    Native. Runtime
    • KT-52430 KMM 1.6.21 framework built with Xcode13, new MM GC Can't support iOS 9.x
    • KT-53534 Kotlin/Native: -Xruntime-logs=gc=info flag doesn't work with compiler caches in 1.7.20-beta
    Native. Runtime. Memory
    • KT-52692 Kotlin/Native: fix tests with aggressive GC
    • KT-52130 Kotlin/Native: use Xallocator for Kotlin objects only
    • KT-51436 Kotlin/Native: optimize mark queue
    Reflection
    • KT-51804 An error occurs when callBy a KFunction that contains a value class as an argument, has a default argument set, and has more than 32 arguments.
    Tools. CLI
    • KT-52465 CLI: IllegalStateException IrSimpleFunctionPublicSymbolImpl when source root is duplicated
    • KT-52380 Invalid path to compiler plugins should be reported as a compiler error
    • KT-51025 JVM CLI compiler takes class file from classpath instead of input java source file
    • KT-51846 Setting random value to the compiler argument where number is expected should produce an error. "-Xbackend-threads=abcd"
    Tools. Compiler Plugins
    • KT-52486 [K2] Looking for function/constructor whose parameters are annotated or meta annotated
    • KT-52872 Mark supportsK2 in ComponentRegistrar.kt as JvmDefault to avoid compatibility problems
    • KT-52804 A function obtained by Fir IrBuiltins has an incorrect package
    • KT-52468 Rename module and jar for lombok compiler plugin
    Tools. Gradle
    • KT-53670 Gradle: Cyclic dependency between kotlin-gradle-plugin-idea-1.7.20-Beta and kotlin-gradle-plugin-idea-proto-1.7.20-Beta
    • KT-53615 Gradle: Fix deprecation warnings in CleanableStoreImpl
    • KT-53118 Fully up-to-date builds are slower with Kotlin 1.7.0
    • KT-51923 Improve usability of errors and warnings by being able to click on them
    • KT-53244 Report from gradle about compiler plugins
    • KT-52839 Warn in Gradle log why incremental compilation failed
    • KT-46019 Compatibility with Gradle 7.1 release
    • KT-47047 Migrate Kotlin Gradle Plugin from using Gradle conventions
    • KT-52698 Don't add InspectClassesForMultiModuleIC task when new incremental compilation is enabled
    • KT-52867 Provide simplified JVM toolchain configuration method
    • KT-45747 Add basic JUnit 5 Kotlin Gradle Plugin Android tests setup
    • KT-46034 Shadow Kotlin Gradle plugin dependencies
    • KT-28664 Support ExtensionContainer on kotlin targets and source sets.
    • KT-19472 Useful extensions of Gradle Kotlin DSL provided by Gradle Kotlin plugin
    • KT-34393 Kotlin Gradle DSL: Inconsistent srcDir configuration between Java and Kotlin
    • KT-51629 There isn't enough info about incremental compilation state in logs while running build with --info key
    Tools. Gradle. Cocoapods
    • KT-53174 CocoaPods: Synthetic Podfile does not specify platform
    • KT-53127 "MaterialComponents normal armv7" in Cocoapods plugin between Kotlin 1.6.21 and 1.7.0
    • KT-44155 Cocoapods doesn't support pods without module map file inside
    • KT-49032 Cocoapods cinterop: some header files are not found
    • KT-53337 Add warning about future changing default linking type of framework provided via cocoapods plugin
    Tools. Gradle. JS
    • KT-52637 KJS / Gradle: Add SCSS webpack config
    • KT-51527 Kotlin/JS: BrowserXRun causes full-screen Webpack error "Compiled with problems: asset size limit/entrypoint size limit" for fresh Kotlin-React project from wizard
    • KT-51532 Kotlin/JS: passing environment variable via Gradle script causes "Execution optimizations have been disabled" warnings
    • KT-52221 Kotlin/JS: failed Node tests are not reported in a standard way
    Tools. Gradle. Multiplatform
    • KT-52243 CInteropProcess is not cacheable despite the annotation
    • KT-52741 MPP: klib outputs are not reproducible
    • KT-52208 MPP: Gradle plugin 1.7 doesn't support latest api versions (1.8, 1.9)
    • KT-54071 MPP/AGP compatibility: Bump maxSupportedVersion to 7.3.0
    Tools. Gradle. Native
    • KT-52632 Gradle / Native: commonizeNativeDistributionTask can never be up-to-date
    • KT-52328 "ld: framework not found SQLCipher" linkDebugTestIosSimulatorArm64 error
    Tools. Incremental Compile
    • KT-53168 Incremental compilation doesn't perform correctly after a few iterations
    • KT-52925 [IR BE] Non incremental build occurs after build failure for compileKotlinJs task
    • KT-52946 CompileKotlinJs task is executed non-incrementally if there were changes made to the dependant module
    • KT-52329 Reduce memory usage of classpath snapshot cache
    • KT-53266 Increment Compilation: "IllegalStateException: The following LookupSymbols are not yet converted to ProgramSymbols" when changing companion object constant field
    • KT-53231 New IC reports build failures for missing classpath snapshots
    Tools. JPS
    • KT-47824 'when expression must be exhaustive' isn't thrown during incremental compilation
    • KT-51873 JPS build is incorrect after gdw build
    • KTIJ-17072 JPS does not rebuild Kotlin usages of declared in Java when enum entry is added
    • KT-51537 Rebuild module on plugin classpaths change
    Tools. REPL
    • KT-45065 REPL: Incorrect output for unsigned value
    • KT-53043 Scripting: Changes in REPL protocol: mark end of errors
    Tools. Scripts
    • KT-52598 Scripts and REPL snippets are not checked against using 'kotlin' package
    • KT-47187 Kotlin ScriptEngine Failes to Compile a Trivial Statement After Failing to Compile a Broken One
    • KT-47191 [Kotlin Script Engine] Compiling Without Evaluating Leaves Engine in a Bad State
    Tools. Kapt
    • KT-52761 Kotlin 1.7.0 breaks kapt processing for protobuf generated java sources

    Configuration

    📅 Schedule: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

    🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

    Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

    🔕 Ignore: Close this PR and you won't be reminded about this update again.


    • [ ] If you want to rebase/retry this PR, check this box

    This PR has been generated by Mend Renovate. View repository job log here.

    opened by renovate[bot] 0
  • Compose validation builders

    Compose validation builders

    This branch features a list of changes:

    Composition of ValidationBuilders

    Currently there is the use of something called PropKey to register builders. A PropKey contains a KProperty and an enum value that explains what type of Validation to build. This is not really extensible.

    I refactored it to use the Composition pattern. A ValidationBuilder can be composed with another that adds an extra level of features.

    This was done so that in the future we can add different algebraic operators (think oneOf, lazy, eager, ...)

    Validation on Java functions

    It also enables us to create validations on Java functions, because that is abstracted away in a MappedValidationBuilder that works allows us to switch from a validation over a type T to a type V where (T) -> V.

    Custom error types

    Now the errors are always strings. This is great in 99% of the cases, but in some cases I'd like to add some context to the error. Thus the error type is now generic.

    enum class Errors { ONE, TWO, THREE }
    val validation = Validation<String, Register, Errors> {
        Register::referredBy {
            pattern(staticHint(TWO), ".+@.+")
        }
    }
    

    The static function returns a HintBuilder;

    typealias HintArguments = List<Any>
    typealias HintBuilder<C, T, E> = C.(T, HintArguments) -> E
    

    Note the default error type is String, this means that almost no breaking changes were introduced.

    Required as Constraint

    It was not possible to add a hint to the required validation (since it was no Constraint).

    The composition pattern makes it easy to rebuild this. So now we can add a hint after a required block:

    SomeObject::optionalValue required with {
        // validate non-null value
    } hint stringHint("whoops!")
    

    or

    SomeObject::optionalValue required with(stringHint("whoops!")) {
        // validate non-null value
    }
    

    Note the with function creates an object that combines a hint with an init function for a ValidationBuilder. This is the most noticeable breaking change.

    Improved type safety

    To catch type errors on compile time instead runtime, a number of functions have improved type safety

    • maxItems
    • minItems
    • uniqueItems

    Breaking changes

    • Introduction of with function for required builder
    • Extra type arguments needed when adding extension functions to ValidationBuilder<A, B, C>
    • addConstraint has different signature
    • More?

    README

    The readme is updated to reflect the latest changes, but needs some changes to elaborate on:

    • Context
    • Custom error types

    Extra notes on the changes

    I know there are a lot of changes in this branch and understand that it is a lot to process in one go. I also believe that the essence of Konform is still intact, proven by the strong backwards compatibility.

    Please consider merging this branch.

    opened by gmulders 3
  • Make validations context aware

    Make validations context aware

    Currently when you want to do a database call or check a value against some set of data, your only option is to wrap the "context" in the builder, e.g.

    val allowedValues: Set<String> = calculateAllowedValues()
    val validation = Validation<String> {
        addConstraint("This value is not allowed!") { allowedValues.contains(it) }
    }
    validation("wim")
    

    The result is that a validation is tightly coupled to its "context". This is troublesome especially when the "context" is not constant.

    Prettier would be:

    val validation = Validation<Set<String>, String> {
        addConstraint("This value is not allowed!") { value -> this.contains(value) }
    }
    val allowedValues: Set<String> = calculateAllowedValues()
    validation(allowedValues, "wim")
    

    The changes in PR #61 allow the user to use a piece of context, with almost no breaking changes for those that don't need any context (the special case, with Unit context). The only breaking change is when you define your own extension function on a ValidationBuilder.

    fun ValidationBuilder<String>.myOwnConstraintBuilder() = ...
    // becomes:
    fun ValidationBuilder<Unit, String>.myOwnConstraintBuilder() = ...
    

    It makes it also possible to combine validations with different contexts (see for example test method composeValidationsWithContext):

    val addressValidation = Validation<AddressContext, Address> {
        Address::country {
            addConstraint("Country is not allowed") {
                this.validCountries.contains(it)
            }
        }
    }
    
    val validation = Validation<Context, Register> {
        Register::home ifPresent {
            run(addressValidation, Context::subContext)
        }
    }
    

    Please consider merging this branch.

    opened by geertmulders 0
  • Make validations context aware

    Make validations context aware

    Currently when you want to do a database call or check a value against some set of data, your only option is to wrap the "context" in the builder, e.g.

    val allowedValues: Set<String> = calculateAllowedValues()
    val validation = Validation<String> {
        addConstraint("This value is not allowed!") { allowedValues.contains(it) }
    }
    validation("wim")
    

    The result is that a validation is tightly coupled to its "context". This is troublesome especially when the "context" is not constant.

    Prettier would be:

    val validation = Validation<Set<String>, String> {
        addConstraint("This value is not allowed!") { context, value -> context.contains(value) }
    }
    val allowedValues: Set<String> = calculateAllowedValues()
    validation(allowedValues, "wim")
    

    The changes in this branch allow the user to use a piece of context, with almost no breaking changes for those that don't need any context (the special case, with Unit context). The only breaking change is when you define your own extension function on a ValidationBuilder.

    fun ValidationBuilder<String>.myOwnConstraintBuilder() = ...
    // becomes:
    fun ValidationBuilder<Unit, String>.myOwnConstraintBuilder() = ...
    
    opened by gmulders 3
  • Add support for polymorphic list validation

    Add support for polymorphic list validation

    Hello, I have one use-case that I cannot achieve with the library right now. I have a list of multiple common types and I want to have specific validation for each type. For example:

    sealed interface Event {
        val id: ObjectId
        val start: Instant
        val end: Instant
        ... some other common fileds
    }
    
    data class ConcertEvent(
         val band: String,
         ... other fields
    ): Event
    
    data class TheaterEvent(
         val stage: String,
         ... other fields
    ): Event
    
    data class Subscribe(
        val events: List<Event>,
        ... other fields
    );
    

    Currently, if I want to add validation for Subscribe.events I can do this only by validating Event fields with

    Subscribe.events onEach {
         here we cannot have validation for `TheaterEvent` or `ConcertEvent` fields only for `Event`
    }
    

    I think is going to be very useful if we have something like this

    Subscribe.events onEach<ConcertEvent> {
        ConcertEvent::band required {}
    }
    
    Subscribe.events onEach<TheaterEvent> {
        TheaterEvent::stage required {}
       ... other validations
    }
    

    What do you think? Is it possible to achieve similar result? Do you think there would be a better approach?

    PS. Konform is great! I love it.

    opened by inakov 0
  • Native platforms support

    Native platforms support

    Please accept this pull request. It includes the following changes

    1. Native platforms are added to build.gradle
    2. Runner environment is changed to macos to provide Apple's native platforms builds

    According to my checks all tests are passed successfully.

    opened by svok 0
Releases(v0.4.0)
  • v0.4.0(Jun 1, 2022)

    • Upgrade to Kotlin 1.6 (backwards compatible with versions 1.4 and 1.5) #41
    • onEach can be used inside of ifPresent when used with nullable collection types #34 h/t @wtomi
    • Use DSL markers to prevent scope leak #39 h/t @cies
    Source code(tar.gz)
    Source code(zip)
  • v0.3.0(Apr 28, 2021)

  • v0.0.3(Mar 9, 2018)

    Konform now supports this list of validations from the JSON Schema spec

    For Any instance

    • type: Check the type of the value (subtype of the declared type)
    • enum: Value must be one of the specified values, can also validate Strings based on Kotlin enum classes
    • const: Value must be equal to the specified value

    For Numbers

    • multipleOf: Value must be a multiple of the given value
    • maximum: Inclusive upper bound on Number
    • exclusiveMaximum: Exclusive upper bound on Number
    • minimum: Inclusive lower bound on Number
    • exclusiveMinimum: Exclusive lower bound on Number

    For Strings

    • minLength: Check minimum length of String
    • maxLength: Check maximum length of String
    • pattern: Validate using a regex pattern

    For Arrays and Iterables

    • minSize: Array must contain a minimum number of items
    • maxSize: Array must not contain more than a maximum number of items
    • uniqueItems: All items in the Array must be unique

    For Maps

    • minSize: Map must contain a minimum number of key value pairs
    • maxSize: Map must not contain more than a maximum number of key value pairs
    Source code(tar.gz)
    Source code(zip)
  • v0.0.2(Mar 9, 2018)

    This release supports validation of collections:

    • Array
    • Iterable (List, ...)
    • Map

    Next steps:

    • v0.1.0 Library of default constraints similar to JSON Schema
    • v0.2.0 i18n and l10n support for hints
    Source code(tar.gz)
    Source code(zip)
  • v0.0.1(Mar 9, 2018)

    Initial release in pretty much proof of concept state.

    What's working:

    • Type-safe builder
      • Works with properties on data classes
      • Recursively validate nested data classes
      • Validate nullable properties
    • Simple Hint/Error string interpolation with values from constraints

    Next steps:

    • v0.0.2 Validation of collection type properties
    • v0.1.0 Library of default constraints similar to JSON Schema
    • v0.2.0 i18n and l10n support for hints
    Source code(tar.gz)
    Source code(zip)
A multiplatform assertion library for Kotlin

Atrium is an open-source multiplatform assertion library for Kotlin with support for JVM, JS and Android. It is designed to support multiple APIs, dif

Robert Stoll 439 Dec 29, 2022
Powerful, elegant and flexible test framework for Kotlin with additional assertions, property testing and data driven testing

Kotest is a flexible and comprehensive testing tool for Kotlin with multiplatform support. To learn more about Kotest, visit kotest.io or see our quic

Kotest 3.8k Jan 3, 2023
mocking library for Kotlin

Kotlin Academy articles Check the series of articles "Mocking is not rocket science" at Kt. Academy describing MockK from the very basics of mocking u

MockK 4.8k Jan 3, 2023
Strikt is an assertion library for Kotlin intended for use with a test runner such as JUnit, Minutest, Spek, or KotlinTest.

Strikt is an assertion library for Kotlin intended for use with a test runner such as JUnit, Minutest, Spek, or KotlinTest.

Rob Fletcher 447 Dec 26, 2022
Fixtures for Kotlin providing generated values for unit testing

A tool to generate well-defined, but essentially random, input following the idea of constrained non-determinism.

Appmattus Limited 191 Dec 21, 2022
A Kotlin Android library for heuristics evasion that prevents your code from being tested.

EvadeMe An Android library for heuristics evasion that prevents your code from being tested. User Instructions Add the maven repository to your projec

Chris Basinger 29 Dec 26, 2022
Lightweight service for creating standalone mock, written in pure Kotlin with Netty container.

MockService The lightweight service for creating a standalone mock, written in pure Kotlin with Netty container. The service allows getting config fil

null 2 Oct 28, 2021
Proyecto de Kotlin y JPA sobre Hibernate, con algunos test usando JUnit 5 y Mockito.

Contactos Kotlin JPA Ejemplos de una aplicación de manejo de contactos con Kotlin y JPA. Usando para testear la aplicación JUnit 5 y Mockito. Almacena

José Luis González Sánchez 3 Sep 13, 2022
This is a sample API project for Rest Assured with Maven Kotlin DSL & JUnit 5

Kotlin-RestAssured Test Automation This is a sample API project for Rest Assured with Maven Kotlin DSL & JUnit 5 Introduction All the Test Cases kept

Dilshan Fernando 0 Dec 9, 2021
Android Sample Kotlin+ MVVM + Coroutines + Retrofit +Hilt+ Junit + Mockito

NTimes AppSample NY Times Most Popular Articles simple app to hit the NY Times Most Popular Articles API and show a list of articles, that shows detai

Amer Elsayed 0 Dec 27, 2021
Snapshot Testing framework for Kotlin.

KotlinSnapshot Snapshot Testing framework for Kotlin. What is this? Snapshot testing is an assertion strategy based on the comparision of the instance

Pedro Gómez 157 Nov 13, 2022
Automated tests using Rest-assured with Kotlin lang

Testes de API em Kotlin Pré-requisitos Instalar o Kotlin Ambiente Para executar os testes localmente, estou utilizando o ServeRest Link do Repo: https

Rafael Berçam 15 Dec 23, 2022
Selenium locators for Java/Kotlin that resemble the Testing Library (testing-library.com).

Selenium Testing Library Testing Library selectors available as Selenium locators for Kotlin/Java. Why? When I use Selenium, I don't want to depend on

Luís Soares 5 Dec 15, 2022
An Android library to build form and form validations easily.

FormBuilder An Android library to build form and form validations easily. Example COMING SOON Requirements Android 4.3+ Installation Add edit your bui

Dario Pellegrini 48 Jun 30, 2022
Kotlin/Native interop to libui: a portable GUI library

kotlin-libui Kotlin/Native bindings to the libui C library. libui is a C lightweight multi-platform UI library using native widgets on Linux (Gtk3), m

Mike Sinkovsky 611 Dec 30, 2022
A modular and portable open source XMPP client library written in Java for Android and Java (SE) VMs

Smack About Smack is an open-source, highly modular, easy to use, XMPP client library written in Java for Java SE compatible JVMs and Android. Being a

Ignite Realtime 2.3k Dec 28, 2022
A modular and portable open source XMPP client library written in Java for Android and Java (SE) VMs

Smack About Smack is an open-source, highly modular, easy to use, XMPP client library written in Java for Java SE compatible JVMs and Android. Being a

Ignite Realtime 2.3k Dec 21, 2021
Repo: Programming problems with solutions in Kotlin to help avid Kotlin learners to get a strong hold on Kotlin programming.

Kotlin_practice_problems Repo: Programming problems with solutions in Kotlin to help avid Kotlin learners to get a strong hold on Kotlin programming.

Aman 0 Oct 14, 2021
Mocking for Kotlin/Native and Kotlin Multiplatform using the Kotlin Symbol Processing API (KSP)

Mockative Mocking for Kotlin/Native and Kotlin Multiplatform using the Kotlin Symbol Processing API (KSP). Installation Mockative uses KSP to generate

Mockative 121 Dec 26, 2022
Run Kotlin/JS libraries in Kotlin/JVM and Kotlin/Native programs

Zipline This library streamlines using Kotlin/JS libraries from Kotlin/JVM and Kotlin/Native programs. It makes it possible to do continuous deploymen

Cash App 1.5k Dec 30, 2022