* 'main' of https://github.com/vitorpamplona/amethyst:
  fix lint warnings
  add custom serializer that accepts both single integers and arrays add tests
  New Crowdin translations by GitHub Action
  updated cz, de, pt, sv
This commit is contained in:
Vitor Pamplona
2025-12-02 15:16:39 -05:00
7 changed files with 447 additions and 3 deletions
@@ -0,0 +1,90 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip11RelayInfo
import com.vitorpamplona.quartz.utils.Log
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonPrimitive
object FlexibleIntListSerializer : KSerializer<List<Int>?> {
private val listSerializer = ListSerializer(Int.serializer())
override val descriptor: SerialDescriptor = listSerializer.descriptor
override fun deserialize(decoder: Decoder): List<Int>? {
require(decoder is JsonDecoder) { "This serializer can only be used with Json format" }
return when (val element = decoder.decodeJsonElement()) {
// Handle JSON null
is JsonNull -> null
// Handle array format (spec-compliant): [1, 2, 3]
is JsonArray -> {
element.mapNotNull { arrayElement ->
try {
arrayElement.jsonPrimitive.int
} catch (e: Exception) {
// Skip elements that aren't valid integers (strings, booleans, floats, etc.)
Log.w("FlexibleIntListSerializer", "Invalid element in array: $arrayElement", e)
null
}
}
}
// Handle single integer format (malformed but found in the wild): 1
is JsonPrimitive if !element.isString -> {
try {
listOf(element.int)
} catch (e: Exception) {
// Can't parse as integer (e.g., float, boolean), treat as missing data
Log.w("FlexibleIntListSerializer", "Invalid primitive: $element", e)
null
}
}
// Unsupported format (strings, objects, etc.)
else -> null
}
}
@OptIn(ExperimentalSerializationApi::class)
override fun serialize(
encoder: Encoder,
value: List<Int>?,
) {
if (value == null) {
encoder.encodeNull()
} else {
listSerializer.serialize(encoder, value)
}
}
}
@@ -32,6 +32,7 @@ class Nip11RelayInformation(
val icon: String? = null,
val pubkey: String? = null,
val contact: String? = null,
@Serializable(with = FlexibleIntListSerializer::class)
val supported_nips: List<Int>? = null,
val supported_nip_extensions: List<String>? = null,
val software: String? = null,
@@ -0,0 +1,209 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip11RelayInfo
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
class Nip11RelayInformationTest {
// Test case for properly formatted relay info with array of supported NIPs
val standardFormatJson =
"""
{
"contact":"_@f7z.io",
"description":"Nostr's Purple Pages",
"limitation":{
"max_limit":5000000,
"max_message_length":131072,
"max_subscriptions":50000
},
"name":"purplepag.es",
"negentropy":1,
"pubkey":"fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52",
"software":"git+https://github.com/hoytech/strfry.git",
"supported_nips":[1,2,9,11],
"version":"1.0.4"
}
""".trimIndent()
// Test case for malformed relay info where supported_nips is a single integer instead of an array
val malformedSingleIntegerJson =
"""
{
"contact":"_@f7z.io",
"description":"Nostr's Purple Pages",
"limitation":{
"max_limit":5000000,
"max_message_length":131072,
"max_subscriptions":50000
},
"name":"purplepag.es",
"negentropy":1,
"pubkey":"fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52",
"software":"git+https://github.com/hoytech/strfry.git",
"supported_nips":1,
"version":"1.0.4"
}
""".trimIndent()
@Test
fun `Parse standard format`() {
val info = Nip11RelayInformation.fromJson(standardFormatJson)
assertNotNull(info)
assertEquals("purplepag.es", info.name)
assertEquals("Nostr's Purple Pages", info.description)
assertEquals("fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52", info.pubkey)
assertEquals(listOf(1, 2, 9, 11), info.supported_nips)
assertEquals("1.0.4", info.version)
assertEquals("git+https://github.com/hoytech/strfry.git", info.software)
}
@Test
fun `Parse malformed single integer`() {
val info = Nip11RelayInformation.fromJson(malformedSingleIntegerJson)
assertNotNull(info)
assertEquals("purplepag.es", info.name)
assertEquals("Nostr's Purple Pages", info.description)
assertEquals("fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52", info.pubkey)
// Single integer should be converted to a list with one element
assertEquals(listOf(1), info.supported_nips)
assertEquals("1.0.4", info.version)
assertEquals("git+https://github.com/hoytech/strfry.git", info.software)
}
@Test
fun `Parse null value`() {
val json = """{"name":"test relay","supported_nips":null}"""
val info = Nip11RelayInformation.fromJson(json)
assertNotNull(info)
assertEquals("test relay", info.name)
assertNull(info.supported_nips)
}
@Test
fun `Parse missing field`() {
val json = """{"name":"test relay","version":"1.0"}"""
val info = Nip11RelayInformation.fromJson(json)
assertNotNull(info)
assertEquals("test relay", info.name)
assertNull(info.supported_nips) // Field is optional with default null
}
@Test
fun `Parse empty array`() {
val json = """{"name":"test relay","supported_nips":[]}"""
val info = Nip11RelayInformation.fromJson(json)
assertNotNull(info)
assertEquals("test relay", info.name)
assertEquals(emptyList(), info.supported_nips)
}
@Test
fun `Parse malformed array With invalid elements`() {
// Array contains valid integers mixed with invalid elements (strings, floats, booleans)
val json = """{"name":"test relay","supported_nips":[1,"invalid",3,4.5,true,11]}"""
val info = Nip11RelayInformation.fromJson(json)
assertNotNull(info)
assertEquals("test relay", info.name)
// Should skip invalid elements and keep only valid integers
assertEquals(listOf(1, 3, 11), info.supported_nips)
}
@Test
fun `Parse malformed array with all invalid elements`() {
// Array contains only invalid elements
val json = """{"name":"test relay","supported_nips":["one","two","three"]}"""
val info = Nip11RelayInformation.fromJson(json)
assertNotNull(info)
assertEquals("test relay", info.name)
// All elements invalid, should result in empty list
assertEquals(emptyList(), info.supported_nips)
}
@Test
fun `Parse invalid primitive float`() {
// Float instead of int
val json = """{"name":"test relay","supported_nips":1.5}"""
val info = Nip11RelayInformation.fromJson(json)
assertNotNull(info)
assertEquals("test relay", info.name)
// Cannot parse float as integer, should be null
assertNull(info.supported_nips)
}
@Test
fun `Parse invalid primitive boolean`() {
// Boolean instead of int/array
val json = """{"name":"test relay","supported_nips":true}"""
val info = Nip11RelayInformation.fromJson(json)
assertNotNull(info)
assertEquals("test relay", info.name)
// Cannot parse boolean as integer, should be null
assertNull(info.supported_nips)
}
@Test
fun `Parse invalid primitive string`() {
// String instead of int/array
val json = """{"name":"test relay","supported_nips":"1"}"""
val info = Nip11RelayInformation.fromJson(json)
assertNotNull(info)
assertEquals("test relay", info.name)
// Strings are explicitly excluded (element.isString check), should be null
assertNull(info.supported_nips)
}
@Test
fun `Parse invalid object`() {
// Object instead of int/array
val json = """{"name":"test relay","supported_nips":{"nip":1}}"""
val info = Nip11RelayInformation.fromJson(json)
assertNotNull(info)
assertEquals("test relay", info.name)
// Objects are unsupported format, should be null
assertNull(info.supported_nips)
}
@Test
fun `Parse large array`() {
// Test with a large array to ensure no performance issues
val nips = (1..100).joinToString(",")
val json = """{"name":"test relay","supported_nips":[$nips]}"""
val info = Nip11RelayInformation.fromJson(json)
assertNotNull(info)
assertEquals("test relay", info.name)
assertEquals((1..100).toList(), info.supported_nips)
}
}