From 614326e8bcc780e83c583c5eddf1da681a034e9c Mon Sep 17 00:00:00 2001 From: petersamokhin Date: Tue, 2 Mar 2021 23:58:59 +0000 Subject: [PATCH] Refactor, apply explicit API mode, enhance the token logic --- README.md | 4 +- build.gradle.kts | 17 +- .../com/petersamokhin/notionapi/Notion.kt | 93 +++----- .../com/petersamokhin/notionapi/NotionImpl.kt | 77 +++++++ .../notionapi/mapper/NotionPropertyMapper.kt | 12 +- .../notionapi/mapper/NotionResponseMapper.kt | 12 +- .../notionapi/model/NotionColumn.kt | 27 +++ .../notionapi/model/NotionCredentials.kt | 4 +- .../notionapi/model/NotionProperty.kt | 162 ++++++++++++++ .../notionapi/model/NotionRow.kt | 19 ++ .../notionapi/model/NotionTable.kt | 206 ++---------------- .../model/error/NotionAuthException.kt | 2 +- .../model/request/NotionRequestBody.kt | 6 +- .../model/response/NotionCollection.kt | 8 +- .../response/NotionCollectionColumnSchema.kt | 116 +++------- .../model/response/NotionCollectionView.kt | 6 +- .../model/response/NotionColumnType.kt | 64 ++++++ .../model/response/NotionResponse.kt | 10 +- .../notionapi/request/LoadPageChunkRequest.kt | 20 ++ .../request/QueryNotionCollectionRequest.kt | 19 +- .../notionapi/request/base/NotionRequest.kt | 14 +- .../serializer/NotionBooleanSerializer.kt | 16 +- .../notionapi/utils/JsonUtils.kt | 10 +- .../notionapi/utils/NotionUtils.kt | 26 ++- 24 files changed, 546 insertions(+), 404 deletions(-) create mode 100644 src/main/kotlin/com/petersamokhin/notionapi/NotionImpl.kt create mode 100644 src/main/kotlin/com/petersamokhin/notionapi/model/NotionColumn.kt create mode 100644 src/main/kotlin/com/petersamokhin/notionapi/model/NotionProperty.kt create mode 100644 src/main/kotlin/com/petersamokhin/notionapi/model/NotionRow.kt create mode 100644 src/main/kotlin/com/petersamokhin/notionapi/model/response/NotionColumnType.kt create mode 100644 src/main/kotlin/com/petersamokhin/notionapi/request/LoadPageChunkRequest.kt diff --git a/README.md b/README.md index 674f4fb..f9bd74c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# KNotion API +# Notion SDK / Kotlin / Unofficial API -Unofficial Notion.so API wrapper, written in Kotlin. Can be used in Java. +Unofficial Notion API wrapper (SDK), written in Kotlin. Can be used in Java. ### Install diff --git a/build.gradle.kts b/build.gradle.kts index 286c418..0ade2d2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,8 +4,21 @@ plugins { id("maven") } -kotlin.sourceSets { - all { +allprojects { + tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class).all { + JavaVersion.VERSION_1_8.toString().also { + kotlinOptions.jvmTarget = it + if (plugins.hasPlugin("org.jetbrains.kotlin.jvm")) { + sourceCompatibility = it + targetCompatibility = it + } + } + } +} + +kotlin { + explicitApiWarning() + sourceSets.all { languageSettings.useExperimentalAnnotation("kotlin.RequiresOptIn") } } diff --git a/src/main/kotlin/com/petersamokhin/notionapi/Notion.kt b/src/main/kotlin/com/petersamokhin/notionapi/Notion.kt index d0a5a9e..f07e2c5 100644 --- a/src/main/kotlin/com/petersamokhin/notionapi/Notion.kt +++ b/src/main/kotlin/com/petersamokhin/notionapi/Notion.kt @@ -1,75 +1,52 @@ package com.petersamokhin.notionapi -import com.petersamokhin.notionapi.mapper.mapTable import com.petersamokhin.notionapi.model.NotionCredentials import com.petersamokhin.notionapi.model.NotionTable -import com.petersamokhin.notionapi.model.error.NotionAuthException -import com.petersamokhin.notionapi.model.request.LoadPageChunkRequestBody -import com.petersamokhin.notionapi.model.request.Loader -import com.petersamokhin.notionapi.model.request.QueryCollectionRequestBody import com.petersamokhin.notionapi.model.response.NotionResponse -import com.petersamokhin.notionapi.request.LoadPageChunkRequest -import com.petersamokhin.notionapi.request.QueryNotionCollectionRequest import com.petersamokhin.notionapi.request.base.NotionRequest -import com.petersamokhin.notionapi.utils.dashifyId import io.ktor.client.* -import io.ktor.client.features.* import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.http.* +import io.ktor.util.* import kotlinx.serialization.json.Json -class Notion internal constructor(token: String, private var httpClient: HttpClient) { - init { - httpClient = httpClient.config { - defaultRequest { - header(HttpHeaders.Cookie, "$NOTION_TOKEN_COOKIE_KEY=$token") - } - } - } +public interface Notion { + public val token: String - suspend fun getCollection(json: Json, pageId: String, sortColumns: Boolean = false): NotionTable? { - val normalPageId = pageId.dashifyId() - val page = loadPage(normalPageId) + public suspend fun getCollection( + json: Json, + pageId: String, + sortColumns: Boolean = false + ): NotionTable? - val collectionId = page.recordMap.collectionsMap?.keys?.firstOrNull() ?: return null - val collectionViewId = page.recordMap.collectionViewsMap?.keys?.firstOrNull() ?: return null + public suspend fun loadPage( + pageId: String, + limit: Int = 50 + ): NotionResponse - val collectionResponse = queryCollection(collectionId, collectionViewId) + public suspend fun queryCollection( + collectionId: String, + collectionViewId: String, + limit: Int = 70 + ): NotionResponse - return collectionResponse.mapTable(json, sortColumns = sortColumns) - } + public fun setHttpClient(newHttpClient: HttpClient) - suspend fun loadPage(pageId: String, limit: Int = 50): NotionResponse { - return LoadPageChunkRequest(httpClient).execute( - LoadPageChunkRequestBody(pageId, limit, 0, false) - ) - } + public fun setToken(token: String) - suspend fun queryCollection(collectionId: String, collectionViewId: String, limit: Int = 70): NotionResponse { - return QueryNotionCollectionRequest(httpClient).execute( - QueryCollectionRequestBody( - collectionId, collectionViewId, Loader(limit, false, "table") - ) - ) - } + public fun close() - fun close() = httpClient.close() - - fun setHttpClient(newHttpClient: HttpClient) { - httpClient = newHttpClient - } - - companion object { - private const val NOTION_TOKEN_COOKIE_KEY = "token_v2" + public companion object { + public const val TOKEN_COOKIE_KEY: String = "token_v2" @JvmStatic - fun fromToken(token: String, httpClient: HttpClient): Notion { - return Notion(token, httpClient) - } + public fun fromToken(token: String, httpClient: HttpClient): Notion = + NotionImpl(token, httpClient) + @OptIn(KtorExperimentalAPI::class) @JvmStatic - suspend fun fromEmailAndPassword(credentials: NotionCredentials, httpClient: HttpClient): Notion { + public suspend fun fromEmailAndPassword(credentials: NotionCredentials, httpClient: HttpClient): Notion { val endpoint = "${NotionRequest.API_BASE_URL}/${NotionRequest.Endpoint.LOGIN_WITH_EMAIL}" val response = httpClient.post(endpoint) { headers.appendAll(NotionRequest.BASE_HEADERS) @@ -77,15 +54,17 @@ class Notion internal constructor(token: String, private var httpClient: HttpCli body = credentials } - println(response) - - val token = response.headers.getAll(HttpHeaders.SetCookie)?.firstOrNull { - it.contains("$NOTION_TOKEN_COOKIE_KEY=", true) - }?.split("; ")?.firstOrNull { - it.contains("$NOTION_TOKEN_COOKIE_KEY=", true) - }?.split("=")?.getOrNull(1) ?: throw NotionAuthException("No $NOTION_TOKEN_COOKIE_KEY in headers!") + val token = response.headers.getAll(HttpHeaders.SetCookie) + ?.asSequence() + ?.map(::parseServerSetCookieHeader) + ?.find { it.name == TOKEN_COOKIE_KEY } + ?.value + .orEmpty() - return fromToken(token, httpClient) + return fromToken( + token = token, + httpClient = httpClient + ) } } } \ No newline at end of file diff --git a/src/main/kotlin/com/petersamokhin/notionapi/NotionImpl.kt b/src/main/kotlin/com/petersamokhin/notionapi/NotionImpl.kt new file mode 100644 index 0000000..1643b5f --- /dev/null +++ b/src/main/kotlin/com/petersamokhin/notionapi/NotionImpl.kt @@ -0,0 +1,77 @@ +package com.petersamokhin.notionapi + +import com.petersamokhin.notionapi.mapper.mapTable +import com.petersamokhin.notionapi.model.NotionTable +import com.petersamokhin.notionapi.model.request.LoadPageChunkRequestBody +import com.petersamokhin.notionapi.model.request.Loader +import com.petersamokhin.notionapi.model.request.QueryCollectionRequestBody +import com.petersamokhin.notionapi.model.response.NotionResponse +import com.petersamokhin.notionapi.request.LoadPageChunkRequest +import com.petersamokhin.notionapi.request.QueryNotionCollectionRequest +import com.petersamokhin.notionapi.utils.dashifyId +import io.ktor.client.* +import io.ktor.client.features.* +import io.ktor.client.request.* +import io.ktor.http.* +import kotlinx.serialization.json.Json +import java.util.concurrent.atomic.AtomicReference + +internal class NotionImpl internal constructor( + token: String, + httpClient: HttpClient +) : Notion { + private val _token = AtomicReference(token) + + private var _httpClient: HttpClient = + httpClient.withToken() + + override val token: String + get() = _token.get() + + override suspend fun getCollection(json: Json, pageId: String, sortColumns: Boolean): NotionTable? { + val normalPageId = pageId.dashifyId() + val page = loadPage(normalPageId) + + val collectionId = page.recordMap.collectionsMap?.keys?.firstOrNull() ?: return null + val collectionViewId = page.recordMap.collectionViewsMap?.keys?.firstOrNull() ?: return null + + val collectionResponse = queryCollection(collectionId, collectionViewId) + + return collectionResponse.mapTable(json, sortColumns = sortColumns) + } + + override suspend fun loadPage(pageId: String, limit: Int): NotionResponse = + LoadPageChunkRequest(_httpClient).execute( + LoadPageChunkRequestBody(pageId, limit, 0, false) + ) + + override suspend fun queryCollection( + collectionId: String, + collectionViewId: String, + limit: Int + ): NotionResponse = + QueryNotionCollectionRequest(_httpClient).execute( + QueryCollectionRequestBody( + collectionId, collectionViewId, Loader(limit, false, "table") + ) + ) + + override fun setHttpClient(newHttpClient: HttpClient) { + close() + _httpClient = newHttpClient.withToken() + } + + override fun setToken(token: String) { + _token.set(token) + } + + override fun close(): Unit = + _httpClient.close() + + private fun HttpClient.withToken(): HttpClient = + config { + defaultRequest { + header(HttpHeaders.Cookie, "${Notion.TOKEN_COOKIE_KEY}=${_token.get()}") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/petersamokhin/notionapi/mapper/NotionPropertyMapper.kt b/src/main/kotlin/com/petersamokhin/notionapi/mapper/NotionPropertyMapper.kt index fdcc0e9..6d558a4 100644 --- a/src/main/kotlin/com/petersamokhin/notionapi/mapper/NotionPropertyMapper.kt +++ b/src/main/kotlin/com/petersamokhin/notionapi/mapper/NotionPropertyMapper.kt @@ -13,13 +13,15 @@ import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.decodeFromJsonElement -fun NotionCollection.title() = value.name?.trimNotionTextField() +public fun NotionCollection.title(): String? = + value.name?.trimNotionTextField() -fun NotionCollection.description() = value.description?.trimNotionTextField() +public fun NotionCollection.description(): String? = + value.description?.trimNotionTextField() -fun parseNotionColumn(json: Json, name: String, type: NotionColumnType, field: JsonArray?): NotionColumn { - if (field.isNullOrEmpty()) return NotionColumn.SingleValue(name, type, null) - val masterList = field.filterIsInstance() +public fun JsonArray?.parseNotionColumn(json: Json, name: String, type: NotionColumnType): NotionColumn { + if (isNullOrEmpty()) return NotionColumn.SingleValue(name, type, null) + val masterList = filterIsInstance() return when (type) { NotionColumnType.Title, NotionColumnType.Text, NotionColumnType.Number, diff --git a/src/main/kotlin/com/petersamokhin/notionapi/mapper/NotionResponseMapper.kt b/src/main/kotlin/com/petersamokhin/notionapi/mapper/NotionResponseMapper.kt index 4e304b9..d4b0a45 100644 --- a/src/main/kotlin/com/petersamokhin/notionapi/mapper/NotionResponseMapper.kt +++ b/src/main/kotlin/com/petersamokhin/notionapi/mapper/NotionResponseMapper.kt @@ -1,12 +1,14 @@ package com.petersamokhin.notionapi.mapper -import com.petersamokhin.notionapi.model.* +import com.petersamokhin.notionapi.model.NotionColumn +import com.petersamokhin.notionapi.model.NotionRow +import com.petersamokhin.notionapi.model.NotionTable import com.petersamokhin.notionapi.model.response.NotionBlock import com.petersamokhin.notionapi.model.response.NotionCollection import com.petersamokhin.notionapi.model.response.NotionResponse -import kotlinx.serialization.json.* +import kotlinx.serialization.json.Json -fun NotionResponse.mapTable(json: Json, sortColumns: Boolean = false): NotionTable? { +public fun NotionResponse.mapTable(json: Json, sortColumns: Boolean = false): NotionTable? { val collectionId = recordMap.collectionsMap?.keys?.firstOrNull() ?: return null val collection = recordMap.collectionsMap[collectionId] @@ -25,7 +27,7 @@ fun NotionResponse.mapTable(json: Json, sortColumns: Boolean = false): NotionTab return blocks?.let { collection?.mapTable(json, it, sortMap) } } -fun NotionCollection.mapTable( +public fun NotionCollection.mapTable( json: Json, blocks: List, sortMap: Map? = null @@ -47,7 +49,7 @@ fun NotionCollection.mapTable( schemaItem.name.also { name -> val field = props[innerRowKey] - rowItems[name] = parseNotionColumn(json, name, schemaItem.type, field) + rowItems[name] = field.parseNotionColumn(json, name, schemaItem.type) } } diff --git a/src/main/kotlin/com/petersamokhin/notionapi/model/NotionColumn.kt b/src/main/kotlin/com/petersamokhin/notionapi/model/NotionColumn.kt new file mode 100644 index 0000000..963f763 --- /dev/null +++ b/src/main/kotlin/com/petersamokhin/notionapi/model/NotionColumn.kt @@ -0,0 +1,27 @@ +package com.petersamokhin.notionapi.model + +import com.petersamokhin.notionapi.model.response.NotionColumnType +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +public sealed class NotionColumn { + public abstract val name: String + public abstract val type: NotionColumnType + + @Serializable + @SerialName("single_value") + public data class SingleValue( + override val name: String, + override val type: NotionColumnType, + val value: NotionProperty? + ) : NotionColumn() + + @Serializable + @SerialName("multi_value") + public data class MultiValue( + override val name: String, + override val type: NotionColumnType, + val values: List + ) : NotionColumn() +} \ No newline at end of file diff --git a/src/main/kotlin/com/petersamokhin/notionapi/model/NotionCredentials.kt b/src/main/kotlin/com/petersamokhin/notionapi/model/NotionCredentials.kt index 7d246c4..be7922d 100644 --- a/src/main/kotlin/com/petersamokhin/notionapi/model/NotionCredentials.kt +++ b/src/main/kotlin/com/petersamokhin/notionapi/model/NotionCredentials.kt @@ -3,7 +3,7 @@ package com.petersamokhin.notionapi.model import kotlinx.serialization.Serializable @Serializable -data class NotionCredentials( +public data class NotionCredentials( val email: String, val password: String -) +) \ No newline at end of file diff --git a/src/main/kotlin/com/petersamokhin/notionapi/model/NotionProperty.kt b/src/main/kotlin/com/petersamokhin/notionapi/model/NotionProperty.kt new file mode 100644 index 0000000..9670e42 --- /dev/null +++ b/src/main/kotlin/com/petersamokhin/notionapi/model/NotionProperty.kt @@ -0,0 +1,162 @@ +package com.petersamokhin.notionapi.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.* + +@Serializable +public data class NotionProperty(val label: String, val value: Value? = null) { + + @Serializable + public sealed class Value { + public abstract fun simpleJson(json: Json): JsonElement + + @Serializable + @SerialName("title") + public data class Title(val text: String) : Value() { + override fun simpleJson(json: Json): JsonPrimitive = + JsonPrimitive(text) + } + + @Serializable + @SerialName("text") + public data class Text(val text: String) : Value() { + override fun simpleJson(json: Json): JsonPrimitive = + JsonPrimitive(text) + } + + @Serializable + @SerialName("checkbox") + public data class Checkbox(val checked: Boolean) : Value() { + override fun simpleJson(json: Json): JsonPrimitive = + JsonPrimitive(checked) + } + + @Serializable + @SerialName("number") + public data class Number(val number: Double) : Value() { + override fun simpleJson(json: Json): JsonPrimitive = + JsonPrimitive(number) + } + + @Serializable + @SerialName("select") + public data class Select(val option: String) : Value() { + override fun simpleJson(json: Json): JsonPrimitive = + JsonPrimitive(option) + } + + @Serializable + @SerialName("multi_select") + public data class MultiSelect(val options: List) : Value() { + override fun simpleJson(json: Json): JsonArray = + JsonArray(options.map(::JsonPrimitive)) + } + + @Serializable + @SerialName("url") + public data class Url(val url: String) : Value() { + override fun simpleJson(json: Json): JsonPrimitive = + JsonPrimitive(url) + } + + @Serializable + @SerialName("email") + public data class Email(val email: String) : Value() { + override fun simpleJson(json: Json): JsonPrimitive = + JsonPrimitive(email) + } + + @Serializable + @SerialName("phone_number") + public data class PhoneNumber(@SerialName("phone_number") val phoneNumber: String) : Value() { + override fun simpleJson(json: Json): JsonPrimitive = + JsonPrimitive(phoneNumber) + } + + @Serializable + @SerialName("entry") + public sealed class Entry : Value() { + + @Serializable + @SerialName("person") + public data class Person(val id: String) : Entry() { + override fun simpleJson(json: Json): JsonPrimitive = + JsonPrimitive(id) + } + + @Serializable + @SerialName("file") + public data class File(val url: String) : Entry() { + override fun simpleJson(json: Json): JsonPrimitive = + JsonPrimitive(url) + } + + @Serializable + @SerialName("date") + public data class Date( + val type: Type, + @SerialName("start_date") + val startDate: String, + @SerialName("time_zone") + val timeZone: String? = null, + val reminder: Reminder? = null, + @SerialName("end_date") + val endDate: String? = null, + @SerialName("end_time") + val endTime: String? = null, + @SerialName("start_time") + val startTime: String? = null + ) : Entry() { + @Serializable + public enum class Type { + @SerialName("date") + Date, + + @SerialName("datetime") + DateTime, + + @SerialName("datetimerange") + DateTimeRange + } + + @Serializable + public data class Reminder( + val time: String, + val unit: ReminderTimeUnit, + val value: Int + ) { + @Serializable + public enum class ReminderTimeUnit { + @SerialName("week") + Week, + + @SerialName("day") + Day, + + @SerialName("hour") + Hour, + + @SerialName("monute") + Minute + } + } + + override fun simpleJson(json: Json): JsonElement = + json.encodeToJsonElement(this) + } + + @Serializable + public enum class Type { + @SerialName("u") + Person, + + @SerialName("d") + Date, + + @SerialName("a") + Link + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/petersamokhin/notionapi/model/NotionRow.kt b/src/main/kotlin/com/petersamokhin/notionapi/model/NotionRow.kt new file mode 100644 index 0000000..322968d --- /dev/null +++ b/src/main/kotlin/com/petersamokhin/notionapi/model/NotionRow.kt @@ -0,0 +1,19 @@ +package com.petersamokhin.notionapi.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +public data class NotionRow( + val properties: Map, + @SerialName("meta_info") + val metaInfo: MetaInfo +) { + @Serializable + public data class MetaInfo( + val lastEditedBy: String, + val lastEditedTime: Long, + val createdBy: String, + val createdTime: Long + ) +} \ No newline at end of file diff --git a/src/main/kotlin/com/petersamokhin/notionapi/model/NotionTable.kt b/src/main/kotlin/com/petersamokhin/notionapi/model/NotionTable.kt index c6be829..05ef28d 100644 --- a/src/main/kotlin/com/petersamokhin/notionapi/model/NotionTable.kt +++ b/src/main/kotlin/com/petersamokhin/notionapi/model/NotionTable.kt @@ -1,20 +1,23 @@ package com.petersamokhin.notionapi.model import com.petersamokhin.notionapi.model.response.NotionCollectionColumnSchema -import com.petersamokhin.notionapi.model.response.NotionColumnType -import kotlinx.serialization.SerialName +import com.petersamokhin.notionapi.utils.get import kotlinx.serialization.Serializable import kotlinx.serialization.json.* @Serializable -data class NotionTable( +public data class NotionTable( val title: String?, val description: String?, val rows: List, val schema: Map ) { - fun simpleJson(json: Json): JsonElement { - return JsonObject( + /** + * Converts the table into a readable json object, + * which can be mapped into a model + */ + public fun simpleJson(json: Json): JsonElement = + JsonObject( mapOf( "title" to JsonPrimitive(title), "description" to JsonPrimitive(description), @@ -40,193 +43,14 @@ data class NotionTable( }.let(::JsonArray) ) ) - } - fun simpleJsonRows(json: Json): JsonArray { - val simpleJson = simpleJson(json) - - return simpleJson.jsonObject["rows"]?.jsonArray?.mapNotNull { it.jsonObject["properties"] }?.let(::JsonArray) + /** + * Get this table's rows as an array of arrays + */ + public fun simpleJsonRows(json: Json): JsonArray = + simpleJson(json)["rows"]?.jsonArray + ?.mapNotNull { it["properties"] } + ?.let(::JsonArray) ?: JsonArray(emptyList()) - } -} - -@Serializable -data class NotionRow( - val properties: Map, - @SerialName("meta_info") - val metaInfo: MetaInfo -) { - @Serializable - data class MetaInfo( - val lastEditedBy: String, - val lastEditedTime: Long, - val createdBy: String, - val createdTime: Long - ) } -@Serializable -sealed class NotionColumn { - abstract val name: String - abstract val type: NotionColumnType - - @Serializable - @SerialName("single_value") - data class SingleValue( - override val name: String, - override val type: NotionColumnType, - val value: NotionProperty? - ) : NotionColumn() - - @Serializable - @SerialName("multi_value") - data class MultiValue( - override val name: String, - override val type: NotionColumnType, - val values: List - ) : NotionColumn() -} - -@Serializable -data class NotionProperty(val label: String, val value: Value? = null) { - - @Serializable - sealed class Value { - abstract fun simpleJson(json: Json): JsonElement - - @Serializable - @SerialName("title") - data class Title(val text: String) : Value() { - override fun simpleJson(json: Json) = JsonPrimitive(text) - } - - @Serializable - @SerialName("text") - data class Text(val text: String) : Value() { - override fun simpleJson(json: Json) = JsonPrimitive(text) - } - - @Serializable - @SerialName("checkbox") - data class Checkbox(val checked: Boolean) : Value() { - override fun simpleJson(json: Json) = JsonPrimitive(checked) - } - - @Serializable - @SerialName("number") - data class Number(val number: Double) : Value() { - override fun simpleJson(json: Json) = JsonPrimitive(number) - } - - @Serializable - @SerialName("select") - data class Select(val option: String) : Value() { - override fun simpleJson(json: Json) = JsonPrimitive(option) - } - - @Serializable - @SerialName("multi_select") - data class MultiSelect(val options: List) : Value() { - override fun simpleJson(json: Json) = JsonArray(options.map(::JsonPrimitive)) - } - - @Serializable - @SerialName("url") - data class Url(val url: String) : Value() { - override fun simpleJson(json: Json) = JsonPrimitive(url) - } - - @Serializable - @SerialName("email") - data class Email(val email: String) : Value() { - override fun simpleJson(json: Json) = JsonPrimitive(email) - } - - @Serializable - @SerialName("phone_number") - data class PhoneNumber(@SerialName("phone_number") val phoneNumber: String) : Value() { - override fun simpleJson(json: Json) = JsonPrimitive(phoneNumber) - } - - @Serializable - @SerialName("entry") - sealed class Entry : Value() { - @Serializable - @SerialName("person") - data class Person(val id: String) : Entry() { - override fun simpleJson(json: Json) = JsonPrimitive(id) - } - - @Serializable - @SerialName("file") - data class File(val url: String) : Entry() { - override fun simpleJson(json: Json) = JsonPrimitive(url) - } - - @Serializable - @SerialName("date") - data class Date( - val type: Type, - @SerialName("start_date") - val startDate: String, - @SerialName("time_zone") - val timeZone: String? = null, - val reminder: Reminder? = null, - @SerialName("end_date") - val endDate: String? = null, - @SerialName("end_time") - val endTime: String? = null, - @SerialName("start_time") - val startTime: String? = null - ) : Entry() { - @Serializable - enum class Type { - @SerialName("date") - Date, - - @SerialName("datetime") - DateTime, - - @SerialName("datetimerange") - DateTimeRange - } - - @Serializable - data class Reminder( - val time: String, - val unit: ReminderTimeUnit, - val value: Int - ) { - @Serializable - enum class ReminderTimeUnit { - @SerialName("week") - Week, - - @SerialName("day") - Day, - - @SerialName("hour") - Hour, - - @SerialName("monute") - Minute - } - } - - override fun simpleJson(json: Json) = json.encodeToJsonElement(this) - } - - @Serializable - enum class Type { - @SerialName("u") - Person, - - @SerialName("d") - Date, - - @SerialName("a") - Link - } - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/petersamokhin/notionapi/model/error/NotionAuthException.kt b/src/main/kotlin/com/petersamokhin/notionapi/model/error/NotionAuthException.kt index bc83741..a4f52cf 100644 --- a/src/main/kotlin/com/petersamokhin/notionapi/model/error/NotionAuthException.kt +++ b/src/main/kotlin/com/petersamokhin/notionapi/model/error/NotionAuthException.kt @@ -1,3 +1,3 @@ package com.petersamokhin.notionapi.model.error -class NotionAuthException(message: String) : Exception(message) \ No newline at end of file +public class NotionAuthException(message: String) : Exception(message) \ No newline at end of file diff --git a/src/main/kotlin/com/petersamokhin/notionapi/model/request/NotionRequestBody.kt b/src/main/kotlin/com/petersamokhin/notionapi/model/request/NotionRequestBody.kt index b3499cb..b451393 100644 --- a/src/main/kotlin/com/petersamokhin/notionapi/model/request/NotionRequestBody.kt +++ b/src/main/kotlin/com/petersamokhin/notionapi/model/request/NotionRequestBody.kt @@ -3,21 +3,21 @@ package com.petersamokhin.notionapi.model.request import kotlinx.serialization.Serializable @Serializable -data class QueryCollectionRequestBody( +public data class QueryCollectionRequestBody( val collectionId: String, val collectionViewId: String, val loader: Loader ) @Serializable -data class Loader( +public data class Loader( val limit: Int, val loadContentCover: Boolean, val type: String ) @Serializable -data class LoadPageChunkRequestBody( +public data class LoadPageChunkRequestBody( val pageId: String, val limit: Int, val chunkNumber: Int, diff --git a/src/main/kotlin/com/petersamokhin/notionapi/model/response/NotionCollection.kt b/src/main/kotlin/com/petersamokhin/notionapi/model/response/NotionCollection.kt index 7c15d1e..f84552e 100644 --- a/src/main/kotlin/com/petersamokhin/notionapi/model/response/NotionCollection.kt +++ b/src/main/kotlin/com/petersamokhin/notionapi/model/response/NotionCollection.kt @@ -4,13 +4,13 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class NotionCollection( +public data class NotionCollection( val role: String, val value: NotionCollectionValue ) @Serializable -data class NotionCollectionValue( +public data class NotionCollectionValue( val id: String, val name: NotionTextField? = null, @SerialName("parent_id") @@ -28,13 +28,13 @@ data class NotionCollectionValue( ) @Serializable -data class NotionCollectionCoverFormat( +public data class NotionCollectionCoverFormat( @SerialName("collection_cover_position") val collectionCoverPosition: Double ) @Serializable -data class NotionCollectionAggregationResult( +public data class NotionCollectionAggregationResult( val id: String? = null, val value: Int? = null ) \ No newline at end of file diff --git a/src/main/kotlin/com/petersamokhin/notionapi/model/response/NotionCollectionColumnSchema.kt b/src/main/kotlin/com/petersamokhin/notionapi/model/response/NotionCollectionColumnSchema.kt index 317acd4..3af7652 100644 --- a/src/main/kotlin/com/petersamokhin/notionapi/model/response/NotionCollectionColumnSchema.kt +++ b/src/main/kotlin/com/petersamokhin/notionapi/model/response/NotionCollectionColumnSchema.kt @@ -4,30 +4,30 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonElement -typealias NotionTextField = List> +public typealias NotionTextField = List> @Serializable -sealed class NotionCollectionColumnSchema { - abstract val name: String - abstract val type: NotionColumnType +public sealed class NotionCollectionColumnSchema { + public abstract val name: String + public abstract val type: NotionColumnType @Serializable @SerialName("title") - data class Title(override val name: String, override val type: NotionColumnType) : NotionCollectionColumnSchema() + public data class Title(override val name: String, override val type: NotionColumnType) : NotionCollectionColumnSchema() @Serializable @SerialName("text") - data class Text(override val name: String, override val type: NotionColumnType) : NotionCollectionColumnSchema() + public data class Text(override val name: String, override val type: NotionColumnType) : NotionCollectionColumnSchema() @Serializable @SerialName("number") - data class Number( + public data class Number( override val name: String, override val type: NotionColumnType, @SerialName("number_format") val numberFormat: String? = null ) : NotionCollectionColumnSchema() { @Serializable - enum class Format { + public enum class Format { @SerialName("number") Number, @@ -74,15 +74,15 @@ sealed class NotionCollectionColumnSchema { @Serializable @SerialName("email") - data class Email(override val name: String, override val type: NotionColumnType) : NotionCollectionColumnSchema() + public data class Email(override val name: String, override val type: NotionColumnType) : NotionCollectionColumnSchema() @Serializable @SerialName("checkbox") - data class Checkbox(override val name: String, override val type: NotionColumnType) : NotionCollectionColumnSchema() + public data class Checkbox(override val name: String, override val type: NotionColumnType) : NotionCollectionColumnSchema() @Serializable @SerialName("select") - data class Select( + public data class Select( override val name: String, override val type: NotionColumnType, val options: List @@ -90,7 +90,7 @@ sealed class NotionCollectionColumnSchema { @Serializable @SerialName("multi_select") - data class MultiSelect( + public data class MultiSelect( override val name: String, override val type: NotionColumnType, val options: List @@ -98,18 +98,18 @@ sealed class NotionCollectionColumnSchema { @Serializable @SerialName("url") - data class Url(override val name: String, override val type: NotionColumnType) : NotionCollectionColumnSchema() + public data class Url(override val name: String, override val type: NotionColumnType) : NotionCollectionColumnSchema() @Serializable @SerialName("date") - data class Date( + public data class Date( override val name: String, override val type: NotionColumnType, @SerialName("time_format") val timeFormat: String? = null, @SerialName("date_format") val dateFormat: String? = null ) : NotionCollectionColumnSchema() { @Serializable - enum class TimeFormat { + public enum class TimeFormat { @SerialName("LT") H_12, @@ -118,7 +118,7 @@ sealed class NotionCollectionColumnSchema { } @Serializable - enum class DateFormat { + public enum class DateFormat { @SerialName("ll") // or null Full, @@ -138,45 +138,45 @@ sealed class NotionCollectionColumnSchema { @Serializable @SerialName("person") - data class Person( + public data class Person( override val name: String, override val type: NotionColumnType ) : NotionCollectionColumnSchema() @Serializable @SerialName("phone_number") - data class PhoneNumber( + public data class PhoneNumber( override val name: String, override val type: NotionColumnType ) : NotionCollectionColumnSchema() @Serializable @SerialName("file") - data class File(override val name: String, override val type: NotionColumnType) : NotionCollectionColumnSchema() + public data class File(override val name: String, override val type: NotionColumnType) : NotionCollectionColumnSchema() @Serializable @SerialName("last_edited_time") - data class LastEditedTime(override val name: String, override val type: NotionColumnType) : NotionCollectionColumnSchema() + public data class LastEditedTime(override val name: String, override val type: NotionColumnType) : NotionCollectionColumnSchema() @Serializable @SerialName("last_edited_by") - data class LastEditedBy(override val name: String, override val type: NotionColumnType) : NotionCollectionColumnSchema() + public data class LastEditedBy(override val name: String, override val type: NotionColumnType) : NotionCollectionColumnSchema() @Serializable @SerialName("created_time") - data class CreatedTime(override val name: String, override val type: NotionColumnType) : NotionCollectionColumnSchema() + public data class CreatedTime(override val name: String, override val type: NotionColumnType) : NotionCollectionColumnSchema() @Serializable @SerialName("created_by") - data class CreatedBy(override val name: String, override val type: NotionColumnType) : NotionCollectionColumnSchema() + public data class CreatedBy(override val name: String, override val type: NotionColumnType) : NotionCollectionColumnSchema() @Serializable @SerialName("rollup") - data class Rollup(override val name: String, override val type: NotionColumnType) : NotionCollectionColumnSchema() + public data class Rollup(override val name: String, override val type: NotionColumnType) : NotionCollectionColumnSchema() @Serializable @SerialName("relation") - data class Relation( + public data class Relation( override val name: String, override val type: NotionColumnType, val property: String, @@ -186,7 +186,7 @@ sealed class NotionCollectionColumnSchema { @Serializable @SerialName("formula") - data class Formula( + public data class Formula( override val name: String, override val type: NotionColumnType, val formula: JsonElement @@ -194,71 +194,11 @@ sealed class NotionCollectionColumnSchema { } @Serializable -data class NotionPropertyFormat( +public data class NotionPropertyFormat( val width: Int? = null, val visible: Boolean, val property: String ) @Serializable -data class NotionSelectOption(val id: String, val color: String, val value: String) - -@Serializable -enum class NotionColumnType { - @SerialName("title") - Title, - - @SerialName("text") - Text, - - @SerialName("number") - Number, - - @SerialName("checkbox") - Checkbox, - - @SerialName("select") - Select, - - @SerialName("multi_select") - MultiSelect, - - @SerialName("email") - Email, - - @SerialName("url") - Url, - - @SerialName("date") - Date, - - @SerialName("person") - Person, - - @SerialName("phone_number") - PhoneNumber, - - @SerialName("file") - File, - - @SerialName("last_edited_time") - LastEditedTime, - - @SerialName("last_edited_by") - LastEditedBy, - - @SerialName("created_time") - CreatedTime, - - @SerialName("created_by") - CreatedBy, - - @SerialName("rollup") - Rollup, - - @SerialName("relation") - Relation, - - @SerialName("formula") - Formula -} \ No newline at end of file +public data class NotionSelectOption(val id: String, val color: String, val value: String) \ No newline at end of file diff --git a/src/main/kotlin/com/petersamokhin/notionapi/model/response/NotionCollectionView.kt b/src/main/kotlin/com/petersamokhin/notionapi/model/response/NotionCollectionView.kt index 75e99c1..fe58b28 100644 --- a/src/main/kotlin/com/petersamokhin/notionapi/model/response/NotionCollectionView.kt +++ b/src/main/kotlin/com/petersamokhin/notionapi/model/response/NotionCollectionView.kt @@ -4,13 +4,13 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class NotionCollectionView( +public data class NotionCollectionView( val role: String, val value: NotionCollectionViewValue ) @Serializable -data class NotionCollectionViewValue( +public data class NotionCollectionViewValue( val id: String, val version: Int, val type: String, @@ -24,7 +24,7 @@ data class NotionCollectionViewValue( ) @Serializable -data class NotionCollectionViewValueFormat( +public data class NotionCollectionViewValueFormat( @SerialName("table_wrap") val tableWrap: Boolean, @SerialName("table_properties") diff --git a/src/main/kotlin/com/petersamokhin/notionapi/model/response/NotionColumnType.kt b/src/main/kotlin/com/petersamokhin/notionapi/model/response/NotionColumnType.kt new file mode 100644 index 0000000..9d0a8cf --- /dev/null +++ b/src/main/kotlin/com/petersamokhin/notionapi/model/response/NotionColumnType.kt @@ -0,0 +1,64 @@ +package com.petersamokhin.notionapi.model.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +public enum class NotionColumnType { + @SerialName("title") + Title, + + @SerialName("text") + Text, + + @SerialName("number") + Number, + + @SerialName("checkbox") + Checkbox, + + @SerialName("select") + Select, + + @SerialName("multi_select") + MultiSelect, + + @SerialName("email") + Email, + + @SerialName("url") + Url, + + @SerialName("date") + Date, + + @SerialName("person") + Person, + + @SerialName("phone_number") + PhoneNumber, + + @SerialName("file") + File, + + @SerialName("last_edited_time") + LastEditedTime, + + @SerialName("last_edited_by") + LastEditedBy, + + @SerialName("created_time") + CreatedTime, + + @SerialName("created_by") + CreatedBy, + + @SerialName("rollup") + Rollup, + + @SerialName("relation") + Relation, + + @SerialName("formula") + Formula +} \ No newline at end of file diff --git a/src/main/kotlin/com/petersamokhin/notionapi/model/response/NotionResponse.kt b/src/main/kotlin/com/petersamokhin/notionapi/model/response/NotionResponse.kt index 1970088..924bacd 100644 --- a/src/main/kotlin/com/petersamokhin/notionapi/model/response/NotionResponse.kt +++ b/src/main/kotlin/com/petersamokhin/notionapi/model/response/NotionResponse.kt @@ -5,26 +5,26 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonArray @Serializable -data class NotionResponse( +public data class NotionResponse( val recordMap: NotionRecordMap, val result: NotionResult? = null ) @Serializable -data class NotionRecordMap( +public data class NotionRecordMap( @SerialName("block") val blocksMap: Map? = null, @SerialName("collection") val collectionsMap: Map? = null, @SerialName("collection_view") val collectionViewsMap: Map? = null ) @Serializable -data class NotionBlock( +public data class NotionBlock( val role: String, val value: NotionBlockValue ) @Serializable -data class NotionBlockValue( +public data class NotionBlockValue( val alive: Boolean, val version: Int, val type: String, @@ -58,7 +58,7 @@ data class NotionBlockValue( ) @Serializable -data class NotionResult( +public data class NotionResult( val blockIds: List? = null, val aggregationResults: List? = null, val total: Int? = null, diff --git a/src/main/kotlin/com/petersamokhin/notionapi/request/LoadPageChunkRequest.kt b/src/main/kotlin/com/petersamokhin/notionapi/request/LoadPageChunkRequest.kt new file mode 100644 index 0000000..5744fc0 --- /dev/null +++ b/src/main/kotlin/com/petersamokhin/notionapi/request/LoadPageChunkRequest.kt @@ -0,0 +1,20 @@ +package com.petersamokhin.notionapi.request + +import com.petersamokhin.notionapi.model.request.LoadPageChunkRequestBody +import com.petersamokhin.notionapi.model.response.NotionResponse +import com.petersamokhin.notionapi.request.base.NotionRequest +import io.ktor.client.* +import io.ktor.client.request.* +import io.ktor.http.* + +public class LoadPageChunkRequest( + httpClient: HttpClient +) : NotionRequest(httpClient) { + + override suspend fun execute(requestBody: LoadPageChunkRequestBody): NotionResponse = + httpClient.post("$API_BASE_URL/${Endpoint.LOAD_PAGE_CHUNK}") { + headers.appendAll(BASE_HEADERS) + contentType(ContentType.Application.Json) + body = requestBody + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/petersamokhin/notionapi/request/QueryNotionCollectionRequest.kt b/src/main/kotlin/com/petersamokhin/notionapi/request/QueryNotionCollectionRequest.kt index fc5d339..456548b 100644 --- a/src/main/kotlin/com/petersamokhin/notionapi/request/QueryNotionCollectionRequest.kt +++ b/src/main/kotlin/com/petersamokhin/notionapi/request/QueryNotionCollectionRequest.kt @@ -1,6 +1,5 @@ package com.petersamokhin.notionapi.request -import com.petersamokhin.notionapi.model.request.LoadPageChunkRequestBody import com.petersamokhin.notionapi.model.request.QueryCollectionRequestBody import com.petersamokhin.notionapi.model.response.NotionResponse import com.petersamokhin.notionapi.request.base.NotionRequest @@ -8,22 +7,14 @@ import io.ktor.client.* import io.ktor.client.request.* import io.ktor.http.* -class QueryNotionCollectionRequest(httpClient: HttpClient) : NotionRequest(httpClient) { - override suspend fun execute(requestBody: QueryCollectionRequestBody): NotionResponse { - return httpClient.post("$API_BASE_URL/${Endpoint.QUERY_COLLECTION}") { - headers.appendAll(BASE_HEADERS) - contentType(ContentType.Application.Json) - body = requestBody - } - } -} +public class QueryNotionCollectionRequest( + httpClient: HttpClient +) : NotionRequest(httpClient) { -class LoadPageChunkRequest(httpClient: HttpClient) : NotionRequest(httpClient) { - override suspend fun execute(requestBody: LoadPageChunkRequestBody): NotionResponse { - return httpClient.post("$API_BASE_URL/${Endpoint.LOAD_PAGE_CHUNK}") { + override suspend fun execute(requestBody: QueryCollectionRequestBody): NotionResponse = + httpClient.post("$API_BASE_URL/${Endpoint.QUERY_COLLECTION}") { headers.appendAll(BASE_HEADERS) contentType(ContentType.Application.Json) body = requestBody } - } } \ No newline at end of file diff --git a/src/main/kotlin/com/petersamokhin/notionapi/request/base/NotionRequest.kt b/src/main/kotlin/com/petersamokhin/notionapi/request/base/NotionRequest.kt index e8b5a4b..e2ae54f 100644 --- a/src/main/kotlin/com/petersamokhin/notionapi/request/base/NotionRequest.kt +++ b/src/main/kotlin/com/petersamokhin/notionapi/request/base/NotionRequest.kt @@ -1,11 +1,11 @@ package com.petersamokhin.notionapi.request.base import com.petersamokhin.notionapi.model.response.NotionResponse -import io.ktor.client.HttpClient -import io.ktor.http.headersOf +import io.ktor.client.* +import io.ktor.http.* -abstract class NotionRequest(protected val httpClient: HttpClient) { - abstract suspend fun execute(requestBody: T): NotionResponse +public abstract class NotionRequest(protected val httpClient: HttpClient) { + public abstract suspend fun execute(requestBody: T): NotionResponse internal object Endpoint { const val QUERY_COLLECTION = "queryCollection" @@ -13,9 +13,9 @@ abstract class NotionRequest(protected val httpClient: HttpClient) { const val LOGIN_WITH_EMAIL = "loginWithEmail" } - companion object { - const val API_BASE_URL = "https://www.notion.so/api/v3" - val BASE_HEADERS = headersOf( + public companion object { + public const val API_BASE_URL: String = "https://www.notion.so/api/v3" + public val BASE_HEADERS: Headers = headersOf( "Accept-Language" to listOf("en-US,en;q=0.9"), "User-Agent" to listOf("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36") ) diff --git a/src/main/kotlin/com/petersamokhin/notionapi/serializer/NotionBooleanSerializer.kt b/src/main/kotlin/com/petersamokhin/notionapi/serializer/NotionBooleanSerializer.kt index b31cf0b..6ee6b5c 100644 --- a/src/main/kotlin/com/petersamokhin/notionapi/serializer/NotionBooleanSerializer.kt +++ b/src/main/kotlin/com/petersamokhin/notionapi/serializer/NotionBooleanSerializer.kt @@ -7,20 +7,22 @@ import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder -object NotionBooleanSerializer : KSerializer { - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("NotionBoolean", PrimitiveKind.STRING) +public object NotionBooleanSerializer : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("NotionBoolean", PrimitiveKind.STRING) - override fun serialize(encoder: Encoder, value: Boolean) = + override fun serialize(encoder: Encoder, value: Boolean): Unit = encoder.encodeString( - if (value) + if (value) { NOTION_TRUE - else + } else { NOTION_FALSE + } ) override fun deserialize(decoder: Decoder): Boolean = decoder.decodeString() == NOTION_TRUE - const val NOTION_TRUE = "Yes" // ingeniously - const val NOTION_FALSE = "No" + public const val NOTION_TRUE: String = "Yes" // ingeniously + public const val NOTION_FALSE: String = "No" } \ No newline at end of file diff --git a/src/main/kotlin/com/petersamokhin/notionapi/utils/JsonUtils.kt b/src/main/kotlin/com/petersamokhin/notionapi/utils/JsonUtils.kt index f28eb74..ad01c44 100644 --- a/src/main/kotlin/com/petersamokhin/notionapi/utils/JsonUtils.kt +++ b/src/main/kotlin/com/petersamokhin/notionapi/utils/JsonUtils.kt @@ -2,8 +2,14 @@ package com.petersamokhin.notionapi.utils import kotlinx.serialization.json.* -val JsonElement.jsonArrayOrNull: JsonArray? +internal operator fun JsonElement.get(key: String): JsonElement? = + jsonObjectOrNull?.get(key) + +internal val JsonElement.jsonObjectOrNull: JsonObject? + get() = try { jsonObject } catch (e: Exception) { null } + +internal val JsonElement.jsonArrayOrNull: JsonArray? get() = try { jsonArray } catch (e: Exception) { null } -val JsonElement.contentAsStringOrNull: String? +internal val JsonElement.contentAsStringOrNull: String? get() = try { jsonPrimitive.contentOrNull } catch (e: Exception) { null } \ No newline at end of file diff --git a/src/main/kotlin/com/petersamokhin/notionapi/utils/NotionUtils.kt b/src/main/kotlin/com/petersamokhin/notionapi/utils/NotionUtils.kt index ca1ef84..8b84a93 100644 --- a/src/main/kotlin/com/petersamokhin/notionapi/utils/NotionUtils.kt +++ b/src/main/kotlin/com/petersamokhin/notionapi/utils/NotionUtils.kt @@ -1,15 +1,22 @@ package com.petersamokhin.notionapi.utils -import com.petersamokhin.notionapi.model.response.NotionCollection - private const val DASH_ID_LENGTH_VALID = 36 private const val DASH_ID_CLEAN_LENGTH_VALID = 32 -fun String.dashifyId(): String { +/** + * In the page URL, it's ID does not contain any hyphens, + * but in requests it does. + * + * This extension implements the correct logic to convert + * the URL page ID to the one for the API requests. + */ +public fun String.dashifyId(): String { if (isValidDashId()) return this + val clean = replace("-", "") - if (clean.length != DASH_ID_CLEAN_LENGTH_VALID) throw IllegalArgumentException("Incorrect id format: $this") + if (clean.length != DASH_ID_CLEAN_LENGTH_VALID) + throw IllegalArgumentException("Incorrect id format: $this") val chars = clean.toCharArray() val resultChars = CharArray(DASH_ID_LENGTH_VALID) @@ -28,10 +35,17 @@ fun String.dashifyId(): String { return String(resultChars) } -fun String.isValidDashId(): Boolean { +public fun String.isValidDashId(): Boolean { if (length != DASH_ID_LENGTH_VALID) return false return toCharArray().all { c -> c in '0'..'9' || c in 'a'..'f' || c in 'A'..'F' || c == '-' } } -fun List>.trimNotionTextField() = +/** + * Text fields look like this in the responses: + * + * ```json + * [ ["foo", "bar"] ] + * ``` + */ +public fun List>.trimNotionTextField(): String = flatten().joinToString("") \ No newline at end of file