Skip to content

Commit

Permalink
feat: better extend feature
Browse files Browse the repository at this point in the history
  • Loading branch information
tpluscode committed Jan 17, 2024
1 parent 407acea commit 2aab3d0
Show file tree
Hide file tree
Showing 9 changed files with 172 additions and 57 deletions.
19 changes: 19 additions & 0 deletions .changeset/slow-emus-sit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
"@zazuko/env": major
"@zazuko/env-node": major
---

Change the method of creating derived environments:

1. The additional factories have been removed from the `create` function.
2. To create a derived environment, use the constructor overload which takes a parent environment as the second argument in addition to the array of additional factories.

For example,

```js
import env from '@zazuko/env';
import Environment from '@zazuko/env/Environment.js';
import MyFactory from './my-factory.js';

const myEnv = Environment([MyFactory], env);
```
31 changes: 5 additions & 26 deletions packages/env-node/index.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,15 @@
import formats from '@rdfjs/formats'
import type { FactoryConstructor } from '@rdfjs/environment/Environment.js'
import Environment from '@rdfjs/environment'
import DataFactory from '@rdfjs/data-model/Factory.js'
import NamespaceFactory from '@rdfjs/namespace/Factory.js'
import FormatsFactory from '@rdfjs/formats/Factory.js'
import TermMapFactory from '@rdfjs/term-map/Factory.js'
import TermSetFactory from '@rdfjs/term-set/Factory.js'
import NsBuildersFactory from '@tpluscode/rdf-ns-builders'
import ClownfaceFactory from 'clownface/Factory.js'
import TraverserFactory from '@rdfjs/traverser/Factory.js'
import DatasetFactory from '@zazuko/env/lib/DatasetFactory.js'
import baseEnv from '@zazuko/env'
import Environment from '@zazuko/env/Environment.js'
import FsUtilsFactory from '@zazuko/rdf-utils-fs/Factory.js'
import FetchFactory from '@rdfjs/fetch-lite/Factory.js'

export function create<F extends FactoryConstructor>(...additionalFactories: F[]) {
// TODO: improve types so that it's not necessary to duplicate this from `@zazuko/env`
export function create() {
const env = new Environment([
DataFactory,
DatasetFactory,
FormatsFactory,
NamespaceFactory,
NsBuildersFactory,
ClownfaceFactory,
TermMapFactory,
TermSetFactory,
TraverserFactory,
FsUtilsFactory,
FetchFactory,
...additionalFactories,
])
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(<any>env).formats.import(formats)
], baseEnv)
env.formats.import(formats)
return env
}

Expand Down
28 changes: 28 additions & 0 deletions packages/env/Environment.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Environment, FactoryConstructor } from '@rdfjs/environment/Environment.js'
import { DerivedEnvironment } from './lib/extend.js'

type Narrow<T> =
| (T extends infer U ? U : never)
| Extract<T, number | string | boolean | bigint | symbol | null | undefined | []>
| ([T] extends [[]] ? [] : { [K in keyof T]: Narrow<T[K]> });

type ReturnFactory<C> = C extends FactoryConstructor<infer X> ? X : C;
type Distribute<U> = U extends any ? ReturnFactory<U> : never;

interface EnvironmentCtor {
new<F extends ReadonlyArray<FactoryConstructor<any>>>(
factories: Narrow<F>,
options?: { bind: boolean },
): Environment<Distribute<F[number]>>

new<F extends ReadonlyArray<FactoryConstructor<any>>, E extends Environment<any>>(
factories: Narrow<F>,
parent?: E,
options?: { bind: boolean },
): DerivedEnvironment<E, Environment<Distribute<F[number]>>>
}

declare const environment: EnvironmentCtor

export default environment
38 changes: 38 additions & 0 deletions packages/env/Environment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { extend } from './lib/extend.js'

export default class Environment {
constructor(factories, parent, { bind = false } = {}) {
this._parent = parent
this._factories = factories

const extended = parent ? extend(parent, this) : this

for (const factory of this._factories) {
if (typeof factory.prototype.init === 'function') {
factory.prototype.init.call(extended)
}

for (const method of factory.exports || []) {
if (bind) {
this[method] = factory.prototype[method].bind(extended)
} else {
this[method] = factory.prototype[method]
}
}
}

return extended
}

clone() {
const env = new Environment(this._factories, this._parent)

for (const factory of env._factories) {
if (typeof factory.prototype.clone === 'function') {
factory.prototype.clone.call(env, this)
}
}

return env
}
}
8 changes: 3 additions & 5 deletions packages/env/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import Environment from '@rdfjs/environment'
import type { FactoryConstructor, Environment as IE } from '@rdfjs/environment/Environment.js'
import DataFactory from '@rdfjs/data-model/Factory.js'
import NamespaceFactory from '@rdfjs/namespace/Factory.js'
import FormatsFactory from '@rdfjs/formats/Factory.js'
Expand All @@ -8,11 +6,12 @@ import TermSetFactory from '@rdfjs/term-set/Factory.js'
import NsBuildersFactory from '@tpluscode/rdf-ns-builders'
import ClownfaceFactory from 'clownface/Factory.js'
import TraverserFactory from '@rdfjs/traverser/Factory.js'
import Environment from './Environment.js'
import DatasetFactory from './lib/DatasetFactory.js'

export type DerivedEnvironment<Env, Ex> = Env extends IE<infer F> ? IE<Ex | F> : never
export { DerivedEnvironment } from './lib/extend.js'

export function create<F extends FactoryConstructor>(...additionalFactories: F[]) {
export function create() {
return new Environment([
DataFactory,
DatasetFactory,
Expand All @@ -23,7 +22,6 @@ export function create<F extends FactoryConstructor>(...additionalFactories: F[]
TermMapFactory,
TermSetFactory,
TraverserFactory,
...additionalFactories,
])
}

Expand Down
27 changes: 27 additions & 0 deletions packages/env/lib/extend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Environment } from '@rdfjs/environment/Environment.js'

type Factories<E> = E extends Environment<infer F> ? F : never;
type Distribute<U> = U extends any ? Factories<U> : never;

type CombinedEnvironment<E extends ReadonlyArray<Environment<any>>> = Environment<Distribute<E[number]>>
export type DerivedEnvironment<Env extends Environment<unknown>, Ex extends Environment<unknown>> = CombinedEnvironment<[Env, Ex]>

export function extend<E extends Environment<any>, P extends Environment<any>>(self: E, parent: P) {
return new Proxy([self, parent], {
get(target, prop) {
const value = target.find(env => prop in env)
if (value) {
return value[prop as keyof E]
}
},
set(target, prop, value) {
target[0][prop as keyof E] = value

return true
},
has(target, prop) {
return target.some(env => prop in env)
},
}) as any as DerivedEnvironment<P, E>
}
37 changes: 37 additions & 0 deletions packages/env/test/Environment.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import DataFactory from '@rdfjs/data-model/Factory.js'
import DatasetFactory from '@rdfjs/dataset/Factory.js'
import { describe } from 'mocha'
import { expect } from 'chai'
import { schema } from '@tpluscode/rdf-ns-builders'
import Environment from '../Environment.js'
import { TestFactory } from './TestFactory.js'

describe('Environment', () => {
context('with parent', () => {
it('merges all factories', () => {
// given
const dataEnv = new Environment([DataFactory])

// when
const env = new Environment([DatasetFactory], dataEnv)

// then
expect(env).to.have.property('dataset')
expect(env).to.have.property('namedNode')
expect(env.namedNode('http://schema.org/Person')).to.deep.eq(schema.Person)
})

it('merge multiple levels', () => {
// given
const dataEnv = new Environment([DataFactory])
const datasetEnv = new Environment([DatasetFactory], dataEnv)

// when
const env = new Environment([TestFactory], datasetEnv)

// then
const test = env.test()
expect(test.size).to.eq(1)
})
})
})
15 changes: 15 additions & 0 deletions packages/env/test/TestFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Environment } from '@rdfjs/environment/Environment.js'
import DataFactory from '@rdfjs/data-model/Factory.js'
import DatasetFactory from '@rdfjs/dataset/Factory.js'

export class TestFactory {
test(this: Environment<DataFactory | DatasetFactory>) {
return this.dataset([
this.quad(this.blankNode(), this.namedNode('http://example.org/'), this.literal('test')),
])
}

static get exports() {
return ['test']
}
}
26 changes: 0 additions & 26 deletions packages/env/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,32 +22,6 @@ describe('@zazuko/env', () => {
testStandardFactories()
})

context('with additional factory', () => {
class Factory {
get foo() {
return 'bar'
}

static get exports() {
return ['foo']
}
}

before(() => {
env = create(Factory)
})

it('adds that factory', () => {
// when
const env = create(Factory)

// then
expect(env.foo).to.eq('bar')
})

testStandardFactories()
})

function testStandardFactories() {
it('includes include clownface factory', () => {
expect(env.clownface().namedNode('http://example.com/foo').term).to.deep.eq($rdf.namedNode('http://example.com/foo'))
Expand Down

0 comments on commit 2aab3d0

Please sign in to comment.