From 2a3cf45229d8c978f492fc4db60a1399699a505b Mon Sep 17 00:00:00 2001 From: Oskari Timperi Date: Wed, 1 Nov 2017 18:02:51 +0200 Subject: Add initial support for record sets and simple constraints The constraints added in this commit are mandatory, prohibit and allowed. --- nimrec/recordset.nim | 90 ++++++++++++++++++++++++ tests/test.nim | 188 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 278 insertions(+) create mode 100644 nimrec/recordset.nim diff --git a/nimrec/recordset.nim b/nimrec/recordset.nim new file mode 100644 index 0000000..9e6d90c --- /dev/null +++ b/nimrec/recordset.nim @@ -0,0 +1,90 @@ +import parser +import record +import utils + +import sequtils +import sets +import streams +import strutils +import tables +import times + +type + RecordSet* = ref object + kind*: string + doc*: string + mandatory*: HashSet[string] + allowed*: HashSet[string] + prohibited*: HashSet[string] + + IntegrityError* = object of Exception + +proc newRecordSet*(record: Record): RecordSet = + new(result) + + init(result.mandatory) + init(result.prohibited) + init(result.allowed) + + result.kind = record["%rec"] + + if "%doc" in record: + result.doc = record["%doc"] + + for value in values(record, "%mandatory"): + for field in split(value): + incl(result.mandatory, field) + incl(result.allowed, field) + if field in result.prohibited: + raise newException(Exception, "a field cannot be mandatory and prohibited at the same time") + + for value in values(record, "%prohibit"): + for field in split(value): + if field in result.mandatory: + raise newException(Exception, "a field cannot be mandatory and prohibited at the same time") + if field in result.allowed: + raise newException(Exception, "a field cannot be allowed and prohibited at the same time") + incl(result.prohibited, field) + + for value in values(record, "%allowed"): + for field in split(value): + if field in result.prohibited: + raise newException(Exception, "a field cannot be allowed and prohibited at the same time") + incl(result.allowed, field) + +proc validate*(record: Record, recordSet: RecordSet) = + let labels = toSet(toSeq(labels(record))) + + for label in labels(record): + if len(recordSet.allowed) > 0: + if label notin recordSet.allowed: + raise newException(IntegrityError, format("not allowed: $1", label)) + + if label in recordSet.prohibited: + raise newException(IntegrityError, format("prohibited: $1", label)) + + for mandatoryLabel in recordSet.mandatory: + if mandatoryLabel notin labels: + raise newException(IntegrityError, format("mandatory: $1", + mandatoryLabel)) + +iterator recordsInSet*(stream: Stream, kind: string): Record = + var + currentSet: RecordSet = nil + + for record in records(stream): + if currentSet == nil: + if "%rec" in record and record["%rec"] == kind: + currentSet = newRecordSet(record) + continue + else: + if "%rec" in record and record["%rec"] == kind: + currentSet = newRecordSet(record) + continue + elif "%rec" in record: + currentSet = nil + continue + + validate(record, currentSet) + + yield record diff --git a/tests/test.nim b/tests/test.nim index 70a9665..9721fb0 100644 --- a/tests/test.nim +++ b/tests/test.nim @@ -222,3 +222,191 @@ Email: jane@doe.me check(len(records) == 2) check("Email" notin records[0]) check("Email" in records[1]) + +suite "recordset": + test "basics": + const data = """ +%rec: Entry +%doc: docs for Entry +""" + + var ss = newStringStream(data) + var records = toSeq(records(ss)) + var rset = newRecordSet(records[0]) + + check(rset.kind == "Entry") + check(rset.doc == "docs for Entry") + + test "descriptor skipped when iterating records": + const data = """ +%rec: Entry +%doc: docs for Entry + +Name: John + +Name: Jane + +Name: Bill +""" + + var ss = newStringStream(data) + var records = toSeq(recordsInSet(ss, "Entry")) + check(len(records) == 3) + check(records[0]["Name"] == "John") + check(records[1]["Name"] == "Jane") + check(records[2]["Name"] == "Bill") + + test "mandatory does not raise if all present": + const data = """ +%rec: Entry +%mandatory: Name Age + +Name: John +Age: 0 + +Name: Jane +Age: 0 + +Name: Bill +Age: 0 +""" + + var ss = newStringStream(data) + var records = toSeq(recordsInSet(ss, "Entry")) + check(len(records) == 3) + + test "mandatory raises if something is missing": + const data = """ +%rec: Entry +%mandatory: Name Age + +Name: John +Age: 0 + +Name: Jane + +Name: Bill +Age: 0 +""" + + var ss = newStringStream(data) + expect(IntegrityError): + discard toSeq(recordsInSet(ss, "Entry")) + + test "prohibit does not fail if fields not present": + const data = """ +%rec: Entry +%prohibit: result + +Name: John + +Name: Jane + +Name: Bill +""" + + var ss = newStringStream(data) + var records = toSeq(recordsInSet(ss, "Entry")) + check(len(records) == 3) + + test "prohibit fails if fields present": + const data = """ +%rec: Entry +%prohibit: result + +Name: John + +Name: Jane + +Name: Bill +result: 100 +""" + + var ss = newStringStream(data) + expect(IntegrityError): + discard toSeq(recordsInSet(ss, "Entry")) + + test "allowed allows allowed fields": + const data = """ +%rec: Entry +%allowed: Name Age Address + +Name: John + +Name: Jane +Age: 29 + +Name: Bill +Age: 56 +Address: 10 Foobar Way +""" + + var ss = newStringStream(data) + var records = toSeq(recordsInSet(ss, "Entry")) + check(len(records) == 3) + + test "allowed does not allow undeclared fields": + const data = """ +%rec: Entry +%allowed: Name Age Address + +Name: John + +Name: Jane +Age: 29 +Phone: 12345 + +Name: Bill +Age: 56 +Address: 10 Foobar Way +""" + + var ss = newStringStream(data) + expect(IntegrityError): + discard toSeq(recordsInSet(ss, "Entry")) + + test "mandatory and prohibit for same field fails": + const data1 = """ +%rec: Entry +%mandatory: Name +%prohibit: Name +""" + + var ss = newStringStream(data1) + var records = toSeq(records(ss)) + expect(Exception): + discard newRecordSet(records[0]) + + const data2 = """ +%rec: Entry +%prohibit: Name +%mandatory: Name +""" + + ss = newStringStream(data2) + records = toSeq(records(ss)) + expect(Exception): + discard newRecordSet(records[0]) + + test "allowed and prohibit for same field fails": + const data1 = """ +%rec: Entry +%allowed: Name +%prohibit: Name +""" + + var ss = newStringStream(data1) + var records = toSeq(records(ss)) + expect(Exception): + discard newRecordSet(records[0]) + + const data2 = """ +%rec: Entry +%prohibit: Name +%allowed: Name +""" + + ss = newStringStream(data2) + records = toSeq(records(ss)) + expect(Exception): + discard newRecordSet(records[0]) -- cgit v1.2.3