
com.twitter.collection.RecordSchema.scala Maven / Gradle / Ivy
The newest version!
package com.twitter.collection
import java.util.IdentityHashMap
/**
* RecordSchema represents the declaration of a heterogeneous
* [[com.twitter.collection.RecordSchema.Record Record]] type, with
* [[com.twitter.collection.RecordSchema.Field Fields]] that are determined at runtime. A Field
* declares the static type of its associated values, so although the record itself is dynamic,
* field access is type-safe.
*
* Given a RecordSchema declaration `schema`, any number of
* [[com.twitter.collection.RecordSchema.Record Records]] of that schema can be obtained with
* `schema.newRecord`. The type that Scala assigns to this value is what Scala calls a
* "[[http://lampwww.epfl.ch/~amin/dot/fpdt.pdf path-dependent type]]," meaning that
* `schema1.Record` and `schema2.Record` name distinct types. The same is true of fields:
* `schema1.Field[A]` and `schema2.Field[A]` are distinct, and can only be used with the
* corresponding Record.
*/
final class RecordSchema {
private[RecordSchema] final class Entry(var value: Any) {
var locked: Boolean = false
}
/**
* Record is an instance of a [[com.twitter.collection.RecordSchema RecordSchema]] declaration.
* Records are mutable; the `update` method assigns or reassigns a value to a given field. If
* the user requires that a field's assigned value is never reassigned later, the user can `lock`
* that field.
*
* '''Note that this implementation is not synchronized.''' If multiple threads access a record
* concurrently, and at least one of the threads modifies the record, it ''must'' be synchronized
* externally.
*/
final class Record private[RecordSchema] (
fields: IdentityHashMap[Field[_], Entry] = new IdentityHashMap[Field[_], Entry]) {
private[this] def getOrInitializeEntry(field: Field[_]): Entry = {
var entry = fields.get(field)
if (entry eq null) {
entry = new Entry(field.default())
fields.put(field, entry)
}
entry
}
/**
* Returns the current value assigned to `field` in this record. If there is no value currently
* assigned (either explicitly by a previous `update`, or by the field's declared default), this
* will throw an `IllegalStateException`, indicating the field was never initialized.
*
* Note that Scala provides two syntactic equivalents for invoking this method:
*
* {{{
* record.apply(field)
* record(field)
* }}}
*
* @param field the field to access in this record
* @return the value associated with `field`.
* @throws IllegalStateException
*/
@throws(classOf[IllegalStateException])
def apply[A](field: Field[A]): A =
getOrInitializeEntry(field).value.asInstanceOf[A]
/**
* Locks the current value for a given `field` in this record, preventing further `update`s. If
* there is no value currently assigned (either explicitly by a previous `update`, or by the
* field's declared default), this will throw an `IllegalStateException`, indicating the field
* was never initialized.
*
* @param field the field to lock in this record
* @return this record
* @throws IllegalStateException
*/
@throws(classOf[IllegalStateException])
def lock(field: Field[_]): Record = {
getOrInitializeEntry(field).locked = true
this
}
/**
* Assigns (or reassigns) a `value` to a `field` in this record. If this field was previously
* `lock`ed, this will throw an `IllegalStateException` to indicate failure.
*
* Note that Scala provides two syntactic equivalents for invoking this method:
*
* {{{
* record.update(field, value)
* record(field) = value
* }}}
*
* @param field the field to assign in this record
* @param value the value to assign to `field` in this record
* @return this record
* @throws IllegalStateException
*/
@throws(classOf[IllegalStateException])
def update[A](field: Field[A], value: A): Record = {
val entry = fields.get(field)
if (entry eq null) {
fields.put(field, new Entry(value))
} else if (entry.locked) {
throw new IllegalStateException(s"attempt to assign $value to a locked field (with current value ${entry.value})")
} else {
entry.value = value
}
this
}
/**
* Assigns (or reassigns) a `value` to a `field` in this record, and locks it to prevent further
* `update`s. This method is provided for convenience only; the following are equivalent:
*
* {{{
* record.updateAndLock(field, value)
* record.update(field, value).lock(field)
* }}}
*
* @param field the field to assign and lock in this record
* @param value the value to assign to `field` in this record
* @return this record
* @throws IllegalStateException
*/
@throws(classOf[IllegalStateException])
def updateAndLock[A](field: Field[A], value: A): Record =
update(field, value).lock(field)
private[this] def copyFields(): IdentityHashMap[Field[_], Entry] = {
val newFields = new IdentityHashMap[Field[_], Entry]
val iter = fields.entrySet().iterator()
while (iter.hasNext()) {
val kv = iter.next()
val entry = kv.getValue()
val newEntry = new Entry(entry.value)
newEntry.locked = entry.locked
newFields.put(kv.getKey(), newEntry)
}
newFields
}
/**
* Create a copy of this record. Fields are locked in the copy iff they were locked in the
* original record.
*
* @return a copy of this record
*/
def copy(): Record = {
new Record(copyFields())
}
/**
* Create a copy of this record with `value` assigned to `field`. `field` will be locked in the
* copy iff it was present and locked in the original record. If `field` was not present in the
* original then the following are equivalent:
*
* {{{
* record.copy(field, value)
* record.copy().update(field, value)
* }}}
*
* @param field the field to assign in the copy
* @param value the value to assign to `field` in the copy
* @return a copy of this record
*/
def copy[A](field: Field[A], value: A): Record = {
val newFields = copyFields()
val entry = newFields.get(field)
if (entry eq null) {
newFields.put(field, new Entry(value))
} else {
entry.value = value
}
new Record(newFields)
}
}
/**
* Creates a new [[com.twitter.collection.RecordSchema.Record Record]] from this Schema.
*
* @return a new [[com.twitter.collection.RecordSchema.Record Record]]
*/
def newRecord(): Record = new Record
/**
* Field is a handle used to access some corresponding value in a
* [[com.twitter.collection.RecordSchema.Record Record]]. A field may also declare a default,
* which is computed for each record it is associated with, at most once per record instance.
*/
sealed trait Field[A] {
def default(): A
}
/**
* Creates a new [[com.twitter.collection.RecordSchema.Field Field]] with no default value, to be
* used only with [[com.twitter.collection.RecordSchema.Record Records]] from this schema.
*
* @return a [[com.twitter.collection.RecordSchema.Field Field]] with no default value
*/
def newField[A](): Field[A] = new Field[A] {
override def default(): A = throw new IllegalStateException("attempt to access uninitialized field")
}
/**
* Creates a new [[com.twitter.collection.RecordSchema.Field Field]] with the given
* `defaultSupplier`, to be used only with [[com.twitter.collection.RecordSchema.Record Records]]
* from this schema.
*
* @param defaultSupplier a computation producing the default value to use, when there is no value
* previously assigned to this field in a given record.
* @return a [[com.twitter.collection.RecordSchema.Field Field]] with the given `defaultSupplier`
*/
def newField[A](defaultSupplier: => A): Field[A] = new Field[A] {
override def default(): A = defaultSupplier
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy