diff --git a/xemantic-osc-api/src/commonMain/kotlin/type/OscReading.kt b/xemantic-osc-api/src/commonMain/kotlin/type/OscReading.kt new file mode 100644 index 0000000..aa5ebaa --- /dev/null +++ b/xemantic-osc-api/src/commonMain/kotlin/type/OscReading.kt @@ -0,0 +1,186 @@ +/* + * xemantic-osc - Kotlin idiomatic and multiplatform OSC protocol support + * Copyright (C) 2024 Kazimierz Pogoda + * + * This file is part of xemantic-osc. + * + * xemantic-osc is free software: you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * xemantic-osc is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with xemantic-osc. + * If not, see . + */ + +package com.xemantic.osc.type + +import com.xemantic.osc.OscInputException +import com.xemantic.osc.io.* +import kotlinx.io.* + +/** + * The reader of an OSC data stream. + * + * It adapts the [Source] to only allow data types available + * in the OSC protocol and make sequential reads of values + * concise. + * + * @param source the source to read from. + */ +public class OscReader( + @PublishedApi + internal val source: Source +) { + + /** + * Reads an OSC signed integer (Type Tag `i`). + * + * @return the int value. + */ + public inline fun int(): Int = source.readInt() + + /** + * Reads an OSC float (Type Tag `f`). + * + * @return the float value. + */ + public inline fun float(): Float = source.readFloat() + + /** + * Reads an OSC String (Type Tag `s`). + * + * @return the string value. + */ + public inline fun string(): String = source.readOscString() + + /** + * Reads an OSC blob as bytes (Type Tag `b`). + * + * @return the blob. + */ + public inline fun blob(): ByteArray = source.readOscBlob() + + /** + * Reads an OSC long (Type Tag `h`- 64 bit big-endian two’s complement integer). + * + * @return the Long value. + */ + public inline fun long(): Long = source.readLong() + + /** + * Reads an OSC Time Tag (Type Tag `t`). + * + * @return the Osc Time Tag value. + */ + public inline fun timeTag(): OscTimeTag = source.readOscTimeTag() + + /** + * Reads an OSC double (Type Tag `d`). + * + * @return the Double value. + */ + public inline fun double(): Double = source.readDouble() + + /** + * Reads an OSC char (Type Tag `c`). + * + * @return the Char value. + */ + public inline fun char(): Char = source.readOscChar() + + /** + * Reads an OSC color (Type Tag `r`). + * + * @return the color value. + */ + public inline fun color(): OscColor = source.readOscColor() + + /** + * Reads an OSC MIDI message (Type Tag `m`). + * + * @return the MIDI message value. + */ + public inline fun midiMessage(): OscMidiMessage = source.readOscMidiMessage() + +} + +/** + * Reads OSC Type Tag. + * _Note: the initial comma described in the protocol is already stripped._ + * + * See [Osc Type Tag String specification](https://ccrma.stanford.edu/groups/osc/spec-1_0.html#osc-type-tag-string) + * + * @return the OSC Type Tag String without leading comma character. + * @throws OscInputException if type tag doesn't exist or is malformed. + */ +public fun OscReader.typeTag(): String { + val head = string() + if (head.isEmpty()) { + throw OscInputException( + "OSC type tag is empty" + ) + } + if (head[0] != ',') { + throw OscInputException( + "OSC type tag must start with ," + ) + } + return head.substring(1) +} + +/** + * Reads the [typeTag] and asserts its value. + * + * @param typeTag the OSC Type Tag to match (without leading comma). + * @throws OscInputException if type tag doesn't exist, is + * malformed or does not match the expected `typeTag`. + * @see OscReader.typeTag + */ +public fun OscReader.assertTypeTag(typeTag: String) { + val tag = typeTag() + if (tag != typeTag) { + throw OscInputException( + "Expected typeTag: '$typeTag', but was: '$tag'" + ) + } +} + +/** + * Creates OSC reader populated with data from specified characters converted to bytes. + * Useful for testing. + * + * @param chars the characters to source the reading from. + */ +public fun OscReader(vararg chars: Char): OscReader = OscReader( + source = Buffer().apply { + write(chars.map { it.code.toByte() }.toByteArray()) + } +) + +/** + * Creates OSC reader populated with data from specified bytes. + * Useful for testing. + * + * @param bytes the bytes to source the reading from. + */ +public fun OscReader(vararg bytes: Byte): OscReader = OscReader( + source = Buffer().apply { write(bytes) } +) + +/** + * Creates OSC reader populated with data from supplied OSC writer. + * Useful for testing. + * + * @param block the sequence of writes to source the reading from. + */ +public fun OscReader( + block: OscWriter.() -> Unit +): OscReader = OscReader( + Buffer().apply { + block(OscWriter(this)) + } +) diff --git a/xemantic-osc-api/src/commonTest/kotlin/type/OscReadingTest.kt b/xemantic-osc-api/src/commonTest/kotlin/type/OscReadingTest.kt new file mode 100644 index 0000000..eca4fff --- /dev/null +++ b/xemantic-osc-api/src/commonTest/kotlin/type/OscReadingTest.kt @@ -0,0 +1,121 @@ +/* + * xemantic-osc - Kotlin idiomatic and multiplatform OSC protocol support + * Copyright (C) 2024 Kazimierz Pogoda + * + * This file is part of xemantic-osc. + * + * xemantic-osc is free software: you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * xemantic-osc is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with xemantic-osc. + * If not, see . + */ + +package com.xemantic.osc.type + +import com.xemantic.osc.OscInputException +import com.xemantic.osc.ZERO +import io.kotest.assertions.throwables.shouldThrowWithMessage +import io.kotest.matchers.shouldBe +import kotlin.test.Test + +class OscReadingTest { + + @Test + fun shouldReadString() { + OscReader('O', 'S', 'C', ZERO).string() shouldBe "OSC" + } + + @Test + fun shouldReadInt() { + OscReader(0, 0, 0, 42).int() shouldBe 42 + } + + @Test + fun shouldReadFloat() { + OscReader(66, 40, 0, 0).float() shouldBe 42.0f + } + + @Test + fun shouldReadDouble() { + OscReader(64, 69, 0, 0, 0, 0, 0, 0).double() shouldBe 42.0 + } + + @Test + fun shouldReadLong() { + OscReader(0, 0, 0, 0, 0, 0, 0, 42).long() shouldBe 42.toLong() + } + + @Test + fun shouldReadChar() { + OscReader(ZERO, ZERO, ZERO, 'a').char() shouldBe 'a' + } + + @Test + fun shouldReadBlob() { + OscReader(0, 0, 0, 1, 42, 0, 0, 0).blob() shouldBe byteArrayOf(42) + } + + @Test + fun shouldReadTimeTag() { + OscReader(0, 0, 0, 0, 0, 0, 0, 1).timeTag() shouldBe OscTimeTag.IMMEDIATE + } + + @Test + fun shouldReadMidiMessage() { + OscReader(1, 2, 3, 4).midiMessage() shouldBe OscMidiMessage(1u, 2u, 3u, 4u) + } + + @Test + fun shouldReadColor() { + OscReader(1, 2, 3, 4).color() shouldBe OscColor(1u, 2u, 3u, 4u) + } + + @Test + fun shouldReadTypeTag() { + OscReader(',', 'i', ZERO, ZERO).typeTag() shouldBe "i" + } + + @Test + fun shouldNotReadEmptyTypeTag() { + shouldThrowWithMessage( + "Cannot read OSC String, because byte sequence is not 0-terminated" + ) { + OscReader {}.typeTag() + } + } + + @Test + fun shouldNotReadTypeTagWhichDoesNotStartWithComma() { + shouldThrowWithMessage( + "OSC type tag must start with ," + ) { + OscReader('i', ZERO, ZERO, ZERO).typeTag() + } + } + + @Test + fun shouldAssertTypeTag() { + OscReader(',', 'i', ZERO, ZERO).assertTypeTag("i") + } + + @Test + fun shouldNotAssertOtherTypeTag() { + shouldThrowWithMessage( + "Expected typeTag: 's', but was: 'i'" + ) { + OscReader(',', 'i', ZERO, ZERO).assertTypeTag("s") + } + } + + @Test + fun shouldCreateOscReaderPopulatedByOscWriter() { + OscReader { string("foo") }.string() shouldBe "foo" + } + +}