Skip to content

Commit e6b0c18

Browse files
committed
feat: add withoutNew option
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.
1 parent 9df979b commit e6b0c18

17 files changed

+442
-83
lines changed

README.md

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@ class Person {
5050
}
5151
}
5252

53-
module.exports = withIs(Person, { className: 'Person', symbolName: '@org/package-x/person' });
53+
module.exports = withIs(Person, {
54+
className: 'Person',
55+
symbolName: '@org/package-x/Person',
56+
});
5457
```
5558

5659
```js
@@ -63,7 +66,10 @@ class Animal {
6366
}
6467
}
6568

66-
module.exports = withIs(Animal, { className: 'Animal', symbolName: '@org/package-y/animal' });
69+
module.exports = withIs(Animal, {
70+
className: 'Animal',
71+
symbolName: '@org/package-y/Animal',
72+
});
6773
```
6874

6975
```js
@@ -93,6 +99,8 @@ function Circle(radius) {
9399
if (!(this instanceof Circle)) {
94100
return new Circle();
95101
}
102+
103+
this.radius = radius;
96104
}
97105
```
98106

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

104-
const Circle = withIs.proto(function () {
112+
const Circle = withIs.proto(function (radius) {
105113
if (!(this instanceof Circle)) {
106114
return new Circle();
107115
}
108-
}, { className: 'Circle', symbolName: '@org/package/circle' });
109-
110-
const circle = Circle();
111116

112-
console.log(Circle.isCircle(circle));
117+
this.radius = radius;
118+
}, {
119+
className: 'Circle',
120+
symbolName: '@org/package/Circle',
121+
});
113122
```
114123

115-
The example above will print:
116-
```
117-
true
124+
...or even better:
125+
126+
```js
127+
const withIs = require('class-is');
128+
129+
function Circle(radius) {
130+
this.radius = radius;
131+
}
132+
133+
module.exports = withIs.proto(Circle, {
134+
className: 'Circle',
135+
symbolName: '@org/package/Circle',
136+
withoutNew: true,
137+
});
118138
```
119139

120140

121141
## API
122142

123-
### withIs(Class, { className: name, symbolName: symbol })
143+
### withIs(Class, { className, symbolName })
124144

125145
###### class
126146

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

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

143-
Example: `@organization/package/class`
163+
Example: `@organization/package/Class`
164+
165+
### withIs.proto(Class, { className, symbolName, withoutNew })
166+
167+
The `className` and `symbolName` parameters are the same as above.
168+
169+
###### withoutNew
144170

145-
### withIs.proto(Class, { className: name, symbolName: symbol })
171+
Type: `Boolean`
172+
Default: `false`
146173

147-
Apply the same parameters as above.
174+
Allow creating an instance without the `new` operator.
148175

149176

150177
## Tests

index.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,16 @@ function withIs(Class, { className, symbolName }) {
2727
return ClassIsWrapper;
2828
}
2929

30-
function withIsProto(Class, { className, symbolName }) {
30+
function withIsProto(Class, { className, symbolName, withoutNew }) {
3131
const symbol = Symbol.for(symbolName);
3232

33+
/* eslint-disable object-shorthand */
3334
const ClassIsWrapper = {
34-
/* eslint-disable object-shorthand */
3535
[className]: function (...args) {
36+
if (withoutNew && !(this instanceof ClassIsWrapper)) {
37+
return new ClassIsWrapper(...args);
38+
}
39+
3640
const _this = Class.call(this, ...args) || this;
3741

3842
if (_this && !_this[symbol]) {
@@ -42,6 +46,10 @@ function withIsProto(Class, { className, symbolName }) {
4246
return _this;
4347
},
4448
}[className];
49+
/* eslint-enable object-shorthand */
50+
51+
ClassIsWrapper.prototype = Object.create(Class.prototype);
52+
ClassIsWrapper.prototype.constructor = ClassIsWrapper;
4553

4654
Object.defineProperty(ClassIsWrapper.prototype, Symbol.toStringTag, {
4755
get() {

test/es5.test.js

Lines changed: 110 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,127 @@
11
'use strict';
22

3-
const withIs = require('..');
3+
const {
4+
Animal,
5+
Plant,
6+
Mammal,
7+
Algae,
48

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

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

17-
const circle = new Circle();
18-
const square = new Square();
18+
expect(Object.getPrototypeOf(animal)).toBe(Animal.prototype);
19+
expect(Object.getPrototypeOf(Object.getPrototypeOf(animal))).toBe(Animal.WrappedClass.prototype);
20+
expect(Object.getPrototypeOf(animal)).not.toBe(Plant.prototype);
21+
expect(Object.getPrototypeOf(plant)).toBe(Plant.prototype);
22+
expect(Object.getPrototypeOf(Object.getPrototypeOf(plant))).toBe(Plant.WrappedClass.prototype);
23+
expect(Object.getPrototypeOf(plant)).not.toBe(Animal.prototype);
1924

20-
test('circle is an instance of Circle class', () => {
21-
expect(Circle.isCircle(circle)).toBe(true);
22-
});
25+
expect(animal instanceof Animal).toBe(true);
26+
expect(animal instanceof Animal.WrappedClass).toBe(true);
27+
expect(animal instanceof Plant).toBe(false);
28+
expect(plant instanceof Plant).toBe(true);
29+
expect(plant instanceof Plant.WrappedClass).toBe(true);
30+
expect(plant instanceof Animal).toBe(false);
2331

24-
test('square is not an instance of Circle class', () => {
25-
expect(Circle.isCircle(square)).toBe(false);
32+
expect(animal.getType()).toBe('mammal');
33+
expect(plant.getType()).toBe('algae');
2634
});
2735

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

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

36-
test('calling without new', () => {
37-
const circle = Circle(); // eslint-disable-line new-cap
46+
expect(Animal.isAnimal(animal)).toBe(true);
47+
expect(Animal.isAnimal(plant)).toBe(false);
48+
expect(Plant.isPlant(plant)).toBe(true);
49+
expect(Plant.isPlant(animal)).toBe(false);
50+
});
3851

39-
expect(Circle.isCircle(circle)).toBe(true);
40-
});
52+
it('should not crash if `null` or `undefined` is passed to is<ClassName>', () => {
53+
expect(Animal.isAnimal(null)).toBe(false);
54+
expect(Animal.isAnimal(undefined)).toBe(false);
55+
});
56+
57+
it('should work correctly for deep inheritance scenarios', () => {
58+
const mammal = new Mammal();
59+
const algae = new Algae();
4160

42-
test('undefined/null is not an instance of any class', () => {
43-
expect(Circle.isCircle(undefined)).toBe(false);
44-
expect(Circle.isCircle(null)).toBe(false);
61+
expect(Mammal.isMammal(mammal)).toBe(true);
62+
expect(Animal.isAnimal(mammal)).toBe(true);
63+
expect(Mammal.isMammal(algae)).toBe(false);
64+
expect(Animal.isAnimal(algae)).toBe(false);
65+
66+
expect(Algae.isAlgae(algae)).toBe(true);
67+
expect(Plant.isPlant(algae)).toBe(true);
68+
expect(Algae.isAlgae(mammal)).toBe(false);
69+
expect(Plant.isPlant(mammal)).toBe(false);
70+
});
4571
});
4672

47-
test('check custom tag of Square class', () => {
48-
expect(Object.prototype.toString.call(square)).toBe('[object Square]');
73+
describe('new operator', () => {
74+
it('should work on explicit without-new handling', () => {
75+
const instance = new ExplicitWithoutNew();
76+
const instance2 = ExplicitWithoutNew(); // eslint-disable-line new-cap
77+
78+
expect(Object.getPrototypeOf(instance)).toBe(ExplicitWithoutNew.prototype);
79+
expect(Object.getPrototypeOf(Object.getPrototypeOf(instance))).toBe(ExplicitWithoutNew.WrappedClass.prototype);
80+
expect(Object.getPrototypeOf(instance2)).toBe(ExplicitWithoutNew.prototype);
81+
expect(Object.getPrototypeOf(Object.getPrototypeOf(instance2))).toBe(ExplicitWithoutNew.WrappedClass.prototype);
82+
83+
expect(instance instanceof ExplicitWithoutNew).toBe(true);
84+
expect(instance instanceof ExplicitWithoutNew.WrappedClass).toBe(true);
85+
expect(instance2 instanceof ExplicitWithoutNew).toBe(true);
86+
expect(instance2 instanceof ExplicitWithoutNew.WrappedClass).toBe(true);
87+
88+
expect(instance.getLabel()).toBe('ExplicitWithoutNew');
89+
expect(instance2.getLabel()).toBe('ExplicitWithoutNew');
90+
});
91+
92+
it('should work on implicit without-new handling', () => {
93+
const instance = new ImplicitWithoutNew();
94+
const instanceNoNew = ImplicitWithoutNew(); // eslint-disable-line new-cap
95+
96+
expect(Object.getPrototypeOf(instance)).toBe(ImplicitWithoutNew.prototype);
97+
expect(Object.getPrototypeOf(Object.getPrototypeOf(instance))).toBe(ImplicitWithoutNew.WrappedClass.prototype);
98+
expect(Object.getPrototypeOf(instanceNoNew)).toBe(ImplicitWithoutNew.prototype);
99+
expect(Object.getPrototypeOf(Object.getPrototypeOf(instanceNoNew))).toBe(ImplicitWithoutNew.WrappedClass.prototype);
100+
101+
expect(instance instanceof ImplicitWithoutNew).toBe(true);
102+
expect(instance instanceof ImplicitWithoutNew.WrappedClass).toBe(true);
103+
expect(instanceNoNew instanceof ImplicitWithoutNew).toBe(true);
104+
expect(instanceNoNew instanceof ImplicitWithoutNew.WrappedClass).toBe(true);
105+
106+
expect(instance.getLabel()).toBe('ImplicitWithoutNew');
107+
expect(instanceNoNew.getLabel()).toBe('ImplicitWithoutNew');
108+
});
109+
110+
it('should work on explicit & implicit without-new handling', () => {
111+
const instance = new ImplicitExplicitWithoutNew();
112+
const instanceNoNew = ImplicitExplicitWithoutNew(); // eslint-disable-line new-cap
113+
114+
expect(Object.getPrototypeOf(instance)).toBe(ImplicitExplicitWithoutNew.prototype);
115+
expect(Object.getPrototypeOf(Object.getPrototypeOf(instance))).toBe(ImplicitExplicitWithoutNew.WrappedClass.prototype);
116+
expect(Object.getPrototypeOf(instanceNoNew)).toBe(ImplicitExplicitWithoutNew.prototype);
117+
expect(Object.getPrototypeOf(Object.getPrototypeOf(instanceNoNew))).toBe(ImplicitExplicitWithoutNew.WrappedClass.prototype);
118+
119+
expect(instance instanceof ImplicitExplicitWithoutNew).toBe(true);
120+
expect(instance instanceof ImplicitExplicitWithoutNew.WrappedClass).toBe(true);
121+
expect(instanceNoNew instanceof ImplicitExplicitWithoutNew).toBe(true);
122+
expect(instanceNoNew instanceof ImplicitExplicitWithoutNew.WrappedClass).toBe(true);
123+
124+
expect(instance.getLabel()).toBe('ImplicitExplicitWithoutNew');
125+
expect(instanceNoNew.getLabel()).toBe('ImplicitExplicitWithoutNew');
126+
});
49127
});

test/es6.test.js

Lines changed: 57 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,67 @@
11
'use strict';
22

3-
const withIs = require('..');
4-
5-
const Person = withIs(class {
6-
constructor(name, city) {
7-
this.name = name;
8-
this.city = city;
9-
}
10-
}, { className: 'Person', symbolName: '@org/package-x/person' });
11-
12-
const Animal = withIs(class {
13-
constructor(species) {
14-
this.species = species;
15-
}
16-
}, { className: 'Animal', symbolName: '@org/package-y/person' });
17-
18-
const diogo = new Person('Diogo', 'Porto');
19-
const wolf = new Animal('Wolf');
20-
21-
test('person is an instance of Person class', () => {
22-
expect(Person.isPerson(diogo)).toBe(true);
23-
});
3+
const {
4+
Animal,
5+
Plant,
6+
Mammal,
7+
Algae,
8+
} = require('./fixtures/es6');
249

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

29-
test('wolf is an instance of Animal class', () => {
30-
expect(Animal.isAnimal(wolf)).toBe(true);
31-
});
14+
expect(Object.getPrototypeOf(animal)).toBe(Animal.prototype);
15+
expect(Object.getPrototypeOf(Object.getPrototypeOf(animal))).toBe(Animal.WrappedClass.prototype);
16+
expect(Object.getPrototypeOf(animal)).not.toBe(Plant.prototype);
17+
expect(Object.getPrototypeOf(plant)).toBe(Plant.prototype);
18+
expect(Object.getPrototypeOf(Object.getPrototypeOf(plant))).toBe(Plant.WrappedClass.prototype);
19+
expect(Object.getPrototypeOf(plant)).not.toBe(Animal.prototype);
3220

33-
test('person is not an instance of Animal class', () => {
34-
expect(Animal.isAnimal(diogo)).toBe(false);
21+
expect(animal instanceof Animal).toBe(true);
22+
expect(animal instanceof Animal.WrappedClass).toBe(true);
23+
expect(animal instanceof Plant).toBe(false);
24+
expect(plant instanceof Plant).toBe(true);
25+
expect(plant instanceof Plant.WrappedClass).toBe(true);
26+
expect(plant instanceof Animal).toBe(false);
27+
28+
expect(animal.getType()).toBe('mammal');
29+
expect(plant.getType()).toBe('algae');
3530
});
3631

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

42-
test('check custom tag of class', () => {
43-
expect(Object.prototype.toString.call(diogo)).toBe('[object Person]');
44-
expect(Object.prototype.toString.call(wolf)).toBe('[object Animal]');
37+
describe('is<className> method', () => {
38+
it('should add a working is<className> static method', () => {
39+
const animal = new Animal('mammal');
40+
const plant = new Plant('algae');
41+
42+
expect(Animal.isAnimal(animal)).toBe(true);
43+
expect(Animal.isAnimal(plant)).toBe(false);
44+
expect(Plant.isPlant(plant)).toBe(true);
45+
expect(Plant.isPlant(animal)).toBe(false);
46+
});
47+
48+
it('should not crash if `null` or `undefined` is passed to is<ClassName>', () => {
49+
expect(Animal.isAnimal(null)).toBe(false);
50+
expect(Animal.isAnimal(undefined)).toBe(false);
51+
});
52+
53+
it('should work correctly for deep inheritance scenarios', () => {
54+
const mammal = new Mammal();
55+
const algae = new Algae();
56+
57+
expect(Mammal.isMammal(mammal)).toBe(true);
58+
expect(Animal.isAnimal(mammal)).toBe(true);
59+
expect(Mammal.isMammal(algae)).toBe(false);
60+
expect(Animal.isAnimal(algae)).toBe(false);
61+
62+
expect(Algae.isAlgae(algae)).toBe(true);
63+
expect(Plant.isPlant(algae)).toBe(true);
64+
expect(Algae.isAlgae(mammal)).toBe(false);
65+
expect(Plant.isPlant(mammal)).toBe(false);
66+
});
4567
});

0 commit comments

Comments
 (0)