/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.libraries.pcc.chronicle.api.optics import com.android.libraries.pcc.chronicle.api.operation.Action typealias ListTraversal = Traversal, List, A, A> typealias ListMapping = Traversal, List, A, B> /** * A [Traversal] is a functional optic intended for getting or updating 0 to N items. * * In Chronicle it serves as a way to apply updates to lists, sets, and other collection types. */ abstract class Traversal( val sourceAccessPath: OpticalAccessPath, val targetAccessPath: OpticalAccessPath, val sourceEntityType: Class, val targetEntityType: Class, val sourceFieldType: Class, val targetFieldType: Class ) { /** Whether or not the [Traversal] is actually monomorphic. */ val isMonomorphic: Boolean = sourceEntityType == targetEntityType && sourceFieldType == targetFieldType /** * Given an [entity] of type [S], returns a [Sequence] containing every targeted item of type [A] * within the entity. */ abstract fun every(entity: S): Sequence /** * Given an [entity] of type [S], and a modifier mapping from [A] to [B], returns an instance of * [T] where each member of [entity] was converted from [A] to [B]. */ abstract fun modify(entity: S, modifier: (A) -> B): T /** * Similar to [modify] but the result type is an [Action], which allows for more nuanced * adjustment to the source data. */ abstract fun modifyWithAction(entity: S, modifier: (value: A) -> Action): Action /** Lift a [modifier] function mapping from [A] -> [B] to the scope of [S] an [T]. */ fun lift(modifier: (A) -> B): (S) -> T = { modify(it, modifier) } /** Compose this [Traversal] with another [Traversal]. */ @Suppress("UNCHECKED_CAST") // We do check, actually. infix fun compose( other: Traversal ): Traversal { require(this canCompose other) { "$this cannot compose with $other" } return object : Traversal( sourceAccessPath = sourceAccessPath compose other.sourceAccessPath, targetAccessPath = targetAccessPath compose other.targetAccessPath, sourceEntityType = sourceEntityType, targetEntityType = targetEntityType, sourceFieldType = other.sourceFieldType, targetFieldType = other.targetFieldType ) { override fun every(entity: S): Sequence = this@Traversal.every(entity).flatMap { other.every(it as AIn) } @Suppress("ALWAYS_NULL", "USELESS_CAST") // The [B] type variable must be nullable, but alas: type-erasure. override fun modify(entity: S, modifier: (NewA) -> NewB): T = this@Traversal.modify(entity) { if (it != null) { other.modify(it as AIn, modifier) } else { // If `it` is null, just return it - since `other` can't be a it as B } } @Suppress("ALWAYS_NULL", "USELESS_CAST") // The [B] type variable must be nullable, but alas: type-erasure. override fun modifyWithAction( entity: S, modifier: (value: NewA) -> Action ): Action { return this@Traversal.modifyWithAction(entity) { if (it != null) { other.modifyWithAction(it as AIn, modifier) } else { Action.Update(it as B) } } } } } /** * Returns whether or not the receiving [Traversal] can compose with the [other] [Traversal]. * * A [Traversal] can compose with another if and only if the original entity type of the [other] * can be assigned to the original field of the entity targeted by the LHS ***and*** if the * modified entity type of the [other] can be assigned to the modified field of the entity * targeted by the LHS. */ infix fun canCompose(other: Traversal<*, *, *, *>): Boolean = sourceFieldType.isAssignableFrom(other.sourceEntityType) && targetFieldType.isAssignableFrom(other.targetEntityType) override fun toString(): String { if (isMonomorphic) return "Traversal($sourceAccessPath)" return "Traversal($sourceAccessPath -> $targetAccessPath)" } companion object { /** An [OpticalAccessPath] representing a [Traversal] over a [List]. */ val LIST_TRAVERSAL_ACCESS_PATH = OpticalAccessPath("List", "forEach") /** Creates a [Traversal] operating on a [List] of elements of type [A]. */ inline fun list(): ListTraversal { val path = OpticalAccessPath("List<${A::class.java.simpleName}>") compose LIST_TRAVERSAL_ACCESS_PATH val dummyEntity = emptyList() return object : ListTraversal( sourceAccessPath = path, targetAccessPath = path, sourceEntityType = dummyEntity::class.java, targetEntityType = dummyEntity::class.java, sourceFieldType = A::class.java, targetFieldType = A::class.java ) { override fun every(entity: List): Sequence = entity.asSequence() override fun modify(entity: List, modifier: (A) -> A): List = entity.map(modifier) override fun modifyWithAction( entity: List, modifier: (value: A) -> Action ): Action> { return Action.Update( entity.mapNotNull { when (val res = modifier(it)) { Action.OmitFromParent -> null is Action.Update -> res.newValue // Immediately bubble-up root omissions and throwing results. is Action.OmitFromRoot -> return res is Action.Throw -> return res } } ) } } } /** * Creates a [Traversal] operating on a [List] of elements of type [A], mapping them to type [B] * . */ inline fun listMap(): ListMapping { val sourcePath = OpticalAccessPath("List<${A::class.java.simpleName}>") compose LIST_TRAVERSAL_ACCESS_PATH val targetPath = OpticalAccessPath("List<${B::class.java.simpleName}>") compose LIST_TRAVERSAL_ACCESS_PATH val dummySource = emptyList() val dummyTarget = emptyList() return object : Traversal, List, A, B>( sourceAccessPath = sourcePath, targetAccessPath = targetPath, sourceEntityType = dummySource::class.java, targetEntityType = dummyTarget::class.java, sourceFieldType = A::class.java, targetFieldType = B::class.java ) { override fun every(entity: List): Sequence = entity.asSequence() override fun modify(entity: List, modifier: (A) -> B): List = entity.map(modifier) override fun modifyWithAction( entity: List, modifier: (value: A) -> Action ): Action> { return Action.Update( entity.mapNotNull { when (val res = modifier(it)) { Action.OmitFromParent -> null is Action.Update -> res.newValue // Immediately bubble-up root omissions and throwing results. is Action.OmitFromRoot -> return res is Action.Throw -> return res } } ) } } } } }