Skip to content

Commit

Permalink
Updated OscReading and unit test
Browse files Browse the repository at this point in the history
  • Loading branch information
morisil committed Sep 4, 2024
1 parent ea1ce80 commit 57ccd16
Show file tree
Hide file tree
Showing 2 changed files with 307 additions and 0 deletions.
186 changes: 186 additions & 0 deletions xemantic-osc-api/src/commonMain/kotlin/type/OscReading.kt
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/

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))
}
)
121 changes: 121 additions & 0 deletions xemantic-osc-api/src/commonTest/kotlin/type/OscReadingTest.kt
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/

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<OscInputException>(
"Cannot read OSC String, because byte sequence is not 0-terminated"
) {
OscReader {}.typeTag()
}
}

@Test
fun shouldNotReadTypeTagWhichDoesNotStartWithComma() {
shouldThrowWithMessage<OscInputException>(
"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<OscInputException>(
"Expected typeTag: 's', but was: 'i'"
) {
OscReader(',', 'i', ZERO, ZERO).assertTypeTag("s")
}
}

@Test
fun shouldCreateOscReaderPopulatedByOscWriter() {
OscReader { string("foo") }.string() shouldBe "foo"
}

}

0 comments on commit 57ccd16

Please sign in to comment.