Skip to content

Commit

Permalink
Allow building types with .from using an object (#252)
Browse files Browse the repository at this point in the history
With `.from`, you can be explicit about which fields you want to
populate, without having to deal with the positional arguments
of the normal builder.

See also:
facebook/jscodeshift#180 (comment)
  • Loading branch information
elliottsj authored and benjamn committed Mar 9, 2018
1 parent 455b1b2 commit ea80bd2
Show file tree
Hide file tree
Showing 2 changed files with 149 additions and 61 deletions.
160 changes: 99 additions & 61 deletions lib/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -533,77 +533,115 @@ module.exports = function () {
// Override Dp.buildable for this Def instance.
Object.defineProperty(self, "buildable", {value: true});

Object.defineProperty(builders, getBuilderName(self.typeName), {
enumerable: true,
function addParam(built, param, arg, isArgAvailable) {
if (hasOwn.call(built, param))
return;

value: function () {
var args = arguments;
var argc = args.length;
var built = Object.create(nodePrototype);
var all = self.allFields;
if (!hasOwn.call(all, param)) {
throw new Error("" + param);
}

if (!self.finalized) {
throw new Error(
"attempting to instantiate unfinalized type " +
self.typeName
);
}
var field = all[param];
var type = field.type;
var value;

if (isArgAvailable) {
value = arg;
} else if (field.defaultFn) {
// Expose the partially-built object to the default
// function as its `this` object.
value = field.defaultFn.call(built);
} else {
var message = "no value or default function given for field " +
JSON.stringify(param) + " of " + self.typeName + "(" +
self.buildParams.map(function (name) {
return all[name];
}).join(", ") + ")";
throw new Error(message);
}

if (!type.check(value)) {
throw new Error(
shallowStringify(value) +
" does not match field " + field +
" of type " + self.typeName
);
}

function add(param, i) {
if (hasOwn.call(built, param))
return;

var all = self.allFields;
if (!hasOwn.call(all, param)) {
throw new Error("" + param);
}

var field = all[param];
var type = field.type;
var value;

if (isNumber.check(i) && i < argc) {
value = args[i];
} else if (field.defaultFn) {
// Expose the partially-built object to the default
// function as its `this` object.
value = field.defaultFn.call(built);
} else {
var message = "no value or default function given for field " +
JSON.stringify(param) + " of " + self.typeName + "(" +
self.buildParams.map(function (name) {
return all[name];
}).join(", ") + ")";
throw new Error(message);
}

if (!type.check(value)) {
throw new Error(
shallowStringify(value) +
" does not match field " + field +
" of type " + self.typeName
);
}

// TODO Could attach getters and setters here to enforce
// dynamic type safety.
built[param] = value;
built[param] = value;
}

// Calling the builder function will construct an instance of the Def,
// with positional arguments mapped to the fields original passed to .build.
// If not enough arguments are provided, the default value for the remaining fields
// will be used.
function builder() {
var args = arguments;
var argc = args.length;

if (!self.finalized) {
throw new Error(
"attempting to instantiate unfinalized type " +
self.typeName
);
}

var built = Object.create(nodePrototype);

self.buildParams.forEach(function (param, i) {
if (i < argc) {
addParam(built, param, args[i], true)
} else {
addParam(built, param, null, false);
}
});

self.buildParams.forEach(function (param, i) {
add(param, i);
});
Object.keys(self.allFields).forEach(function (param) {
// Use the default value.
addParam(built, param, null, false);
});

Object.keys(self.allFields).forEach(function (param) {
add(param); // Use the default value.
});
// Make sure that the "type" field was filled automatically.
if (built.type !== self.typeName) {
throw new Error("");
}

// Make sure that the "type" field was filled automatically.
if (built.type !== self.typeName) {
throw new Error("");
return built;
}

// Calling .from on the builder function will construct an instance of the Def,
// using field values from the passed object. For fields missing from the passed object,
// their default value will be used.
builder.from = function (obj) {
if (!self.finalized) {
throw new Error(
"attempting to instantiate unfinalized type " +
self.typeName
);
}

var built = Object.create(nodePrototype);

Object.keys(self.allFields).forEach(function (param) {
if (hasOwn.call(obj, param)) {
addParam(built, param, obj[param], true);
} else {
addParam(built, param, null, false);
}
});

return built;
// Make sure that the "type" field was filled automatically.
if (built.type !== self.typeName) {
throw new Error("");
}

return built;
}

Object.defineProperty(builders, getBuilderName(self.typeName), {
enumerable: true,
value: builder
});

return self; // For chaining.
Expand Down
50 changes: 50 additions & 0 deletions test/ecmascript.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,56 @@ describe("basic type checking", function() {
});
});

describe("builders", function() {
it("should build types using positional arguments", function() {
var fooId = b.identifier("foo");
var consequent = b.blockStatement([
b.expressionStatement(b.callExpression(fooId, []))
]);
var ifFoo = b.ifStatement(fooId, consequent);

assert.ok(n.Identifier.check(fooId));
assert.ok(n.IfStatement.check(ifFoo));
assert.ok(n.Statement.check(ifFoo));

assert.strictEqual(fooId.name, "foo");
assert.strictEqual(fooId.optional, false);

assert.strictEqual(ifFoo.test, fooId);
assert.strictEqual(ifFoo.consequent, consequent);
assert.strictEqual(ifFoo.alternate, null);
});

it("should build types using `.from`", function() {
var fooId = b.identifier.from({
name: "foo",
optional: true
});
var consequent = b.blockStatement.from({
body: [
b.expressionStatement.from({
expression: b.callExpression.from({ callee: fooId, arguments: [] })
})
]
});
var ifFoo = b.ifStatement.from({
test: fooId,
consequent: consequent
});

assert.ok(n.Identifier.check(fooId));
assert.ok(n.IfStatement.check(ifFoo));
assert.ok(n.Statement.check(ifFoo));

assert.strictEqual(fooId.name, "foo");
assert.strictEqual(fooId.optional, true);

assert.strictEqual(ifFoo.test, fooId);
assert.strictEqual(ifFoo.consequent, consequent);
assert.strictEqual(ifFoo.alternate, null);
});
});

describe("isSupertypeOf", function() {
it("should report correct supertype relationships", function() {
var def = types.Type.def;
Expand Down

0 comments on commit ea80bd2

Please sign in to comment.