Skip to content

Commit

Permalink
feat: add withoutNew option
Browse files Browse the repository at this point in the history
Also fix a bug where the proto inheritance chain was not setup properly.
Refactored tests to their own files and improved the overall test suite.
  • Loading branch information
satazor committed Apr 3, 2018
1 parent 9df979b commit e6b0c18
Show file tree
Hide file tree
Showing 17 changed files with 442 additions and 83 deletions.
55 changes: 41 additions & 14 deletions README.md
Expand Up @@ -50,7 +50,10 @@ class Person {
}
}

module.exports = withIs(Person, { className: 'Person', symbolName: '@org/package-x/person' });
module.exports = withIs(Person, {
className: 'Person',
symbolName: '@org/package-x/Person',
});
```

```js
Expand All @@ -63,7 +66,10 @@ class Animal {
}
}

module.exports = withIs(Animal, { className: 'Animal', symbolName: '@org/package-y/animal' });
module.exports = withIs(Animal, {
className: 'Animal',
symbolName: '@org/package-y/Animal',
});
```

```js
Expand Down Expand Up @@ -93,6 +99,8 @@ function Circle(radius) {
if (!(this instanceof Circle)) {
return new Circle();
}

this.radius = radius;
}
```

Expand All @@ -101,26 +109,38 @@ In such cases you can use the `withIs.proto` method:
```js
const withIs = require('class-is');

const Circle = withIs.proto(function () {
const Circle = withIs.proto(function (radius) {
if (!(this instanceof Circle)) {
return new Circle();
}
}, { className: 'Circle', symbolName: '@org/package/circle' });

const circle = Circle();

console.log(Circle.isCircle(circle));
this.radius = radius;
}, {
className: 'Circle',
symbolName: '@org/package/Circle',
});
```

The example above will print:
```
true
...or even better:

```js
const withIs = require('class-is');

function Circle(radius) {
this.radius = radius;
}

module.exports = withIs.proto(Circle, {
className: 'Circle',
symbolName: '@org/package/Circle',
withoutNew: true,
});
```


## API

### withIs(Class, { className: name, symbolName: symbol })
### withIs(Class, { className, symbolName })

###### class

Expand All @@ -140,11 +160,18 @@ Type: `String`

Unique *id* for the class. This should be namespaced so different classes from different modules do not collide and give false positives.

Example: `@organization/package/class`
Example: `@organization/package/Class`

### withIs.proto(Class, { className, symbolName, withoutNew })

The `className` and `symbolName` parameters are the same as above.

###### withoutNew

### withIs.proto(Class, { className: name, symbolName: symbol })
Type: `Boolean`
Default: `false`

Apply the same parameters as above.
Allow creating an instance without the `new` operator.


## Tests
Expand Down
12 changes: 10 additions & 2 deletions index.js
Expand Up @@ -27,12 +27,16 @@ function withIs(Class, { className, symbolName }) {
return ClassIsWrapper;
}

function withIsProto(Class, { className, symbolName }) {
function withIsProto(Class, { className, symbolName, withoutNew }) {
const symbol = Symbol.for(symbolName);

/* eslint-disable object-shorthand */
const ClassIsWrapper = {
/* eslint-disable object-shorthand */
[className]: function (...args) {
if (withoutNew && !(this instanceof ClassIsWrapper)) {
return new ClassIsWrapper(...args);
}

const _this = Class.call(this, ...args) || this;

if (_this && !_this[symbol]) {
Expand All @@ -42,6 +46,10 @@ function withIsProto(Class, { className, symbolName }) {
return _this;
},
}[className];
/* eslint-enable object-shorthand */

ClassIsWrapper.prototype = Object.create(Class.prototype);
ClassIsWrapper.prototype.constructor = ClassIsWrapper;

Object.defineProperty(ClassIsWrapper.prototype, Symbol.toStringTag, {
get() {
Expand Down
142 changes: 110 additions & 32 deletions test/es5.test.js
@@ -1,49 +1,127 @@
'use strict';

const withIs = require('..');
const {
Animal,
Plant,
Mammal,
Algae,

const Circle = withIs.proto(function () {
if (!(this instanceof Circle)) { // eslint-disable-line no-invalid-this
return new Circle();
}
}, { className: 'Circle', symbolName: '@org/package/circle' });
ExplicitWithoutNew,
ImplicitWithoutNew,
ImplicitExplicitWithoutNew,
} = require('./fixtures/es5');

const Square = withIs.proto(function () {
if (!(this instanceof Square)) { // eslint-disable-line no-invalid-this
return new Square();
}
}, { className: 'Square', symbolName: '@org/package/square' });
it('should setup the prototype chain correctly', () => {
const animal = new Animal('mammal');
const plant = new Plant('algae');

const circle = new Circle();
const square = new Square();
expect(Object.getPrototypeOf(animal)).toBe(Animal.prototype);
expect(Object.getPrototypeOf(Object.getPrototypeOf(animal))).toBe(Animal.WrappedClass.prototype);
expect(Object.getPrototypeOf(animal)).not.toBe(Plant.prototype);
expect(Object.getPrototypeOf(plant)).toBe(Plant.prototype);
expect(Object.getPrototypeOf(Object.getPrototypeOf(plant))).toBe(Plant.WrappedClass.prototype);
expect(Object.getPrototypeOf(plant)).not.toBe(Animal.prototype);

test('circle is an instance of Circle class', () => {
expect(Circle.isCircle(circle)).toBe(true);
});
expect(animal instanceof Animal).toBe(true);
expect(animal instanceof Animal.WrappedClass).toBe(true);
expect(animal instanceof Plant).toBe(false);
expect(plant instanceof Plant).toBe(true);
expect(plant instanceof Plant.WrappedClass).toBe(true);
expect(plant instanceof Animal).toBe(false);

test('square is not an instance of Circle class', () => {
expect(Circle.isCircle(square)).toBe(false);
expect(animal.getType()).toBe('mammal');
expect(plant.getType()).toBe('algae');
});

test('square is an instance of Square class', () => {
expect(Square.isSquare(square)).toBe(true);
it('should have a custom toStringTag', () => {
expect(Object.prototype.toString.call(new Animal())).toBe('[object Animal]');
expect(Object.prototype.toString.call(new Plant())).toBe('[object Plant]');
});

test('circle is not an instance of Square class', () => {
expect(Square.isSquare(circle)).toBe(false);
});
describe('is<className> method', () => {
it('should add a working is<className> static method', () => {
const animal = new Animal('mammal');
const plant = new Plant('algae');

test('calling without new', () => {
const circle = Circle(); // eslint-disable-line new-cap
expect(Animal.isAnimal(animal)).toBe(true);
expect(Animal.isAnimal(plant)).toBe(false);
expect(Plant.isPlant(plant)).toBe(true);
expect(Plant.isPlant(animal)).toBe(false);
});

expect(Circle.isCircle(circle)).toBe(true);
});
it('should not crash if `null` or `undefined` is passed to is<ClassName>', () => {
expect(Animal.isAnimal(null)).toBe(false);
expect(Animal.isAnimal(undefined)).toBe(false);
});

it('should work correctly for deep inheritance scenarios', () => {
const mammal = new Mammal();
const algae = new Algae();

test('undefined/null is not an instance of any class', () => {
expect(Circle.isCircle(undefined)).toBe(false);
expect(Circle.isCircle(null)).toBe(false);
expect(Mammal.isMammal(mammal)).toBe(true);
expect(Animal.isAnimal(mammal)).toBe(true);
expect(Mammal.isMammal(algae)).toBe(false);
expect(Animal.isAnimal(algae)).toBe(false);

expect(Algae.isAlgae(algae)).toBe(true);
expect(Plant.isPlant(algae)).toBe(true);
expect(Algae.isAlgae(mammal)).toBe(false);
expect(Plant.isPlant(mammal)).toBe(false);
});
});

test('check custom tag of Square class', () => {
expect(Object.prototype.toString.call(square)).toBe('[object Square]');
describe('new operator', () => {
it('should work on explicit without-new handling', () => {
const instance = new ExplicitWithoutNew();
const instance2 = ExplicitWithoutNew(); // eslint-disable-line new-cap

expect(Object.getPrototypeOf(instance)).toBe(ExplicitWithoutNew.prototype);
expect(Object.getPrototypeOf(Object.getPrototypeOf(instance))).toBe(ExplicitWithoutNew.WrappedClass.prototype);
expect(Object.getPrototypeOf(instance2)).toBe(ExplicitWithoutNew.prototype);
expect(Object.getPrototypeOf(Object.getPrototypeOf(instance2))).toBe(ExplicitWithoutNew.WrappedClass.prototype);

expect(instance instanceof ExplicitWithoutNew).toBe(true);
expect(instance instanceof ExplicitWithoutNew.WrappedClass).toBe(true);
expect(instance2 instanceof ExplicitWithoutNew).toBe(true);
expect(instance2 instanceof ExplicitWithoutNew.WrappedClass).toBe(true);

expect(instance.getLabel()).toBe('ExplicitWithoutNew');
expect(instance2.getLabel()).toBe('ExplicitWithoutNew');
});

it('should work on implicit without-new handling', () => {
const instance = new ImplicitWithoutNew();
const instanceNoNew = ImplicitWithoutNew(); // eslint-disable-line new-cap

expect(Object.getPrototypeOf(instance)).toBe(ImplicitWithoutNew.prototype);
expect(Object.getPrototypeOf(Object.getPrototypeOf(instance))).toBe(ImplicitWithoutNew.WrappedClass.prototype);
expect(Object.getPrototypeOf(instanceNoNew)).toBe(ImplicitWithoutNew.prototype);
expect(Object.getPrototypeOf(Object.getPrototypeOf(instanceNoNew))).toBe(ImplicitWithoutNew.WrappedClass.prototype);

expect(instance instanceof ImplicitWithoutNew).toBe(true);
expect(instance instanceof ImplicitWithoutNew.WrappedClass).toBe(true);
expect(instanceNoNew instanceof ImplicitWithoutNew).toBe(true);
expect(instanceNoNew instanceof ImplicitWithoutNew.WrappedClass).toBe(true);

expect(instance.getLabel()).toBe('ImplicitWithoutNew');
expect(instanceNoNew.getLabel()).toBe('ImplicitWithoutNew');
});

it('should work on explicit & implicit without-new handling', () => {
const instance = new ImplicitExplicitWithoutNew();
const instanceNoNew = ImplicitExplicitWithoutNew(); // eslint-disable-line new-cap

expect(Object.getPrototypeOf(instance)).toBe(ImplicitExplicitWithoutNew.prototype);
expect(Object.getPrototypeOf(Object.getPrototypeOf(instance))).toBe(ImplicitExplicitWithoutNew.WrappedClass.prototype);
expect(Object.getPrototypeOf(instanceNoNew)).toBe(ImplicitExplicitWithoutNew.prototype);
expect(Object.getPrototypeOf(Object.getPrototypeOf(instanceNoNew))).toBe(ImplicitExplicitWithoutNew.WrappedClass.prototype);

expect(instance instanceof ImplicitExplicitWithoutNew).toBe(true);
expect(instance instanceof ImplicitExplicitWithoutNew.WrappedClass).toBe(true);
expect(instanceNoNew instanceof ImplicitExplicitWithoutNew).toBe(true);
expect(instanceNoNew instanceof ImplicitExplicitWithoutNew.WrappedClass).toBe(true);

expect(instance.getLabel()).toBe('ImplicitExplicitWithoutNew');
expect(instanceNoNew.getLabel()).toBe('ImplicitExplicitWithoutNew');
});
});
92 changes: 57 additions & 35 deletions test/es6.test.js
@@ -1,45 +1,67 @@
'use strict';

const withIs = require('..');

const Person = withIs(class {
constructor(name, city) {
this.name = name;
this.city = city;
}
}, { className: 'Person', symbolName: '@org/package-x/person' });

const Animal = withIs(class {
constructor(species) {
this.species = species;
}
}, { className: 'Animal', symbolName: '@org/package-y/person' });

const diogo = new Person('Diogo', 'Porto');
const wolf = new Animal('Wolf');

test('person is an instance of Person class', () => {
expect(Person.isPerson(diogo)).toBe(true);
});
const {
Animal,
Plant,
Mammal,
Algae,
} = require('./fixtures/es6');

test('wolf is not an instance of Person class', () => {
expect(Person.isPerson(wolf)).toBe(false);
});
it('should setup the prototype chain correctly', () => {
const animal = new Animal('mammal');
const plant = new Plant('algae');

test('wolf is an instance of Animal class', () => {
expect(Animal.isAnimal(wolf)).toBe(true);
});
expect(Object.getPrototypeOf(animal)).toBe(Animal.prototype);
expect(Object.getPrototypeOf(Object.getPrototypeOf(animal))).toBe(Animal.WrappedClass.prototype);
expect(Object.getPrototypeOf(animal)).not.toBe(Plant.prototype);
expect(Object.getPrototypeOf(plant)).toBe(Plant.prototype);
expect(Object.getPrototypeOf(Object.getPrototypeOf(plant))).toBe(Plant.WrappedClass.prototype);
expect(Object.getPrototypeOf(plant)).not.toBe(Animal.prototype);

test('person is not an instance of Animal class', () => {
expect(Animal.isAnimal(diogo)).toBe(false);
expect(animal instanceof Animal).toBe(true);
expect(animal instanceof Animal.WrappedClass).toBe(true);
expect(animal instanceof Plant).toBe(false);
expect(plant instanceof Plant).toBe(true);
expect(plant instanceof Plant.WrappedClass).toBe(true);
expect(plant instanceof Animal).toBe(false);

expect(animal.getType()).toBe('mammal');
expect(plant.getType()).toBe('algae');
});

test('undefined/null is not an instance of any class', () => {
expect(Person.isPerson(undefined)).toBe(false);
expect(Person.isPerson(null)).toBe(false);
it('should have a custom toStringTag', () => {
expect(Object.prototype.toString.call(new Animal())).toBe('[object Animal]');
expect(Object.prototype.toString.call(new Plant())).toBe('[object Plant]');
});

test('check custom tag of class', () => {
expect(Object.prototype.toString.call(diogo)).toBe('[object Person]');
expect(Object.prototype.toString.call(wolf)).toBe('[object Animal]');
describe('is<className> method', () => {
it('should add a working is<className> static method', () => {
const animal = new Animal('mammal');
const plant = new Plant('algae');

expect(Animal.isAnimal(animal)).toBe(true);
expect(Animal.isAnimal(plant)).toBe(false);
expect(Plant.isPlant(plant)).toBe(true);
expect(Plant.isPlant(animal)).toBe(false);
});

it('should not crash if `null` or `undefined` is passed to is<ClassName>', () => {
expect(Animal.isAnimal(null)).toBe(false);
expect(Animal.isAnimal(undefined)).toBe(false);
});

it('should work correctly for deep inheritance scenarios', () => {
const mammal = new Mammal();
const algae = new Algae();

expect(Mammal.isMammal(mammal)).toBe(true);
expect(Animal.isAnimal(mammal)).toBe(true);
expect(Mammal.isMammal(algae)).toBe(false);
expect(Animal.isAnimal(algae)).toBe(false);

expect(Algae.isAlgae(algae)).toBe(true);
expect(Plant.isPlant(algae)).toBe(true);
expect(Algae.isAlgae(mammal)).toBe(false);
expect(Plant.isPlant(mammal)).toBe(false);
});
});

0 comments on commit e6b0c18

Please sign in to comment.