Code Monkey home page Code Monkey logo

Comments (17)

samselikoff avatar samselikoff commented on May 28, 2024 3

@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.

happycollision avatar happycollision commented on May 28, 2024 2

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.

leonardoraele avatar leonardoraele commented on May 28, 2024 1

@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.

samselikoff avatar samselikoff commented on May 28, 2024

miragejs/ember-cli-mirage#1141

from discuss.

samselikoff avatar samselikoff commented on May 28, 2024

@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.

samselikoff avatar samselikoff commented on May 28, 2024

cc @ryanto would also like your input given your experience with factory girl

from discuss.

samselikoff avatar samselikoff commented on May 28, 2024

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.

Oreoz avatar Oreoz commented on May 28, 2024

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.

chbonser avatar chbonser commented on May 28, 2024

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.

happycollision avatar happycollision commented on May 28, 2024

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.

samselikoff avatar samselikoff commented on May 28, 2024

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.

leonardoraele avatar leonardoraele commented on May 28, 2024

@samselikoff

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.

samselikoff avatar samselikoff commented on May 28, 2024

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.

samselikoff avatar samselikoff commented on May 28, 2024

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.

leonardoraele avatar leonardoraele commented on May 28, 2024

What happens if a model in my app has a relationship named traits?

from discuss.

leonardoraele avatar leonardoraele commented on May 28, 2024

Maybe it should be named modelTraits instead. (or factoryTraits, creationTraits, mirageTraits, other?)

from discuss.

samselikoff avatar samselikoff commented on May 28, 2024

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)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.