trailblazer/roar

parse_strategy feature should allow richer matching with existing linked or embedded resources

joshco opened this issue · 18 comments

I've got a model for People and Addresses. People have many addresses.
In my PersonRepresenter, I've got:

 collection :addresses, :class => Address, :extend => AddressRepresenter, 
 :embedded => true , :parse_strategy => :sync

I initially stumbled upon the sync thing because I noticed that if I did a POST of a person, but didn't include the _embedded address array, it would delete my existing address collection for a given person.

Note, I'm doing some matching on POSTs. If you do a post of a person and the server finds that a matching person already exists then it passes the existing person object to the consume! call in the person controller.

Is there a way I can:

  1. If a collection property is not present in the request to the server, then leave the collection alone. Dont get rid of it.
  2. control how parse_strategy does its matching? How does it know which record it is supposed to be updating?

Copy/Paste of Nick's response on roar-talk:

the new option parse_strategy: :sync is pretty dumb: Instead of automatically creating new Address instances for the :addresses collection, it uses the instances and syncs their properties.
However, this only works when the parsed document contains the exact same number of addresses than the object #from_json is being called onto.
What we need to do is provide some more "cleverer" strategies which find out which object is an existing object and which one should be created. These strategies could provide some standard semantics for handling PUTs and POSTs in classic Rails apps.

Should we try to identify the strategies in a github issue?

One valuable strategy could be where

  1. The server individually matches up representations in incoming request body for embedded resources with existing resources on the server
  2. The matching algorithm is likely to be specific to a given API in terms of what fields constitute a match or not. Therefore, the matching method is provided to roar as some sort of callback ( proc, labmda, inheritance etc)

Ignoring how the matching would work, I see those fundamental concepts for strategies:

  1. Ruthlessly synchronize the existing collection with the incoming. Breaks if they're not the same size. This is what :sync does.
  2. Create a new Address for each item in the incoming addresses collection and replace the latter with the new collection. This is kinda of a "stupid POST" and is what the current ::collection implementation does.
  3. Make the ::collection :addresses configurable so Roar knows if it should create a new Address or retrieve an existing from the data layer. Configuration could be a lambda or a strategy. This would fit both PUT and POST semantics.
collection :addresses, parse_strategy: [
  sync_with:  lambda {|doc| a = Address.find(doc[:id]) ? a : Address.new }]

That could be abstracted with a dedicated strategy.

Actually, now that I look at it, this works already if you use the :instance option.

That is encouraging!

But what is the :instance option? I don't recognize it in the documentation.

The :instance option is defined here.

I'll use representable's Album has_many Song model to sketch what we've to specify.

These 2 examples illustrate a POST or PUT to albums/best-of-police/.

(UPDATE: What I wanna point out is: it's quite simple to implement the "model-syncing" in representable where representable will retrieve real object instances from the DB. However, we have to speak about different semantics when processing collections.)

Adding/Replacing

songs: [
  {id: 1, title: "Roxanne"},
  {id: 2, title: "So Lonely"}
]
  1. Add those songs to album.
  2. Replace songs with new list ("Roxanne" and "So Lonely")

Adding New

songs: [
  {id: 1, title: "Roxanne"},
  {title: "Fallout"}
]
  1. Create "Fallout", Find "Roxanne"
  2. Add/Replace songs

Skipping/Deleting

songs: []
  1. Leave songs alone, don't invoke album.songs=,
  2. Delete all songs (we need this semantic as well).

After editing the last comment 3 times I finally understood what we want! It's two different things.

1. Process Incoming Collection

songs: [
  {id: 1, title: "Roxanne"},
  {title: "Fallout"}
]

Representable will parse this collection and sort out which item to retrieve from the DB (probably by checking the id: field) and which to create.

2. Sync Collection

After the incoming collection is mapped to real objects, the user actually has to decide if he wants to add those objects, update the collection, update existing objects, only, etc.

That looks pretty good!

In the case of replacement, it would be nice to decide what to do with the replaced document. In some cases you might want to keep the resource available for others (e.g. removing a user from your twitter's "following" list) and in some cases you might want to delete the resource entirely (e.g. removing a tweet ... NSA notwithstanding). Perhaps a callback would work there?

Hi guys, just interesting is it any progress on this?

thanks for your work!

Yeah, totally! This goes into disposable which will take care of abstracting model-specific operations to the form/representer layer. What are you trying to achieve, @faust45 ?

@apotonick thanks for feedback

i have json collections on client side
and i need perform operations delete/create/update on client side
then sync with server (Rails app)

i looking for some solution which allow me sync in simple way

what you mean "goes into disposable"?

Create and update already works with representable, only delete needs to be implemented.

Are we talking about collections or singular properties here?

Haha, disposable is a gem I am working on to extract all the "real" database behaviour from representable/roar. Never mind, you won't need to know about it if you're using Roar.

Ok, some more explanations. Representable/Roar/Reform's job is transforming data to and from Ruby objects and not to create, find or delete ActiveRecord models. This belongs to another layer, which will be provided in disposable.

Parse strategies got superseded by populators which are way more intuitive. http://trailblazer.to/gems/representable/3.0/populator.html

that link doesnt work

I know, still writing it! 😉

hurry up! :)