Skip to content

Commit b57d19b

Browse files
ex0nsShastick
authored andcommitted
Move Entities from trip to timeseries
GitOrigin-RevId: e2f2faabe8c8ced5f4d52c73ae708925429b95f9
1 parent 0e42b07 commit b57d19b

File tree

10 files changed

+410
-0
lines changed

10 files changed

+410
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package io.sqooba.oss.timeseries.entity
2+
3+
import io.sqooba.oss.timeseries.labels.{LabelUnitMapper, TsLabel}
4+
5+
import scala.annotation.implicitNotFound
6+
import scala.util.{Failure, Success, Try}
7+
8+
/** Encapsulates the parsing logic for [[TsLabel]], [[TsId]] and
9+
* [[TimeSeriesEntityId]]. It lets the user provide their own mapping from string
10+
* keys to entities and to units.
11+
*/
12+
@implicitNotFound("Cannot parse entities and/or units for TsId/TsLabel because no parser was defined.")
13+
trait EntityParser extends LabelUnitMapper {
14+
15+
/** Parse the unit of a TsLabel from the given string key.
16+
*
17+
* @return an option containing the string representation of the unit
18+
*/
19+
def deriveUnit(in: TsLabel): Option[String]
20+
21+
/** Parse the key prefix of a TsId from the given string key.
22+
*
23+
* @return a TsKeyPrefix or a Failure
24+
*/
25+
def parseKeyPrefix(string: String): Try[TsKeyPrefix]
26+
}
27+
28+
object EntityParser {
29+
30+
/** Default parser that returns [[GenericKeyPrefix]]es and no units. */
31+
val default: EntityParser = new EntityParser {
32+
33+
override def deriveUnit(in: TsLabel): Option[String] = None
34+
35+
override def parseKeyPrefix(string: String): Try[TsKeyPrefix] = Success(GenericKeyPrefix(string))
36+
37+
}
38+
39+
val hexEntityParser: EntityParser = default
40+
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package io.sqooba.oss.timeseries.entity
2+
3+
import io.sqooba.oss.timeseries.labels.TsLabel
4+
5+
import scala.util.Try
6+
7+
trait TimeSeriesEntityId {
8+
9+
/**
10+
* @return the key prefix for the implementing identifier.
11+
*/
12+
def keyPrefix: TsKeyPrefix
13+
14+
/**
15+
* @return the internal identification number of whatever the implementor identifies.
16+
*/
17+
def id: Long
18+
19+
/**
20+
* @param signal the name of the sensor we want to build the TSDB identifier for
21+
* @return the full key identifying a time series in the TSDB.
22+
*/
23+
// TODO: the use of this function probably causes AnyVal implementations to require an instantiation.
24+
// At the moment we mostly care about correctness, not performance, but we need to keep it in mind.
25+
// If at some point we want to do things allocation-free, have a look at
26+
// https://docs.scala-lang.org/overviews/core/value-classes.html
27+
// under the extension methods paragraph
28+
def buildTimeseriesID(signal: TsLabel): TsId
29+
30+
/**
31+
* Gets the entity Id, formatted as PREFIX_ID
32+
*/
33+
def formatted: String
34+
35+
}
36+
37+
abstract class GenericEntityId(override val id: Long, override val keyPrefix: TsKeyPrefix) extends TimeSeriesEntityId {
38+
override def buildTimeseriesID(signal: TsLabel): TsId = GenericTsId(this, signal)
39+
override val formatted: String = s"${keyPrefix.prefix}${GenericTsId.prefixSeparator}$id"
40+
}
41+
42+
case class TripEntityId(id: Long, keyPrefix: TsKeyPrefix) extends TimeSeriesEntityId {
43+
override def buildTimeseriesID(signal: TsLabel): TsId = TripTsId(this, signal)
44+
override val formatted: String = s"${keyPrefix.prefix}${TripTsId.prefixSeparator}$id"
45+
}
46+
47+
object TimeSeriesEntityId {
48+
49+
/** Builds a [[TimeSeriesEntityId]] from a given prefix and id. */
50+
def from(prefix: String, id: Long)(implicit entityParser: EntityParser): Try[TimeSeriesEntityId] =
51+
entityParser.parseKeyPrefix(prefix).map(_.toEntityId(id))
52+
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package io.sqooba.oss.timeseries.entity
2+
3+
import io.sqooba.oss.timeseries.labels.TsLabel
4+
5+
import scala.util.Try
6+
7+
trait TsId {
8+
val entityId: TimeSeriesEntityId
9+
val label: TsLabel
10+
val id: String
11+
}
12+
13+
/** Identifies a single materialized time series.
14+
*
15+
* @param entityId An entity which has time series data
16+
* @param label The label of the time series (e.g sensor data)
17+
*/
18+
case class TripTsId(entityId: TimeSeriesEntityId, label: TsLabel) extends TsId {
19+
import TripTsId._
20+
21+
/** Formats the [[TsId]] into a string of the format: LABEL-PREFIX_ID */
22+
override val id: String = s"${label.value}$keySeparator${entityId.formatted}"
23+
}
24+
25+
object TripTsId {
26+
27+
val keySeparator = "-"
28+
val prefixSeparator = "_"
29+
30+
/** Tries to construct a [[TsId]] from a raw string representation. By using the
31+
* given entityParser it will try to parse the entity type and unit of the label.
32+
*
33+
* The label is a string (anything is accepted).
34+
* The idRaw is in the format PREFIX_ID.
35+
* - PREFIX must be a known prefix, by the given entityParser
36+
* - ID must be a number
37+
*/
38+
def from(label: String, idRaw: String)(implicit entityParser: EntityParser): Try[TsId] = {
39+
val idTokens = idRaw.split(prefixSeparator)
40+
41+
for {
42+
idLong <- Try(idTokens(1).toLong)
43+
id <- TimeSeriesEntityId.from(idTokens(0), idLong)
44+
} yield TripTsId(id, TsLabel(label))
45+
}
46+
47+
}
48+
49+
/** Identifies a single materialized time series.
50+
*
51+
* @param entityId An entity which has time series data
52+
* @param label The label of the time series (e.g sensor data)
53+
*/
54+
case class GenericTsId(entityId: TimeSeriesEntityId, label: TsLabel) extends TsId {
55+
import GenericTsId._
56+
57+
/** Formats the [[TsId]] into a string of the format: LABEL-PREFIX_ID */
58+
override val id: String = s"${label.value}$keySeparator${entityId.formatted}"
59+
}
60+
61+
object GenericTsId {
62+
63+
val keySeparator = "-"
64+
val prefixSeparator = "_"
65+
66+
/** Tries to construct a [[TsId]] from a raw string representation. By using the
67+
* given entityParser it will try to parse the entity type and unit of the label.
68+
*
69+
* The label is a string (anything is accepted).
70+
* The idRaw is in the format PREFIX_ID.
71+
* - PREFIX must be a known prefix, by the given entityParser
72+
* - ID must be a number
73+
*/
74+
def from(label: String, idRaw: String)(implicit entityParser: EntityParser): Try[TsId] = {
75+
val idTokens = idRaw.split(prefixSeparator)
76+
77+
for {
78+
idLong <- Try(idTokens(1).toLong)
79+
id <- TimeSeriesEntityId.from(idTokens(0), idLong)
80+
} yield GenericTsId(id, TsLabel(label))
81+
}
82+
}
83+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package io.sqooba.oss.timeseries.entity
2+
3+
/**
4+
* Represents a key prefix to distinguish between different types of entities for which
5+
* data is stored
6+
*/
7+
trait TsKeyPrefix {
8+
9+
val prefix: String
10+
11+
override def toString: String = prefix
12+
13+
def toEntityId(id: Long): TimeSeriesEntityId
14+
}
15+
16+
case class GenericKeyPrefix(prefix: String) extends TsKeyPrefix {
17+
18+
def toEntityId(id: Long): TimeSeriesEntityId = new GenericEntityId(id, this) {}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package io.sqooba.oss.timeseries.labels
2+
3+
import scala.annotation.implicitNotFound
4+
5+
/**
6+
* Provide a mapping from label to units:
7+
* Implementations may provide any logic (either through a static mapping or deriving it from the label itself)
8+
* to return an optional unit for a given label.
9+
*/
10+
@implicitNotFound("Cannot derive units for TsLabel because no LabelUnitMapper was defined.")
11+
trait LabelUnitMapper {
12+
13+
/**
14+
* @param in the label for which the unit should be determined
15+
* @return the unit corresponding to this label, if any can be derived.
16+
*/
17+
def deriveUnit(in: TsLabel): Option[String]
18+
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package io.sqooba.oss.timeseries.labels
2+
3+
/** A time series label.
4+
*
5+
* Units can be inferred by specifying the (implicit) LabelUnitMapper
6+
*/
7+
case class TsLabel(value: String) {
8+
9+
def unit(implicit unitMapper: LabelUnitMapper): Option[String] = unitMapper.deriveUnit(this)
10+
11+
// A label should never contain a key separator, since it would break the formatting
12+
// require(!value.contains(TsId.keySeparator))
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package io.sqooba.oss.timeseries.entity
2+
3+
import io.sqooba.oss.timeseries.labels.TsLabel
4+
import org.scalatest.FlatSpec
5+
import org.scalatest.matchers.should.Matchers
6+
7+
import scala.util.{Failure, Success}
8+
9+
class GenericTsIdSpec extends FlatSpec with Matchers {
10+
11+
implicit val parser: EntityParser = fruitParser
12+
13+
"TsId.from" should "be parsable from a string" in {
14+
GenericTsId.from("test", "ba_1") should be(Success(GenericTsId(BananaId(1L), TsLabel("test"))))
15+
16+
GenericTsId.from("test_2", "ch_2") should be(Success(GenericTsId(CherryId(2L), TsLabel("test_2"))))
17+
}
18+
19+
it should "not be parsable if prefix is unknown" in {
20+
GenericTsId.from("test_2", "does_not_exist_2") shouldBe a[Failure[_]]
21+
}
22+
23+
it should "not be parsable if id is malformed" in {
24+
GenericTsId.from("test_2", "t_malformed_id") shouldBe a[Failure[_]]
25+
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package io.sqooba.oss.timeseries.entity
2+
3+
import io.sqooba.oss.timeseries.labels.TsLabel
4+
import org.scalatest.FlatSpec
5+
import org.scalatest.matchers.should.Matchers
6+
7+
import scala.util.Failure
8+
9+
class TimeSeriesEntityIdSpec extends FlatSpec with Matchers {
10+
11+
implicit val parser: EntityParser = fruitParser
12+
13+
"TimeSeriesEntity" should "return the correct TSDB identification string" in {
14+
BananaId(666).buildTimeseriesID(TsLabel("bob")).id should be("bob-ba_666")
15+
}
16+
17+
"TimeSeriesEntity.from" should "be able to parse a fruit id" in {
18+
TimeSeriesEntityId.from("ch", 111).get shouldBe CherryId(111L)
19+
}
20+
21+
it should "be built from a prefix and an id" in {
22+
TimeSeriesEntityId.from("ap", 2).get shouldBe AppleId(2L)
23+
}
24+
25+
it should "not be created from unknown prefix" in {
26+
('A' to 'z')
27+
.filter(character => character != 'w' && character != 't')
28+
.foreach { character =>
29+
TimeSeriesEntityId.from(character.toString, 2) shouldBe a[Failure[_]]
30+
}
31+
}
32+
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package io.sqooba.oss.timeseries.entity
2+
3+
import io.sqooba.oss.timeseries.labels.TsLabel
4+
import org.scalatest.flatspec.AnyFlatSpec
5+
import org.scalatest.matchers.should.Matchers
6+
7+
import scala.util.{Failure, Try}
8+
9+
class TsLabelSpec extends AnyFlatSpec with Matchers {
10+
11+
// scalastyle:off non.ascii.character.disallowed
12+
13+
"TsLabel.fromString" should "be derived from label name" in {
14+
15+
implicit val physicsParser: EntityParser = new EntityParser {
16+
private val AMPERE_PATTERN = "(.*_A_.*)".r
17+
private val HERTZ_PATTERN = "(.*_Hz_.*)".r
18+
private val PHS_VOLT_PATTERN = "(.*_PhsV_.*)".r
19+
private val TEMP_PATTERN = "(.*Temp_.*)".r
20+
21+
def parseKeyPrefix(string: String): Try[TsKeyPrefix] = Failure(new Exception("This is not used"))
22+
23+
override def deriveUnit(in: TsLabel): Option[String] =
24+
in.value match {
25+
case AMPERE_PATTERN(_) => Some("A")
26+
case HERTZ_PATTERN(_) => Some("Hz")
27+
case PHS_VOLT_PATTERN(_) => Some("V")
28+
case TEMP_PATTERN(_) => Some("°C")
29+
case _ => None
30+
}
31+
}
32+
33+
Seq(
34+
("MMCX_A_10m_Avg", Some("A")),
35+
("MMCX_Hz_10m_Avg", Some("Hz")),
36+
("MMCX_PhsV_PhsA_10m_Avg", Some("V")),
37+
("WCNV_XXTemp_10m_Avg", Some("°C")),
38+
("MMCX_PF_10m_Avg", None),
39+
("this_is_an_unkown_unit", None)
40+
).foreach {
41+
case (label, unit) => TsLabel(label).unit shouldBe unit
42+
}
43+
}
44+
// scalastyle:on non.ascii.character.disallowed
45+
46+
it should "return a failure if a case is not handled" in {
47+
48+
implicit val parser: EntityParser = new EntityParser {
49+
50+
def parseKeyPrefix(string: String): Try[TsKeyPrefix] = Failure(new Exception("This is not used"))
51+
52+
/** Parse the unit of a TsLabel from the given string key.
53+
*
54+
* @return an option containing the string representation of the unit
55+
*/
56+
override def deriveUnit(in: TsLabel): Option[String] =
57+
in.value match {
58+
case "not exhaustive" => Some("unit")
59+
case _ => None
60+
}
61+
}
62+
63+
TsLabel("anything").unit shouldBe None
64+
TsLabel("not exhaustive").unit shouldBe Some("unit")
65+
}
66+
}

0 commit comments

Comments
 (0)