From b1fde3bf94ed4831b4b536f18a93ddcd7cc75862 Mon Sep 17 00:00:00 2001 From: Stephen Mizell Date: Mon, 25 May 2015 21:36:13 +0200 Subject: [PATCH] Add convenience methods for instances This adds the following methods for Minim instances: 1. equals - All element types 2. getById - For collections 3. contains - For collections 4. first - For collections 5. second - For collection 6. last - For collections This also makes meta attributes into Minin instances for convenience purposes. --- README.md | 53 +++++++++++++ package.json | 2 +- src/primitives.es6 | 161 +++++++++++++++++++++++++++++++++++---- test.js | 8 -- test/primitives-test.es6 | 104 ++++++++++++++++++++++++- test/registry-test.es6 | 34 ++++++++- 6 files changed, 338 insertions(+), 24 deletions(-) delete mode 100644 test.js diff --git a/README.md b/README.md index d0887edb..822e6c8a 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,15 @@ var stringType = minim.convertToType("foobar"); var compact = stringType.toCompactRefract(); // ['string', {}, {}, 'foobar'] ``` +#### equals + +Allows for testing equality with the content of the element. + +```javascript +var stringType = minim.convertToType("foobar"); +stringType.equals('abcd'); // returns false +``` + ### Element Types Minim supports the following primitive types and the @@ -310,6 +319,50 @@ var numbers = arrayType.children(function(el) { Because only children are tested with the condition function, the values `[1, 2]` are seen as an `array` type whose content is never tested. Thus, the only direct child which is a number type is `3`. +##### getById + +Search the entire tree to find a matching ID. + +```javascript +elTree.getById('some-id'); +``` + +##### contains + +Test to see if a collection contains the value given. Does a deep equality check. + +```javascript +var arrayType = new minim.ArrayType(['a', [1, 2], 'b', 3]); +arrayType.contains('a'); // returns true +``` + +##### first + +Returns the first element in the collection. + +```javascript +var arrayType = new minim.ArrayType(['a', [1, 2], 'b', 3]); +arrayType.first(); // returns the element for "a" +``` + +##### second + +Returns the second element in the collection. + +```javascript +var arrayType = new minim.ArrayType(['a', [1, 2], 'b', 3]); +arrayType.second(); // returns the element for "[1, 2]" +``` + +##### last + +Returns the last element in the collection. + +```javascript +var arrayType = new minim.ArrayType(['a', [1, 2], 'b', 3]); +arrayType.last(); // returns the element for "3" +``` + #### ObjectType This is a type for representing objects. Objects store their items as an ordered array, so they inherit most of the methods above from the `ArrayType`. diff --git a/package.json b/package.json index 2c7bbda8..81d7ccba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minim", - "version": "0.3.1", + "version": "0.4.0", "description": "A library for interacting with JSON through Refract elements", "main": "lib/minim.js", "scripts": { diff --git a/src/primitives.es6 b/src/primitives.es6 index e2a16cb9..75c35cce 100644 --- a/src/primitives.es6 +++ b/src/primitives.es6 @@ -12,6 +12,78 @@ import registry from './registry'; export const attributeElementKeys = Symbol('attributeElementKeys'); export const storedElement = Symbol('storedElement'); +export class Meta { + constructor(meta = {}) { + this.meta = {}; + + for (let key of Object.keys(meta)) { + this.meta[key] = registry.toType(meta[key]); + } + } + + toObject() { + let meta = {}; + + for (let key of Object.keys(this.meta)) { + meta[key] = this.meta[key].toValue(); + } + + return meta; + } + + getProperty(name, value) { + if (!this.meta[name]) { + this.meta[name] = registry.toType(value); + } + + return this.meta[name]; + } + + setProperty(name, value) { + this.meta[name] = registry.toType(value); + } + + get id() { + return this.getProperty('id', ''); + } + + set id(value) { + this.setProperty('id', value); + } + + get class() { + return this.getProperty('class', []); + } + + set class(value) { + this.setProperty('class', value); + } + + get name() { + return this.getProperty('name', ''); + } + + set name(value) { + this.setProperty('name', value); + } + + get title() { + return this.getProperty('title', ''); + } + + set title(value) { + this.setProperty('title', value); + } + + get description() { + return this.getProperty('description', ''); + } + + set description(value) { + this.setProperty('description', value); + } +} + /* * ElementType is the base element from which all other elements are built. * It has no specific information about how to handle the content, but is @@ -19,7 +91,7 @@ export const storedElement = Symbol('storedElement'); */ export class ElementType { constructor(content = null, meta = {}, attributes = {}) { - this.meta = meta; + this.meta = new Meta(meta); this.attributes = attributes; this.content = content; this[attributeElementKeys] = []; @@ -46,7 +118,7 @@ export class ElementType { const attributes = this.convertAttributesToRefract('toRefract'); const initial = { element: this.element, - meta: this.meta, + meta: this.meta.toObject(), attributes, content: this.content }; @@ -55,7 +127,7 @@ export class ElementType { toCompactRefract() { const attributes = this.convertAttributesToRefract('toCompactRefract'); - return [this.element, this.meta, attributes, this.content]; + return [this.element, this.meta.toObject(), attributes, this.content]; } /* @@ -91,7 +163,7 @@ export class ElementType { } fromRefract(dom) { - this.meta = dom.meta; + this.meta = new Meta(dom.meta); this.attributes = dom.attributes; this.content = dom.content; @@ -106,7 +178,7 @@ export class ElementType { } fromCompactRefract(tuple) { - this.meta = tuple[1]; + this.meta = new Meta(tuple[1]); this.attributes = tuple[2]; this.content = tuple[3]; @@ -128,6 +200,10 @@ export class ElementType { this.content = content; return this; } + + equals(value) { + return _.isEqual(this.toValue(), value); + } } export class NullType extends ElementType { @@ -209,11 +285,11 @@ class Collection extends ElementType { const attributes = this.convertAttributesToRefract('toCompactRefract'); const compactDoms = this.content.map((el) => el.toCompactRefract()); - return [this.element, this.meta, attributes, compactDoms]; + return [this.element, this.meta.toObject(), attributes, compactDoms]; } fromRefract(dom) { - this.meta = dom.meta; + this.meta = new Meta(dom.meta); this.attributes = dom.attributes; this.content = (dom.content || []).map((content) => registry.fromRefract(content)); @@ -221,11 +297,15 @@ class Collection extends ElementType { this.convertAttributesToElements((attribute) => registry.fromRefract(attribute)); + if (this.element !== dom.element) { + this.element = dom.element; + } + return this; } fromCompactRefract(tuple) { - this.meta = tuple[1]; + this.meta = new Meta(tuple[1]); this.attributes = tuple[2]; this.content = (tuple[3] || []).map((content) => registry.fromCompactRefract(content)); @@ -233,6 +313,10 @@ class Collection extends ElementType { this.convertAttributesToElements((attribute) => registry.fromCompactRefract(attribute)); + if (this.element !== tuple[0]) { + this.element = tuple[0]; + } + return this; } @@ -302,6 +386,47 @@ class Collection extends ElementType { newArray.content = this.findElements(condition, {recursive: false}); return newArray; } + + /* + * Search the tree recursively and find the element with the matching ID + */ + getById(id) { + return this.find(item => item.meta.id.toValue() === id).first(); + } + + /* + * Return the first item in the collection + */ + first() { + return this.get(0); + } + + /* + * Return the second item in the collection + */ + second() { + return this.get(1); + } + + /* + * Return the last item in the collection + */ + last() { + return this.get(this.length - 1); + } + + /* + * Looks for matching children using deep equality + */ + contains(value) { + for (let item of this.content) { + if (_.isEqual(item.toValue(), value)) { + return true; + } + } + + return false; + } } export class ArrayType extends Collection { @@ -337,19 +462,19 @@ export class ObjectType extends Collection { toValue() { return this.content.reduce((results, el) => { - results[el.meta.name] = el.toValue(); + results[el.meta.name.toValue()] = el.toValue(); return results; }, {}); } get(name) { return name === undefined ? this : _.first( - this.content.filter((value) => value.meta.name === name) + this.content.filter((value) => value.meta.name.toValue() === name) ); } set(name, value) { - const location = this.content.map(item => item.meta.name).indexOf(name); + const location = this.content.map(item => item.meta.name.toValue()).indexOf(name); let refractedContent = registry.toType(value); // TODO: Should we mutate or copy here? Of course it doesn't matter @@ -369,14 +494,24 @@ export class ObjectType extends Collection { } keys() { - return this.content.map((value) => value.meta.name); + return this.content.map((value) => value.meta.name.toValue()); } values() { return this.content.map((value) => value.get()); } + hasKey(value) { + for (let item of this.content) { + if (item.meta.name.equals(value)) { + return true; + } + } + + return false; + } + items() { - return this.content.map((value) => [value.meta.name, value.get()]); + return this.content.map((value) => [value.meta.name.toValue(), value.get()]); } } diff --git a/test.js b/test.js deleted file mode 100644 index ed1a0f4e..00000000 --- a/test.js +++ /dev/null @@ -1,8 +0,0 @@ -var minim = require('./index'); - -var objectType = new minim.ObjectType() - .set('name', 'John Doe') - .set('email', 'john@example.com') - .set('id', 4) - -console.log(objectType.toValue()); diff --git a/test/primitives-test.es6 b/test/primitives-test.es6 index a63cf56b..98ae649a 100644 --- a/test/primitives-test.es6 +++ b/test/primitives-test.es6 @@ -3,6 +3,28 @@ import minim from '../lib/minim'; describe('Minim Primitives', () => { describe('ElementType', () => { + context('when initializing', () => { + let el; + + before(() => { + el = new minim.ElementType({}, { + id: 'foobar', + class: ['a', 'b'], + name: 'name', + title: 'Title', + description: 'Description' + }); + }); + + it('should initialize the correct meta data', () => { + expect(el.meta.id.get()).to.equal('foobar'); + expect(el.meta.class.toValue()).to.deep.equal(['a', 'b']); + expect(el.meta.name.get()).to.equal('name'); + expect(el.meta.title.get()).to.equal('Title'); + expect(el.meta.description.get()).to.equal('Description'); + }); + }); + describe('#element', () => { context('when getting an element that has not been set', () => { let el; @@ -29,6 +51,31 @@ describe('Minim Primitives', () => { }); }) }); + + describe('#equals', () => { + let el; + + before(() => { + el = new minim.ElementType({ + foo: 'bar' + }, { + id: 'foobar' + }); + }); + + it('returns true when they are equal', () => { + expect(el.meta.id.equals('foobar')).to.be.true; + }); + + it('returns false when they are not equal', () => { + expect(el.meta.id.equals('not-equal')).to.be.false; + }); + + it('does a deep equality check', () => { + expect(el.equals({ foo: 'bar'})).to.be.true; + expect(el.equals({ foo: 'baz'})).to.be.false; + }); + }); }); describe('convertToType', () => { @@ -444,6 +491,9 @@ describe('Minim Primitives', () => { content: [ { element: 'string', + meta: { + id: 'nested-id' + }, content: 'bar' }, { element: 'number', @@ -455,11 +505,13 @@ describe('Minim Primitives', () => { } ] }; + + let doc; let strings; let recursiveStrings; before(() => { - const doc = minim.convertFromRefract(refract); + doc = minim.convertFromRefract(refract); strings = doc.children(el => el.element === 'string'); recursiveStrings = doc.find(el => el.element === 'string'); }); @@ -483,6 +535,49 @@ describe('Minim Primitives', () => { expect(recursiveStrings.toValue()).to.deep.equal(['foobar', 'hello world', 'baz', 'bar']); }); }); + + describe('#first', () => { + it('returns the first item', () => { + expect(doc.first()).to.deep.equal(doc.content[0]); + }); + }); + + describe('#second', () => { + it('returns the first item', () => { + expect(doc.second()).to.deep.equal(doc.content[1]); + }); + }); + + describe('#last', () => { + it('returns the first item', () => { + expect(doc.last()).to.deep.equal(doc.content[2]); + }); + }); + + describe('#getById', () => { + it('returns the item for the ID given', () => { + expect(doc.getById('nested-id').toValue()).to.equal('bar'); + }); + }); + + describe('#contains', () => { + it('uses deep equality', () => { + expect(doc.get(2).contains(['not', 'there'])).to.be.false; + expect(doc.get(2).contains(['bar', 4])).to.be.true; + }); + + context('when given a value that is in the array', () => { + it('returns true', () => { + expect(doc.contains('foobar')).to.be.true; + }); + }); + + context('when given a value that is not in the array', () => { + it('returns false', () => { + expect(doc.contains('not-there')).to.be.false; + }); + }); + }); }); }); @@ -801,6 +896,13 @@ describe('Minim Primitives', () => { }); }); + describe('#hasKey', function() { + it('checks to see if a key exists', () => { + expect(objectType.hasKey('foo')).to.be.true; + expect(objectType.hasKey('does-not-exist')).to.be.false; + }); + }); + describe('#items', () => { it('provides a list of name/value pairs to iterate', () => { const keys = []; diff --git a/test/registry-test.es6 b/test/registry-test.es6 index cc96115f..f11668c9 100644 --- a/test/registry-test.es6 +++ b/test/registry-test.es6 @@ -42,7 +42,7 @@ describe('Minim registry', () => { expect(converted).to.equal(myType); }); - it('should allow for roundtrip conversions', () => { + it('should allow for roundtrip conversions for value types', () => { registry.register('foo', minim.StringType); // Full version @@ -53,6 +53,38 @@ describe('Minim registry', () => { const compactValue = registry.fromCompactRefract(['foo', {}, {}, 'test']).toCompactRefract(); expect(compactValue).to.deep.equal(['foo', {}, {}, 'test']); }); + + it('should allow for roundtrip conversions for collection types', () => { + registry.register('foo', minim.ArrayType); + + const fullRefractSample = { + element: 'foo', + meta: {}, + attributes: {}, + content: [ + { + element: 'string', + meta: {}, + attributes: {}, + content: 'bar' + } + ] + } + + const compactRefractSample = [ + 'foo', {}, {}, [ + ['string', {}, {}, 'bar'] + ] + ] + + // Full version + const fullVersion = registry.fromRefract(fullRefractSample).toRefract(); + expect(fullVersion).to.deep.equal(fullRefractSample); + + // Compact version + const compactValue = registry.fromCompactRefract(compactRefractSample).toCompactRefract(); + expect(compactValue).to.deep.equal(compactRefractSample); + }); }); describe('#getElementClass', () => {