Skip to content

Commit

Permalink
Finish the mapping API and write some tests
Browse files Browse the repository at this point in the history
  • Loading branch information
swankjesse committed Apr 20, 2024
1 parent 9c05d2f commit 0bf63eb
Show file tree
Hide file tree
Showing 6 changed files with 272 additions and 70 deletions.
61 changes: 51 additions & 10 deletions okio/src/commonMain/kotlin/okio/FileSystemExtension.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,38 +15,79 @@
*/
package okio

import okio.FileSystemExtension.Mapping

/**
* Marks an object that can be attached to a [FileSystem], and that supplements the file system's
* capabilities.
*
* Implementations must support transforms to input and output paths with [PathMapper]. To simplify
* implementation, use [PathMapper.NONE] by default and use [chain] to combine mappers.
* Implementations must support transforms to input and output paths with [Mapping]. To simplify
* implementation, use [Mapping.NONE] by default and use [Mapping.chain] to combine mappings.
*
* ```kotlin
* class DiskUsageExtension private constructor(
* private val pathMapper: PathMapper,
* private val mapping: Mapping,
* ) : FileSystemExtension {
* constructor() : this(PathMapper.NONE)
* constructor() : this(Mapping.NONE)
*
* override fun map(pathMapper: PathMapper): FileSystemExtension {
* return DiskUsageExtension(chain(pathMapper, this.pathMapper))
* override fun map(outer: Mapping): FileSystemExtension {
* return DiskUsageExtension(mapping.chain(outer))
* }
*
* fun sizeOnDisk(path: Path): Long {
* val mappedPath = pathMapper.onPathParameter(path, "sizeOnDisk", "path")
* val mappedPath = mapping.mapParameter(path, "sizeOnDisk", "path")
* return lookUpSizeOnDisk(mappedPath)
* }
*
* fun largestFiles(): Sequence<Path> {
* val largestFiles: Sequence<Path> = lookUpLargestFiles()
* return largestFiles.map {
* pathMapper.onPathResult(it, "largestFiles")
* mapping.mapResult(it, "largestFiles")
* }
* }
* }
* ```
*/
interface FileSystemExtension {
/** Returns a file system of the same type, that applies [pathMapper] to all paths. */
fun map(pathMapper: PathMapper) : FileSystemExtension
/** Returns a file system of the same type, that applies [outer] to all paths. */
fun map(outer: Mapping): FileSystemExtension

abstract class Mapping {
abstract fun mapParameter(path: Path, functionName: String, parameterName: String): Path
abstract fun mapResult(path: Path, functionName: String): Path

fun chain(outer: Mapping): Mapping {
val inner = this
return object : Mapping() {
override fun mapParameter(path: Path, functionName: String, parameterName: String): Path {
return inner.mapParameter(
outer.mapParameter(
path,
functionName,
parameterName,
),
functionName,
parameterName,
)
}

override fun mapResult(path: Path, functionName: String): Path {
return outer.mapResult(
inner.mapResult(
path,
functionName,
),
functionName,
)
}
}
}

companion object {
val NONE = object : Mapping() {
override fun mapParameter(path: Path, functionName: String, parameterName: String) = path
override fun mapResult(path: Path, functionName: String) = path
}
}
}
}
20 changes: 19 additions & 1 deletion okio/src/commonMain/kotlin/okio/ForwardingFileSystem.kt
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,18 @@ open class ForwardingFileSystem internal constructor(
val delegate: FileSystem,
extensions: Map<KClass<*>, Any>,
) : FileSystem() {
/** Extensions added at this layer. Additional extensions may exist in [delegate]. */
internal val extensions = extensions.toMap()

/** Maps paths with [onPathParameter] and [onPathResult]. */
val extensionMapping: FileSystemExtension.Mapping = object : FileSystemExtension.Mapping() {
override fun mapParameter(path: Path, functionName: String, parameterName: String) =
onPathParameter(path, functionName, parameterName)

override fun mapResult(path: Path, functionName: String) =
onPathResult(path, functionName)
}

constructor(delegate: FileSystem) : this(delegate, emptyMap())

/**
Expand Down Expand Up @@ -148,7 +158,15 @@ open class ForwardingFileSystem internal constructor(
*/
open fun onPathResult(path: Path, functionName: String): Path = path

open fun <T : Any> onExtension(type: KClass<T>, extension: T): T = extension
/**
* Invoked each time an extension is returned from [ForwardingFileSystem.extension].
*
* Overrides of this function must call [FileSystemExtension.map] with [extensionMapping],
* otherwise path mapping will not be applied. Or call `super.onExtension()` to do this.
*/
open fun <T : FileSystemExtension> onExtension(type: KClass<T>, extension: T): T {
return type.cast(extension.map(extensionMapping))
}

@Throws(IOException::class)
override fun canonicalize(path: Path): Path {
Expand Down
31 changes: 2 additions & 29 deletions okio/src/commonMain/kotlin/okio/Okio.kt
Original file line number Diff line number Diff line change
Expand Up @@ -77,38 +77,11 @@ inline fun <T : Closeable?, R> T.use(block: (T) -> R): R {
* Returns a new file system that forwards all calls to this, and that also returns [extension]
* when it is requested.
*
* When [E] is requested on the returned file system, it will return [extension], regardless of what
* is returned by this file system.
* When [E] is requested on the returned file system, it will return [extension]. If this file
* system already has an extension of this type, [extension] takes precedence.
*/
inline fun <reified E : FileSystemExtension> FileSystem.extend(extension: E): FileSystem =
extend(E::class, extension)

/** Returns the extension for [E] if it exists, and null otherwise. */
inline fun <reified E : FileSystemExtension> FileSystem.extension(): E? = extension(E::class)


fun chain(outer: PathMapper, inner: PathMapper): PathMapper {
return object : PathMapper {
override fun onPathParameter(path: Path, functionName: String, parameterName: String): Path {
return inner.onPathParameter(
outer.onPathParameter(
path,
functionName,
parameterName,
),
functionName,
parameterName,
)
}

override fun onPathResult(path: Path, functionName: String): Path {
return outer.onPathResult(
inner.onPathResult(
path,
functionName,
),
functionName,
)
}
}
}
28 changes: 0 additions & 28 deletions okio/src/commonMain/kotlin/okio/PathMapper.kt

This file was deleted.

193 changes: 193 additions & 0 deletions okio/src/commonTest/kotlin/okio/FileSystemExtensionMappingTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/*
* Copyright (C) 2024 Square, Inc.
*
* 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 okio

import kotlin.test.Test
import kotlin.test.assertEquals
import okio.FileSystemExtension.Mapping
import okio.Path.Companion.toPath
import okio.fakefilesystem.FakeFileSystem

class FileSystemExtensionMappingTest {
private val rawFileSystem = FakeFileSystem()

@Test
fun happyPath() {
// Create a bunch of files in the /monday directory.
val mondayExtension = ChefExtension(rawFileSystem)
val mondayFs = rawFileSystem.extend(mondayExtension)
mondayFs.createDirectory("/monday".toPath())
mondayFs.write("/monday/breakfast.txt".toPath()) { writeUtf8("croissant") }
mondayFs.write("/monday/lunch.txt".toPath()) { writeUtf8("peanut butter sandwich") }
mondayFs.write("/monday/dinner.txt".toPath()) { writeUtf8("pizza") }

// Associate data with these files in the extension.
mondayExtension.setChef("/monday/dinner.txt".toPath(), "jesse")
mondayExtension.setChef("/monday/breakfast.txt".toPath(), "benoit")
mondayExtension.setChef("/monday/lunch.txt".toPath(), "jesse")

// Confirm we can query the extension.
assertEquals(
listOf(
"/monday/dinner.txt".toPath(),
"/monday/lunch.txt".toPath(),
),
mondayExtension.listForChef("/monday".toPath(), "jesse"),
)
assertEquals(
listOf(
"/monday/breakfast.txt".toPath(),
),
mondayExtension.listForChef("/monday".toPath(), "benoit"),
)

// Apply a path mapping transformation and confirm we can read the metadata.
val tuesdayFs = TuesdayFileSystem(mondayFs)
val tuesdayExtension = tuesdayFs.extension<ChefExtension>()!!
assertEquals(
listOf(
"/tuesday/dinner.txt".toPath(),
"/tuesday/lunch.txt".toPath(),
),
tuesdayExtension.listForChef("/tuesday".toPath(), "jesse"),
)
assertEquals(
listOf(
"/tuesday/breakfast.txt".toPath(),
),
tuesdayExtension.listForChef("/tuesday".toPath(), "benoit"),
)

// We should also be able to write through the extension...
tuesdayFs.write("/tuesday/snack.txt".toPath()) { writeUtf8("doritos") }
tuesdayExtension.setChef("/tuesday/snack.txt".toPath(), "jake")
assertEquals(
listOf(
"/tuesday/snack.txt".toPath(),
),
tuesdayExtension.listForChef("/tuesday".toPath(), "jake"),
)

// ...And the extension data should map all the way through to the source file system.
assertEquals(
listOf(
"/monday/snack.txt".toPath(),
),
mondayExtension.listForChef("/monday".toPath(), "jake"),
)
}

@Test
fun chainTransformations() {
val mondayExtension = ChefExtension(rawFileSystem)
val mondayFs = rawFileSystem.extend(mondayExtension)
mondayFs.createDirectory("/monday".toPath())
mondayFs.write("/monday/breakfast.txt".toPath()) { writeUtf8("croissant") }
mondayExtension.setChef("/monday/breakfast.txt".toPath(), "benoit")

// Map /monday to /tuesday.
val tuesdayFs = TuesdayFileSystem(mondayFs)

// Map / to /menu.
val menuFs = MenuFileSystem(tuesdayFs)
val menuExtension = menuFs.extension<ChefExtension>()!!

// Confirm we can read through the mappings.
assertEquals(
listOf(
"/menu/tuesday/breakfast.txt".toPath(),
),
menuExtension.listForChef("/menu/tuesday".toPath(), "benoit"),
)

// Confirm we can write through also.
menuFs.write("/menu/tuesday/lunch.txt".toPath()) { writeUtf8("tomato soup") }
menuExtension.setChef("/menu/tuesday/lunch.txt".toPath(), "jesse")
assertEquals(
"tomato soup",
mondayFs.read("/monday/lunch.txt".toPath()) { readUtf8() },
)
assertEquals(
listOf(
"/monday/lunch.txt".toPath(),
),
mondayExtension.listForChef("/monday".toPath(), "jesse"),
)

// Each extension gets its own mapping.
assertEquals(
"tomato soup",
tuesdayFs.read("/tuesday/lunch.txt".toPath()) { readUtf8() },
)
assertEquals(
listOf(
"/tuesday/lunch.txt".toPath(),
),
tuesdayFs.extension<ChefExtension>()!!.listForChef("/tuesday".toPath(), "jesse"),
)
}

/**
* This test extension associates paths with optional metadata: the chef of a file.
*
* When tests run there will be multiple instances of this extension, all sharing one [chefs]
* store, and each with its own [mapping]. The contents of [chefs] uses mapped paths for its keys.
*
* Real world extensions will have similar obligations for path mapping, but they'll likely do
* real things with the paths such as passing them to system APIs.
*/
class ChefExtension(
private val target: FileSystem,
private val chefs: MutableMap<Path, String> = mutableMapOf(),
private val mapping: Mapping = Mapping.NONE,
) : FileSystemExtension {
override fun map(outer: Mapping) = ChefExtension(target, chefs, mapping.chain(outer))

fun setChef(path: Path, chef: String) {
val mappedPath = mapping.mapParameter(path, "set", "path")
chefs[mappedPath] = chef
}

fun listForChef(dir: Path, chef: String): List<Path> {
val mappedDir = mapping.mapParameter(dir, "listForChef", "dir")
return target.list(mappedDir)
.filter { chefs[it] == chef }
.map { mapping.mapResult(it, "listForChef") }
}
}

class TuesdayFileSystem(delegate: FileSystem) : ForwardingFileSystem(delegate) {
private val monday = "/monday".toPath()
private val tuesday = "/tuesday".toPath()

override fun onPathParameter(path: Path, functionName: String, parameterName: String) =
monday / path.relativeTo(tuesday)

override fun onPathResult(path: Path, functionName: String) =
tuesday / path.relativeTo(monday)
}

class MenuFileSystem(delegate: FileSystem) : ForwardingFileSystem(delegate) {
private val root = "/".toPath()
private val menu = "/menu".toPath()

override fun onPathParameter(path: Path, functionName: String, parameterName: String) =
root / path.relativeTo(menu)

override fun onPathResult(path: Path, functionName: String) =
menu / path.relativeTo(root)
}
}

0 comments on commit 0bf63eb

Please sign in to comment.