Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# v1.10.4-alpha0

* Add support for marking properties as read-only, pr #470.

# v1.10.3

* Add `int.of_type(..)` to convert an ID to a stored intance, pr #466.
Expand Down
17 changes: 9 additions & 8 deletions inc/ti/field.t.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,19 @@ typedef ti_val_t * (*ti_field_dval_cb) (ti_field_t *);

enum
{
TI_FIELD_FLAG_SAME_DEEP=1,
TI_FIELD_FLAG_SAME_DEEP=1<<0,

/* exactly one of the DEEP flags below will be set on a field */
TI_FIELD_FLAG_MIN_DEEP=2,
TI_FIELD_FLAG_MAX_DEEP=4,
TI_FIELD_FLAG_DEEP=8, /* this flag returns deep as-is */
TI_FIELD_FLAG_ENAME=16, /* return enumerator names instead of value */
TI_FIELD_FLAG_MIN_DEEP=1<<1,
TI_FIELD_FLAG_MAX_DEEP=1<<2,
TI_FIELD_FLAG_DEEP=1<<3, /* this flag returns deep as-is */
TI_FIELD_FLAG_ENAME=1<<4, /* return enumerator names instead of value */

TI_FIELD_FLAG_NO_IDS=TI_FLAGS_NO_IDS, /* 32 */
TI_FIELD_FLAG_NO_IDS=TI_FLAGS_NO_IDS, /* 32 (1<<5) */

TI_FIELD_FLAG_SKIP_NIL=64, /* skip when prop is nil */
TI_FIELD_FLAG_SKIP_FALSE=128, /* skip when prop evaluates to false */
TI_FIELD_FLAG_SKIP_NIL=1<<6, /* skip when prop is nil */
TI_FIELD_FLAG_SKIP_FALSE=1<<7, /* skip when prop evaluates to false */
TI_FIELD_FLAG_RO=1<<8, /* read-only */
};

#define TI_FIELD_MIN_MAX (TI_FIELD_FLAG_MIN_DEEP|TI_FIELD_FLAG_MAX_DEEP)
Expand Down
44 changes: 42 additions & 2 deletions inc/ti/fn/fnmodtype.h
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ static int modtype__mod_cb(ti_thing_t * thing, modtype__mod_t * w)
ex_set_mem(w->e);
}

if (w->e->nr == 0 && ex.nr)
if (w->e->nr == 0)
{
ex_set(w->e, EX_OPERATION,
"field `%s` on type `%s` is modified but at least "
Expand Down Expand Up @@ -722,9 +722,31 @@ static int type__mod_using_callback(
if (!modjob.field)
goto fail1; /* error is set */

if (modjob.field->flags & TI_FIELD_FLAG_RO)
{
if (field->flags & TI_FIELD_FLAG_RO)
ex_set(e, EX_NUM_ARGUMENTS,
"function `%s` takes at most 4 arguments when used "
"on a read-only property; if a callback is required, "
"remove the read-only mark first and re-apply read-only "
"in a subsequent request"DOC_MOD_TYPE_MOD,
fnname);
else
ex_set(e, EX_NUM_ARGUMENTS,
"function `%s` takes at most 4 arguments when making a "
"property read-only; if a callback is required, make the "
"changes first, then mark the property read-only in a "
"subsequent request"DOC_MOD_TYPE_MOD,
fnname);
goto fail2;
}

modjob.dval = modjob.field->dval_cb(modjob.field);
if (!modjob.dval)
goto fail2; /* error is set */
{
ex_set_mem(e);
goto fail2;
}

imap = ti_type_collect_things(query, field->type);
if (!imap)
Expand Down Expand Up @@ -1147,6 +1169,15 @@ static void type__rel_add(
ti_field_t * ofield;
ti_task_t * task;

if (field->flags & TI_FIELD_FLAG_RO)
{
ex_set(e, EX_OPERATION,
"failed to create relation; "
"property `%s` on type `%s` is read-only",
field->name->str, field->type->name);
return;
}

otype = modtype__type_from_field(field, e);
if (!otype)
return;
Expand All @@ -1173,6 +1204,15 @@ static void type__rel_add(
return;
}

if (ofield->flags & TI_FIELD_FLAG_RO)
{
ex_set(e, EX_OPERATION,
"failed to create relation; "
"property `%s` on type `%s` is read-only",
ofield->name->str, otype->name);
return;
}

if (type != otype && (
modtype__has_lock(query, otype, e) || ti_type_try_lock(otype, e)))
return;
Expand Down
4 changes: 2 additions & 2 deletions inc/ti/version.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

#define TI_VERSION_MAJOR 1
#define TI_VERSION_MINOR 10
#define TI_VERSION_PATCH 3
#define TI_VERSION_PATCH 4

/* The syntax version is used to test compatibility with functions
* using the `ti_nodes_check_syntax()` function */
Expand All @@ -25,7 +25,7 @@
* "-rc0"
* ""
*/
#define TI_VERSION_PRE_RELEASE ""
#define TI_VERSION_PRE_RELEASE "-alpha0"

#define TI_MAINTAINER \
"Jeroen van der Heijden <[email protected]>"
Expand Down
117 changes: 110 additions & 7 deletions itest/test_advanced.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ async def test_adv_rename(self, client):
d: '[A?]',
e: '[A?]?',
f: '{A}',
g: '{A}?',
g: '{A}?(ro)',
h: 'C',
i: 'C?',
j: '[C]',
Expand All @@ -242,7 +242,7 @@ async def test_adv_rename(self, client):
['d', '[Name?]'],
['e', '[Name?]?'],
['f', '{Name}'],
['g', '{Name}?'],
['g', '{Name}?(ro)'],
['h', 'Color'],
['i', 'Color?'],
['j', '[Color]'],
Expand Down Expand Up @@ -1622,7 +1622,7 @@ async def test_export(self, client):
});
set_type('W', {
arr: [{
w: '?W?',
w: '?W?(ro)',
}],
obj: {
obj: {
Expand Down Expand Up @@ -2028,7 +2028,7 @@ async def test_ren_type_typed(self, client):
set_type('A', {
a: 'A?',
b: 'B',
da: '&^dict<uuid:A>',
da: '&^dict<uuid:A>(ro)',
db: 'dict<any:B>?',
dc: 'dict<str:C?>',
dd: 'dict<int:D?>?',
Expand All @@ -2040,10 +2040,13 @@ async def test_ren_type_typed(self, client):
lb: '[B]?',
lc: '[C?]',
ld: '[D?]?',
ra: 'A?(ro)',
rb: 'A(ro)',
sa: '{A}',
sb: '{B}?',
sc: '{B}(ro)',
na: [{
a: '&+A?'
a: '&+A?(ro)'
}],
nt: {
t: {
Expand Down Expand Up @@ -2077,7 +2080,7 @@ async def test_ren_type_typed(self, client):
],
[
"da",
"&^dict<uuid:AA>"
"&^dict<uuid:AA>(ro)"
],
[
"db",
Expand Down Expand Up @@ -2123,6 +2126,14 @@ async def test_ren_type_typed(self, client):
"ld",
"[DD?]?"
],
[
"ra",
"AA?(ro)"
],
[
"rb",
"AA(ro)"
],
[
"sa",
"{AA}"
Expand All @@ -2131,11 +2142,15 @@ async def test_ren_type_typed(self, client):
"sb",
"{BB}?"
],
[
"sc",
"{BB}(ro)"
],
[
"na",
[
{
"a": "&+AA?"
"a": "&+AA?(ro)"
}
]
],
Expand Down Expand Up @@ -3181,6 +3196,94 @@ async def test_wse_shortcut_str(self, client: Client):
r"add a space between `\)` and `!=` to resolve"):
await client.query('test()!=0;')

async def test_read_only(self, client: Client):
await client.query("""//ti
set_type('T', {
name: 'str<::unknown>(ro)'
});
.t1 = T{};
.t2 = T{name: 'iris'};
.t3 = T({name: 'fleur'});
.t4 = .t3.dup();
""")
names = await client.query("""//ti
[.t1.name, .t2.name, .t3.name, .t4.name];
""")
self.assertEqual(names, ['unknown', 'iris', 'fleur', 'fleur'])
with self.assertRaisesRegex(
OperationError,
r"mismatch in type `T`; property `name` is read-only"):
await client.query(f"""//ti
.t4.name = 'Nissa';
""")
with self.assertRaisesRegex(
NumArgumentsError,
r"function `mod_type` with task `mod` takes at most 4 "
r"arguments when used on a read-only property; if a callback "
r"is required, remove the read-only mark first and re-apply "
r"read-only in a subsequent request; "):
await client.query("""//ti
mod_type('T', 'mod', 'name', 'str<::unknown>(ro)', |t| {
`{t.name[0].upper()}{t.name[1:]}`;
});
""")
await client.query("""//ti
mod_type('T', 'mod', 'name', 'str<::unknown>', |t| {
`{t.name[0].upper()}{t.name[1:]}`;
});
""")

with self.assertRaisesRegex(
NumArgumentsError,
r"function `mod_type` with task `mod` takes at most 4 "
r"arguments when making a property read-only; if a callback "
r"is required, make the changes first, then mark the "
r"property read-only in a subsequent request;"):
await client.query("""//ti
mod_type('T', 'mod', 'name', 'str<::unknown>(ro)', |t| nil);
""")

await client.query("""//ti
.t4.name = 'Nissa';
mod_type('T', 'mod', 'name', 'str<::unknown>(ro)');
""")
names = await client.query("""//ti
[.t1.name, .t2.name, .t3.name, .t4.name];
""")
self.assertEqual(names, ['Unknown', 'Iris', 'Fleur', 'Nissa'])
with self.assertRaisesRegex(
OperationError,
r"mismatch in type `T`; property `name` is read-only"):
await client.query(f"""//ti
.t1.name = 'Unknown';
""")

await client.query("""//ti
new_type('A');
new_type('B');
set_type('A', {
a: 'A?(ro)',
b: 'B?(ro)',
});
set_type('B', {
a: 'A?',
});
""")
with self.assertRaisesRegex(
OperationError,
r"failed to create relation; property `a` on "
r"type `A` is read-only"):
await client.query(f"""//ti
mod_type('A', 'rel', 'a', 'a');
""")
with self.assertRaisesRegex(
OperationError,
r"failed to create relation; property `b` on "
r"type `A` is read-only"):
await client.query(f"""//ti
mod_type('B', 'rel', 'a', 'b');
""")


if __name__ == '__main__':
run_test(TestAdvanced())
4 changes: 2 additions & 2 deletions itest/test_enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -925,7 +925,7 @@ async def test_def_enum_definition(self, client):
await q("""//ti
set_type('T', {
C1: 'Colors{Purple}',
C2: 'Colors{Yellow}?',
C2: 'Colors{Yellow}?(ro)',
C3: 'Colors',
C4: 'Colors?',
C5: '?[Colors?]?'
Expand Down Expand Up @@ -1039,7 +1039,7 @@ async def test_def_enum_definition(self, client):
""")
self.assertEqual(res, [
['C1', 'Color{Red}'],
['C2', 'Color{Orange}?'],
['C2', 'Color{Orange}?(ro)'],
['C3', 'Color'],
['C4', 'Color?'],
['C5', '?[Color?]?'],
Expand Down
3 changes: 3 additions & 0 deletions itest/test_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ async def test_export(self, client0, client1):
.r.name = 'master';
.r.r = R{name: 'slave'};
.room.set_name('ROOM');
mod_type('T', 'add', 'key', 'str(ro)', 'secretkey');
""")

dump = await client0.query(r"""//ti
Expand Down Expand Up @@ -199,6 +200,8 @@ async def test_import(self, client0, client1):
assert(is_nil(.t.id()));
assert(procedures_info().len() == 1);
assert(.room.name() == 'ROOM');
assert(.key == 'secretkey');
assert(is_err(try(.key = 'test', operation_err())));
""")

res = await client.query(
Expand Down
2 changes: 1 addition & 1 deletion src/ti/ctask.c
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ static int ctask__set(ti_thing_t * thing, mp_unp_t * up)
{
log_critical(
"task `set` to "TI_THING_ID": "
"cannot make value relation",
"error setting property",
thing->id);
goto fail;
}
Expand Down
18 changes: 18 additions & 0 deletions src/ti/field.c
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,12 @@ static int field__init(ti_field_t * field, ex_t * e)
field->flags |= TI_FIELD_FLAG_DEEP; /* most common, first check */
}

if (n >= 4 && memcmp(str+n-4, "(ro)", 4) == 0)
{
n-=4;
field->flags |= TI_FIELD_FLAG_RO;
}

if (str[n-1] == '?')
{
field->spec |= TI_SPEC_NILLABLE;
Expand Down Expand Up @@ -2044,6 +2050,11 @@ int ti_field_make_assignable(
{
uint16_t spec = field->spec & TI_SPEC_MASK_NILLABLE;

if ((field->flags & TI_FIELD_FLAG_RO) &&
parent &&
vec_get(parent->items.vec, field->idx))
goto read_only_error;

if (spec < TI_SPEC_ANY)
{
/*
Expand Down Expand Up @@ -2481,6 +2492,13 @@ int ti_field_make_assignable(
field->type->name,
field->name->str);
return e->nr;

read_only_error:
ex_set(e, EX_OPERATION,
"mismatch in type `%s`; property `%s` is read-only",
field->type->name,
field->name->str);
return e->nr;
}

/*
Expand Down
Loading
Loading