Skip to content

Commit

Permalink
feat(keto-relations-parser): build relation tuple builder using a flu…
Browse files Browse the repository at this point in the history
…ent API
  • Loading branch information
getlarge committed Jan 12, 2024
1 parent 00a2c3f commit 0ffa301
Show file tree
Hide file tree
Showing 3 changed files with 259 additions and 0 deletions.
11 changes: 11 additions & 0 deletions packages/keto-relations-parser/src/lib/relation-tuple-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,17 @@ export function parseRelationTuple(
}
}

/**
* @description Converts a {@link RelationTuple} to a string
* @example
* ```typescript
* const relationTuple = new RelationTuple('namespace', 'object', 'relation', 'subjectId');
* relationTuple.toString() // => 'namespace:object#relation@subjectId'
* ```
*
* @param tuple
* @returns
*/
export const relationTupleToString = (
tuple: Partial<RelationTuple>
): RelationTupleString => {
Expand Down
121 changes: 121 additions & 0 deletions packages/keto-relations-parser/src/lib/relation-tuple.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { RelationTupleBuilder } from './relation-tuple';

describe('RelationTupleBuilder', () => {
let builder: RelationTupleBuilder;

beforeEach(() => {
builder = new RelationTupleBuilder();
});

it('should create an instance', () => {
expect(builder).toBeDefined();
});

it('should throw error when setting empty relation', () => {
expect(() => {
builder.relation = '';
}).toThrow('relation cannot be empty');
});

it('should throw error when setting relation with forbidden characters', () => {
expect(() => {
builder.relation = 'relation@';
}).toThrow('relation cannot contain any of the following characters: :@#');
});

it('should set and get relation', () => {
builder.relation = 'relation';
expect(builder.relation).toBe('relation');
});

it('should throw error when setting empty namespace', () => {
expect(() => {
builder.namespace = '';
}).toThrow('namespace cannot be empty');
});

it('should throw error when setting namespace with forbidden characters', () => {
expect(() => {
builder.namespace = 'namespace@';
}).toThrow('namespace cannot contain any of the following characters: :@#');
});

it('should set and get namespace', () => {
builder.namespace = 'namespace';
expect(builder.namespace).toBe('namespace');
});

it('should throw error when setting empty object', () => {
expect(() => {
builder.object = '';
}).toThrow('object cannot be empty');
});

it('should throw error when setting object with forbidden characters', () => {
expect(() => {
builder.object = 'object@';
}).toThrow('object cannot contain any of the following characters: :@#');
});

it('should set and get object', () => {
builder.object = 'object';
expect(builder.object).toBe('object');
});

it('should throw error when setting empty subjectIdOrSet', () => {
expect(() => {
builder.subjectIdOrSet = '';
}).toThrow('subjectIdOrSet cannot be empty');
});

it('should throw error when setting subjectIdOrSet with forbidden characters', () => {
expect(() => {
builder.subjectIdOrSet = 'subjectIdOrSet@';
}).toThrow(
'subjectIdOrSet cannot contain any of the following characters: :@#'
);
});

it('should set and get subjectIdOrSet', () => {
builder.subjectIdOrSet = 'subjectIdOrSet';
expect(builder.subjectIdOrSet).toBe('subjectIdOrSet');
});

it('should throw error when setting empty relation tuple', () => {
expect(() => {
builder.isIn('').of('', '');
}).toThrow('relation cannot be empty');
});

it('should build a relation tuple string with only subjectId', () => {
builder.subject('subjectId').isIn('relation').of('namespace', 'object');
expect(builder.toString()).toBe('namespace:object#relation@subjectId');
});

it('should build a relation tuple string with subjectSet', () => {
builder
.subject('subjectNamespace', 'subjectObject', 'subjectRelation')
.isIn('relation')
.of('namespace', 'object');
expect(builder.toString()).toBe(
'namespace:object#relation@subjectNamespace:subjectObject#subjectRelation'
);
});

it('should build a human readable relation tuple string with subjectId', () => {
builder.subject('subjectId').isIn('relation').of('namespace', 'object');
expect(builder.toHumanReadableString()).toBe(
'subjectId is in relation of namespace:object'
);
});

it('should build a human readable relation tuple string with subjectSet', () => {
builder
.subject('subjectNamespace', 'subjectObject', 'subjectRelation')
.isIn('relation')
.of('namespace', 'object');
expect(builder.toHumanReadableString()).toBe(
'subjectRelation of subjectNamespace:subjectObject is in relation of namespace:object'
);
});
});
127 changes: 127 additions & 0 deletions packages/keto-relations-parser/src/lib/relation-tuple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,20 @@ export class SubjectSet {
this.object = object;
this.relation = relation;
}

toString(): string {
return `${this.namespace}:${this.object}${
this.relation ? `#${this.relation}` : ''
}`;
}
}

export class RelationTuple {
namespace: string;
object: string;
relation: string;
subjectIdOrSet: string | SubjectSet;

constructor(
namespace: string,
object: string,
Expand All @@ -32,3 +39,123 @@ export class RelationTuple {
return relationTupleToString(this);
}
}

export class RelationTupleBuilder {
private readonly forbiddenCharacters = /[:@#]/g;
private tuple: RelationTuple;

constructor() {
this.tuple = new RelationTuple('', '', '', '');
}

private validateInput(key: keyof RelationTuple, value: string) {
if (!value) {
throw new Error(`${key} cannot be empty`);
}
if (this.forbiddenCharacters.test(value)) {
throw new Error(
`${key} cannot contain any of the following characters: :@#`
);
}
}

get relation(): string {
return this.tuple.relation;
}

set relation(relation: string) {
this.validateInput('relation', relation);
this.tuple.relation = relation;
}

get namespace(): string {
return this.tuple.namespace;
}

set namespace(namespace: string) {
this.validateInput('namespace', namespace);
this.tuple.namespace = namespace;
}

get object(): string {
return this.tuple.object;
}

set object(object: string) {
this.validateInput('object', object);
this.tuple.object = object;
}

get subjectIdOrSet(): string | SubjectSet {
return this.tuple.subjectIdOrSet;
}

set subjectIdOrSet(subjectIdOrSet: string | SubjectSet) {
if (typeof subjectIdOrSet === 'string') {
this.validateInput('subjectIdOrSet', subjectIdOrSet);
} else {
this.validateInput('namespace', subjectIdOrSet.namespace);
this.validateInput('object', subjectIdOrSet.object);
if (subjectIdOrSet.relation) {
this.validateInput('relation', subjectIdOrSet.relation);
}
}
this.tuple.subjectIdOrSet = subjectIdOrSet;
}

isIn(relation: string): this {
this.relation = relation;
return this;
}

of(namespace: string, object: string): this {
this.namespace = namespace;
this.object = object;
return this;
}

subject(subjectId: string): this;
subject(namespace: string, object: string): this;
subject(namespace: string, object: string, relation: string): this;
subject(
namespaceOrSubjectId: string,
object?: string,
relation?: string
): this {
if (object) {
this.subjectIdOrSet = new SubjectSet(
namespaceOrSubjectId,
object,
relation
);
} else {
this.subjectIdOrSet = namespaceOrSubjectId;
}
return this;
}

toString(): string {
return this.tuple.toString();
}

/**
* @description Returns a human readable string
* @example
* ```typescript
* const relationTuple = new RelationTupleBuilder()
* .subject('subject_namespace', 'subject_object', 'subject_relation')
* .of('namespace', 'object')
* .isIn('relations');
* relationTuple.toHumanReadableString(); // => subject_relation of subject_namespace:subject_object is in relations of namespace:object
* ```
* @returns {string} human readable string
*/
toHumanReadableString(): string {
if (typeof this.subjectIdOrSet === 'string') {
return `${this.subjectIdOrSet} is in ${this.relation} of ${this.namespace}:${this.object}`;
}
const { namespace, object, relation } = this.subjectIdOrSet;
const base = `${namespace}:${object} is in ${this.relation} of ${this.namespace}:${this.object}`;
return relation ? `${relation} of ${base}` : base;
}
}

0 comments on commit 0ffa301

Please sign in to comment.