Comments (17)
@happycollision not bad, not bad at all... functions + destructuring definitely feels modern and is a great escape hatch.
export default Factory.extend({
title: 'Lorem ipsum',
traits: {
withAuthor() {
this.model.update('author', server.create('author'));
},
withComments(count) {
this.server.createList('comment', count, { post: this.model });
},
}
});
// usage
server.createList('post', 3, ({ traits }) => {
traits.withAuthor();
traits.withComments(3);
});
from discuss.
Or just a second argument to the extend function. First argument is an one that where you define all properties, and the second is one where you define all mirage-provided things. Looks basically the same as your preferred one except it has },{
in the middle.
from discuss.
@samselikoff The use of destructor seems unnecessary there. The callback can have just traits
and i
as parameters. But I like where it is going.
I don't like that traits
is part of the definition of the factory since the model can actually have a traits
property.
Another thing to consider is that the trait code might want to access the properties of the entity being created.
What do you think about the following propose?
// mirage/factories/post.js
export default Factory.extend({
title: 'Lorem ipsum',
published: true,
// Old syntax still works
asDraft: trait({
published: false,
}),
// New syntax, with parameters, but still familiar
withLikes: trait((likeCount) => ({
likeCount,
// Works like before
badge() {
return this.published && this.likeCount > 100 ? 'highlight' : null;
},
})),
withComments: trait((quantity = 10) => ({
afterCreate(post, server) {
server.createList('comment', quantity, { post });
},
})),
});
// usage example
server.create('post', {title: 'blah blah'}, post => // second argument `i` omitted
post.asDraft();
post.withLikes(42);
post.withComments(3);
);
// old syntax still works, thought arguments would be undefined if client doesn't set a default parameter in the factory
server.create('post', 'asDraft', 'withComments', {title: 'blah blah'});
We could also make the trait calling be chainable.
// chained traits
server.create('post', {title: 'blah blah'}, post => // second argument `i` omitted
post.asDraft()
.withLikes(42)
.withComments(3),
);
// single line
server.create('post', {title: 'blah blah'}, p => p.asDraft().withLikes(42).withComments(3));
I'm willing to work on this, if needed.
from discuss.
miragejs/ember-cli-mirage#1141
from discuss.
@Oreoz Wanted to kick off some brainstorming here on factory arguments.
// 1
// seems dumb
test('it works', function() {
server.create('post', ['withComments', { count: 3 }], 'published');
await visit('/');
// ...
});
// 2
// seems dumb
test('it works', function() {
server.create('post', { trait: 'withComments', args: { count: 3 }}, 'published');
await visit('/');
// ...
});
// 3
// little verbose but closer? I do feel like we need a function or chaining somewhere
test('it works', function() {
server.create('post', post => {
post.withComments(3).published();
});
await visit('/');
// ...
});
// 4
// this wouldn't really work because server.create('post') returns a model not a factory
test('it works', function() {
server.create('post').withComments(3).published();
await visit('/');
// ...
});
// 5
// could do something like this, new function that allows chaining and finish
// with a .value(). Kinda always hated that though.
test('it works', function() {
server.createWithTraits('post').withComments(3).published().value();
await visit('/');
// ...
});
// 6
// Could go to the FactoryBot transient attrs approach: https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md#transient-attributes
// Kinda crappy thing here is you dont know by looking a this that the
// commentsCount arg is for the withComments trait.
test('it works', function() {
server.create('post', 'published', 'withComments', { commentsCount: 3 });
await visit('/');
// ...
});
Anyone have other examples of compositional JS APIs from different libs? Chaining, arg list, what else?
from discuss.
cc @ryanto would also like your input given your experience with factory girl
from discuss.
Could also use function imports but come on this sucks
published(withComments(server.create('post'), 3));
Would be better if we had piping in js https://thoughtbot.com/blog/announcing-ex-machina
from discuss.
I'm personally not a fan of function chaining but out of the options laid out, 5 is the more appealing to me.
For 6, we could possibly add an API for wiring up args to specific traits inside of the factory itself. It doesn't solve the problem where it wouldn't be clear when you're consuming it which args goes to what trait, but it's a possibility and makes it so args could have more meaningful names.
That being said, I was thinking about this yesterday a little bit, and what kept coming back was making Trait into a class since it makes composing them so much more easier in the long run -- at least in my head.
// Perhaps the users that want args for traits would be willing to give up
// some of the readability that the current synthax gives us?
const traits = [
new Trait('withComments', { count: 3 }),
new Trait('withAuthor', { age: 29 }),
];
server.create('post', traits, { title: 'Hello Traits' });
// Major con (at least to me), really hard to read when you try to "one-line" it, but when you split it up, I kinda like it.
server.create('post', [
new Trait('withComments', { count: 3 }),
new Trait('withAuthor', { age: 29 }),
], { title: 'Hello Traits'});
// My other thought is that as a user, I'd want to keep this syntax as it's highly declarative and readable (when we don't need the args).
server.create('post', 'withComments', 'withAuthor', { title: 'Hello Traits' });
from discuss.
Here is an option where you allow traits to be specified in either the current syntax, or as an optional second to last object parameter where keys are trait names, values are trait transients. Its also not amazing but solves the ambiguity of which transient keys belong to which traits.
server.create('post', 'published',
{ withComments: { commentsCount: 3 }, withOtherThing: { thingProp: 2 }},
{ title: 'abc' }
);
With that said, the ambiguity inherit to option 6 has been livable in Rails imo.
from discuss.
Taking a quick crack at it.
server.create('post',
'withComments', // same as ever
(t) => t.withAuthor({name: 'Jane Doe'}), // if function, it gets a traits hash
{title: 'Functions as Arguments'}, // object merges into "post"
(t) => [t.liked(3), t.withCoAuthor()] // optionally return an array of traits
)
Without mixing trait invocation types, it is more readable
server.create('post', {title: 'Functions as Arguments'}, (t) => [
t.withComments(),
t.withAuthor({name: 'Jane Doe'}),
t.liked(3),
t.withCoAuthor(),
])
Alternatively, slightly more verbose, but allowing for future things that might be nice to pass into a function:
// the function could actually get an object containing a traits hash instead
server.create('post', ({trait, futureMirageIdea}) => [
trait.withComments({count: 3}),
trait.withAuthor(),
futureMirageIdea(true),
])
Either way, createList
could do some fun stuff with a possible second arg to the function...
server.createList('post', 3, ({trait}, i) => [
trait.withAuthor({name: ['Jack', 'Jill', 'Jane'][i]}),
trait.withComments({count: i * 3}),
])
from discuss.
Not bad not bad 👍
The destructuring buys us future API flexibility. For example our route handler api
this.get('/users', (schema, request) => {
})
sucks because if you want request
you have to add in schema
. Destructuring works nice here because if you just want the request you just destructure it:
this.get('/users', ({ request }) => {
})
and it makes it easier to inject additional APIs in the future. Same with traits, so we wouldn't want to do
server.create('post', {title: 'blah blah'}, traits => // second argument `i` omitted
traits.asDraft();
traits.withLikes(42);
traits.withComments(3);
);
because then if you want to get i
(or a future API) you have to go through traits
. Now if it's not traits
but a generic post
that's like the factory or something, and we can just throw things on it, not a huge deal I guess.
server.create('post', post =>
post.withComments(3);
);
server.create('post', (post, i) =>
post.withComments(i * 2);
);
// alternative
server.create('post', ({ withComments }) =>
withComments(2);
);
server.create('post', ({ withComments, i }) =>
withComments(i * 2);
);
// alternative 2
server.create('post', ({ traits, i }) =>
traits.withComments(i * 2);
);
Not sure which is most clear yet. But I think one of them is very close.
Another quick thing, with this one:
server.create('post', { title: 'blah blah' }, post =>
post.asDraft();
post.withLikes(42);
post.withComments(3);
);
it looks great but how do we not confuse users with post
here, to make sure they know it's a factory not a model?
As far as the trait authoring API goes, only problem with this part
withLikes: trait((likeCount) => ({
likeCount,
// Works like before
badge() {
return this.published && this.likeCount > 100 ? 'highlight' : null;
},
})),
is it might be a bit "uncanny" – it looks like an object but it's really a function returning an object. So when people want to do other logic here we get
withLikes: trait((likeCount) => {
let retweetCount = likeCount / 5
return {
likeCount,
retweetCount,
badge() {
return this.published && this.likeCount > 100 ? 'highlight' : null;
}
}
})
which feels like it might be a bit unnecessary, maybe it would be better to just have a function that lets folks mutate their model's state
withLikes: trait((likeCount) => {
let retweetCount = likeCount / 5
this.model.likeCount = likeCount
this.model.retweetCount = likeCount
this.model.badge = this.published && this.likeCount > 100 ? 'highlight' : null;
})
and of course this is starting to look familiar, and Mirage models have an update
method
withLikes: trait((likeCount) => {
let retweetCount = likeCount / 5
this.model.update({
likeCount: likeCount,
retweetCount: likeCount,
badge: this.published && this.likeCount > 100 ? 'highlight' : null;
})
})
from discuss.
The destructor makes much more sense to me now. Thanks for the clarification.
Now, regarding to
// 0
withLikes: trait((likeCount) => {
let retweetCount = likeCount / 5
this.model.update({
likeCount: likeCount,
retweetCount: likeCount,
badge: this.model.published && likeCount > 100 ? 'highlight' : null;
})
})
I believe this
would be the global context here (because arrow function), unless the client uses function
instead:
// 1
withLikes: trait(function(likeCount) {
this.model.update({
likeCount,
retweetCount: likeCount / 5,
badge: this.model.published && likeCount > 100 ? 'highlight' : null;
});
// this.server
})
An (a bit clunky) option if we want to keep the arrow function would be:
// 2
withLikes: trait(({model /*, server */}) => (likeCount) => {
model.update({
likeCount,
retweetCount: likeCount / 5,
badge: model.published && likeCount > 100 ? 'highlight' : null;
});
})
from discuss.
Good catch. Arrow thing sucks.
export default Factory.extend({
title: 'Lorem ipsum',
published: true,
// using function keyword, but subject to fat arrow pitfall
withLikes: trait(function(likeCount) {
this.model.update({
likeCount,
retweetCount: likeCount / 5,
badge: model.published && likeCount > 100 ? 'highlight' : null;
});
}),
// injecting model as first arg
withLikes: trait((model, [ likeCount ]) {
model.update({
likeCount,
retweetCount: likeCount / 5,
badge: model.published && likeCount > 100 ? 'highlight' : null;
});
}),
// using traits key
traits: {
withLikes(likeCount) {
this.model.update({
likeCount,
retweetCount: likeCount / 5,
badge: model.published && likeCount > 100 ? 'highlight' : null;
});
}
},
// ...but even with traits key, you can run into fat arrow pitfall
traits: {
withLikes: likeCount => {
this.model.update({ // ❌ won't work, this context is wrong
likeCount,
retweetCount: likeCount / 5,
badge: model.published && likeCount > 100 ? 'highlight' : null;
});
}
}
});
hmm...
from discuss.
Honestly I think I like this one the best
export default Factory.extend({
title: 'Lorem ipsum',
published: true,
traits: {
withLikes(likeCount) {
this.model.update({
likeCount,
retweetCount: likeCount / 5,
badge: model.published && likeCount > 100 ? 'highlight' : null;
});
}
}
});
but might be worth looking at ES6 class APIs and seeing what fits in best with the current landscape.
from discuss.
What happens if a model in my app has a relationship named traits
?
from discuss.
Maybe it should be named modelTraits
instead. (or factoryTraits
, creationTraits
, mirageTraits
, other?)
from discuss.
FYI: Transferred this to our Discuss repo, our new home for more open-ended conversations about Mirage!
If things become more concrete + actionable we can create a tracking issue in the main repo.
from discuss.
Related Issues (20)
- Add Middleware support to Mirage HOT 1
- Custom exception post-processing HOT 2
- Support multiple server instance. HOT 5
- Tracking issue for routing DSL / passthrough / namespace improvements
- Model hooks / default attributes HOT 5
- Passthrough external requests with wildcard HOT 4
- hasMany to hasMany (manyToMany) fixture associations not created HOT 1
- nativescript-vue with miragejs HOT 1
- Question: Expose mocked API as standalone on a specific port? HOT 3
- Be able to mock websocket calls HOT 2
- More array functions for models / collections HOT 8
- cannot use trait with afterCreate HOT 8
- routes for related resources HOT 2
- [Suggestion] Automatically parse request body based on content type HOT 2
- Feature Request: Generate API docs from test runs HOT 3
- How to only intercept request that defined in routes HOT 2
- server.createList() and server.create() creates duplicate values with dynamic factory definition
- Is it possible to use a combination of mock API's and actual API calls ? HOT 1
- Using a nested primary id for update and deletes
- Adding context object to handler to mock auth
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from discuss.