Skip to content
This repository has been archived by the owner on Feb 8, 2024. It is now read-only.

Commit

Permalink
refactor(csv-#45): update internal implementation for reading a CSV file
Browse files Browse the repository at this point in the history
  • Loading branch information
LVMVRQUXL committed Nov 23, 2022
1 parent f1f670d commit 31bcea5
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 71 deletions.
113 changes: 47 additions & 66 deletions csv/src/main/kotlin/kotools/csv/Reader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,68 +6,40 @@ import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotools.types.collections.NotEmptyList
import kotools.types.collections.NotEmptyMap
import kotools.types.collections.toNotEmptyListOrNull
import kotools.types.collections.toNotEmptyMapOrThrow
import kotools.types.string.NotBlankString
import kotools.types.string.notBlankStringOrThrow
import kotools.types.string.toNotBlankStringOrNull
import java.io.File
import java.net.URL

// ---------- CSV reader ----------
// ---------- Reading operations ----------

/**
* Returns the [records][Record] in the CSV file matching this path, or returns
* a [CsvReaderResult.Exception.FileNotFound] if no file matches this path, or
* returns a [CsvReaderResult.Exception.FileHeaderWithBlankField] if the
* existing file has a header with a blank field, or returns a
* [CsvReaderResult.Exception.FileWithoutRecord] if the existing file doesn't
* contain a record.
* a [CsvFileNotFoundException] if no file matches this path, or returns a
* [CsvFileHeaderWithBlankFieldException] if the existing file has a header with
* a blank field, or returns a [CsvFileWithoutRecordException] if the existing
* file doesn't contain a record.
*/
internal suspend fun CsvPathResult.Success.read(): CsvReaderResult =
withContext(CoroutineName("CsvReader") + Dispatchers.IO) {
val url: URL = ClassLoader.getSystemResource(path.value)
?: return@withContext csvFileNotFoundException(path)
val file = File(url.path)
val csv: CsvReader = csvReader {
delimiter = ','
skipEmptyLine = true
}
val records: NotEmptyList<Record> = csv.readAllWithHeader(file)
.map { record: Map<String, String> ->
val fields: NotEmptyMap<NotBlankString, NotBlankString?> =
record.mapKeys {
it.key.toNotBlankStringOrNull()
?: return@withContext csvFileHeaderWithBlankField(
path
)
}
.mapValues { it.value.toNotBlankStringOrNull() }
.toNotEmptyMapOrThrow()
Record(fields)
}
.toNotEmptyListOrNull()
?: return@withContext csvFileWithoutRecord(path)
CsvReaderResult.Success(records)
}

// ---------- CsvReaderResult ----------

private fun csvFileNotFoundException(
path: NotBlankString
): CsvReaderResult.Exception.FileNotFound =
CsvReaderResult.Exception.FileNotFound(path)
withContext(CoroutineName("CsvReader") + Dispatchers.IO) { path.read() }

private fun csvFileHeaderWithBlankField(
path: NotBlankString
): CsvReaderResult.Exception.FileHeaderWithBlankField =
CsvReaderResult.Exception.FileHeaderWithBlankField(path)
private fun NotBlankString.read(): CsvReaderResult {
val url: URL = ClassLoader.getSystemResource(value)
?: return fileNotFoundException
val file = File(url.path)
val csv: CsvReader = csvReader {
delimiter = ','
skipEmptyLine = true
}
val records: NotEmptyList<Record> = csv.readAllWithHeader(file)
.toRecordsOrElse { return fileHeaderWithBlankFieldException }
.toNotEmptyListOrNull()
?: return fileWithoutRecordException
return CsvReaderResult.Success(records)
}

private fun csvFileWithoutRecord(
path: NotBlankString
): CsvReaderResult.Exception.FileWithoutRecord =
CsvReaderResult.Exception.FileWithoutRecord(path)
// ---------- Result ----------

internal inline fun CsvReaderResult.onException(
action: (CsvReaderResult.Exception) -> CsvReaderResult.Success
Expand All @@ -82,24 +54,33 @@ internal sealed interface CsvReaderResult {

sealed class Exception(reason: NotBlankString) :
IllegalStateException("The file at ${reason.value}."),
CsvReaderResult {
class FileNotFound(path: NotBlankString) : Exception(
notBlankStringOrThrow("\"$path\" doesn't exist")
)
CsvReaderResult
}

class FileHeaderWithBlankField(path: NotBlankString) : Exception(
notBlankStringOrThrow("\"$path\" has a header with a blank field")
)
// ---------- Exceptions ----------

class FileWithoutRecord(path: NotBlankString) : Exception(
notBlankStringOrThrow("\"$path\" doesn't contain a record")
)
}
}
private val NotBlankString.fileHeaderWithBlankFieldException:
CsvFileHeaderWithBlankFieldException
get() = CsvFileHeaderWithBlankFieldException(this)

internal class CsvFileHeaderWithBlankFieldException(path: NotBlankString) :
CsvReaderResult.Exception(
notBlankStringOrThrow("\"$path\" has a header with a blank field")
)

private val NotBlankString.fileNotFoundException: CsvFileNotFoundException
get() = CsvFileNotFoundException(this)

internal class CsvFileNotFoundException(path: NotBlankString) :
CsvReaderResult.Exception(
notBlankStringOrThrow("\"$path\" doesn't exist")
)

// ---------- Record ----------
private val NotBlankString.fileWithoutRecordException:
CsvFileWithoutRecordException
get() = CsvFileWithoutRecordException(this)

@JvmInline
internal value class Record(
val fields: NotEmptyMap<NotBlankString, NotBlankString?>
)
internal class CsvFileWithoutRecordException(path: NotBlankString) :
CsvReaderResult.Exception(
notBlankStringOrThrow("\"$path\" doesn't contain a record")
)
29 changes: 29 additions & 0 deletions csv/src/main/kotlin/kotools/csv/Record.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package kotools.csv

import kotools.types.collections.NotEmptyMap
import kotools.types.collections.toNotEmptyMapOrThrow
import kotools.types.string.NotBlankString
import kotools.types.string.toNotBlankStringOrNull
import kotools.types.string.toNotBlankStringOrThrow

private fun NotEmptyMap<NotBlankString, NotBlankString?>.toRecord(): Record =
Record(this)

private fun Map<String, String>.toRecordOrNull(): Record? =
takeIf(Map<String, String>::hasNotBlankKeys)
?.mapKeys { it.key.toNotBlankStringOrThrow() }
?.mapValues { it.value.toNotBlankStringOrNull() }
?.toNotEmptyMapOrThrow()
?.toRecord()

internal inline fun List<Map<String, String>>.toRecordsOrElse(
action: (Map<String, String>) -> Record
): List<Record> = map { it.toRecordOrNull() ?: action(it) }

private fun Map<String, String>.hasNotBlankKeys(): Boolean =
all { it.key.isNotBlank() }

@JvmInline
internal value class Record(
val fields: NotEmptyMap<NotBlankString, NotBlankString?>
)
9 changes: 4 additions & 5 deletions csv/src/test/kotlin/kotools/csv/ReaderTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ class CsvReaderTest {
.read()
when (result) {
is CsvReaderResult.Success -> fail()
is CsvReaderResult.Exception.FileNotFound -> result.message
.assertNotNull()
is CsvFileNotFoundException -> result.message.assertNotNull()
.isNotBlank()
.assertTrue()
is CsvReaderResult.Exception -> fail(result.message, result)
Expand All @@ -42,8 +41,8 @@ class CsvReaderTest {
.read()
when (result) {
is CsvReaderResult.Success -> fail()
is CsvReaderResult.Exception.FileHeaderWithBlankField -> result
.message.assertNotNull()
is CsvFileHeaderWithBlankFieldException -> result.message
.assertNotNull()
.isNotBlank()
.assertTrue()
is CsvReaderResult.Exception -> fail(result.message, result)
Expand All @@ -57,7 +56,7 @@ class CsvReaderTest {
.read()
when (result) {
is CsvReaderResult.Success -> fail()
is CsvReaderResult.Exception.FileWithoutRecord -> result.message
is CsvFileWithoutRecordException -> result.message
.assertNotNull()
.isNotBlank()
.assertTrue()
Expand Down

0 comments on commit 31bcea5

Please sign in to comment.