Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Creating arbitrary hash on lonely collection and encapsulating collection items #111

Open
konung opened this issue Oct 14, 2014 · 17 comments

Comments

@konung
Copy link

konung commented Oct 14, 2014

Hello.

Trying to figure out how to send some arbitrary data on a lonely collection. I tried every which way and it's either overwriting items or just get ignored

Have something like

# representers/product_representer.rb
class ProductRepresenter  < Roar::Decorator
  include Roar::Representer::JSON

  property :id
  property :name
  property :created_at
  property :updated_at
end

# representers/products_representer.rb
class ProductsRepresenter < Roar::Decorator
  include Representable::JSON::Collection

  items extend: ProductRepresenter, class: Product
end

This works find and produces basic output when I make call to /products.json

However I would like to add some arbitrary attributes to the collection

so right now I get

[{
"id": 51,
"name": "product 1",
"created_at": "2008-09-15T09:37:12.000-05:00",
"updated_at": "2013-12-12T17:04:28.000-06:00",
},
{
"id": 52,
"name": "product 2",
"created_at": "2008-09-15T09:37:12.000-05:00",
"updated_at": "2013-12-12T17:28:45.000-06:00",
},
.....
]

I'd like to be able to spit out something like this separate items (products) and add meta attributes:

{
"meta" : {
"ordered_by": "asc",
"request_made_at" : "",
"total_records" : "4000"
},
"products" : [
{
"id": 51,
"name": "product 1",
"created_at": "2008-09-15T09:37:12.000-05:00",
"updated_at": "2013-12-12T17:04:28.000-06:00",
},
{
"id": 52,
"name": "product 2",
"created_at": "2008-09-15T09:37:12.000-05:00",
"updated_at": "2013-12-12T17:28:45.000-06:00",
},
.....]
}

These are just some examples. The idea being that the API responses I'm writing require some extensive meta data included. Right now I'm doing that just using plain old Rails json responses and templates, but would like to switch to roar.

@konung
Copy link
Author

konung commented Oct 14, 2014

Just to give a better idea here is a response from my current app that I'm trying to match. ( sensitive data taken out)

{
"status": "success",
"message": "Product(s) found",
"count": 1,
"products": [
{
"id": 108527,
"manufacturer_id": 2,
"manufacturer_number": null,
"masterfile_details": {},
"minimum_order_quantity": null,
"retail_sale_amount_cents": 1231,
"stats": {},
"status_id": 1,
"total_stock": 1,
"updated_at": "2014-10-13T20:33:58-05:00",
"created_at": "2012-02-21T20:26:19-06:00",
}
],
"params": {
"q": {
"id_eq": "108527"
},
"action": "search",
"controller": "products",
"format": "json"
}
}

@apotonick
Copy link
Member

Nick, hey! Your "problem" is you want to render a full-blown document and not just a collection. This is why you should think "in that document" and not in collections! 😉

What you have to do is use an appropriate representer composition.

class ProductsRepresenter  < Roar::Decorator
  property :status
  property :count
  collection :products, extend: ProductRepresenter, class: Product
  # and so on
end

The object you pass to the representer has to expose the correct API.

OpenStruct.new(status: :ok, count: products.size, products: products)

There's several tricks to simplify this but start playing with it and let me know if it works.

@konung
Copy link
Author

konung commented Oct 14, 2014

@apotonick Thanks Nick (hey!)

So I tried you suggestios, but I'm a bit confused as to how I would write my respond_with block in the contoller. Here is what I came up with - but I get "null" as my response

class ProductsController < ApplicationController
  before_action :set_product, only: [:show, :edit, :update, :destroy]
  include Roar::Rails::ControllerAdditions::Render

  def index
    @q = Product.ransack(params[:q])
    @products = @q.result().order(params[:order]).page(params[:page]).per(params[:per])

    respond_with(@products) do |format|
      format.html {render}
      format.json {render json: OpenStruct.new(products: @products, count: @products.total_count), represent_with: ProductsRepresenter}
    end
  end

end

@konung
Copy link
Author

konung commented Oct 14, 2014

If I do this in console:

reload!; 
ps = Product.page(5);
 r = ProductsRepresenter.new(OpenStruct.new(products: ps, count: ps.total_count));
 r.represented

I can see both attributes (count = 30000, and products containing ActiveRecord::Relation, but for some reason roar doesn't see them )

@apotonick
Copy link
Member

This is because render + :represent_with doesn't work! Rails uses it's built-in #to_json and that is wrong, of course. Let me check out how render works in that context. BTW - this is roar-rails, not roar core! 😮

@konung
Copy link
Author

konung commented Oct 14, 2014

Sorry it started as a Roar question, turned into Roar-Rails questions

It's getting late for me here. But just for the heck of it I re implemented it just using representable directly. It does exactly what I need in the format that I need . For now I don't need Hypermedia and other Roar features. For posterity here is what I have. ( If you happen to come up with a solution to my rails problem - I'd be happy to try it out)

class ProductsRepresentation < Representable::Decorator
  include Representable::JSON

  property :count
  collection :products, class: Product, decorator: ProductRepresentation
end

in Rails calling it just like PORO

class ProductsController < ApplicationController
  before_action :set_product, only: [:show, :edit, :update, :destroy]

  def index
    @q = Product.ransack(params[:q])
    @products = @q.result().order(params[:order]).page(params[:page]).per(params[:per])

    respond_with(@products) do |format|
      format.html {render}
      format.json {render json:  ProductsRepresentation.new(OpenStruct.new(products: @products, count: @products.total_count))}
    end
  end

@konung
Copy link
Author

konung commented Oct 14, 2014

One more question you might know how it's done since you dealt with both representable and roar and rails. At this point my brain is fried.

Here is what I have now:

class ProductRepresentation < Representable::Decorator
  include Representable::JSON

  property :id
  property :name
  property :category do
    property :id
    property :name
    property :code
  end
  #......
end

I'd like to use category representer instead of inline. I can't seem to get it right:

class CategoryRepresentation < Representable::Decorator
  include Representable::JSON

  property :id
  property :name
  property :code
  property :description
  property :type
  property :cateogry_id, as: :parent_id
end


class ProductRepresentation < Representable::Decorator
  include Representable::JSON

  property :id
  property :name
  property :category_id # <--------  RIGHT HERE, how can I pass this properly to the CategoryRepresentaion?
  #......
end

category_id is a belong_to style association

THANK YOU!

@apotonick
Copy link
Member

Right now, this needs to be supported on the model layer.

category.category_id #=> 1

You could pass the product model into the to_json call and then use :getter.

class CategoryRepresenter < ..
  property :category_id, getter: lambda {|options| options[:product].category_id }

Then, invoke all that as follows.

ProductRepresenter.prepare(product).to_json(product: product)

I want to make this easier in future versions, though, but give it a go!

@ichthala
Copy link

I just wrestled with a similar issue for a while. I had a representer like this:

class EmailAddressCollectionRepresenter < Roar::Decorator
  include Roar::Representer::JSON::HAL
  include Representable::JSON::Collection

  self.representation_wrap = :email_addresses

  items decorator: EmailAddressRepresenter, class: Channel

  link :self do |opts|
    "#{opts[:path]}/email_addresses"
  end

end

...and I was extremely puzzled as to why the link was being omitted when the resource was accessed. It's pretty confusing that collection allows extra properties at the same level as the collection's namespace, but items doesn't. It really seems to me like this should be supported, but even if this isn't something you want to support, it would be helpful to make it clearer in the documentation. (Maybe in the documentation for Representable rather than Roar.)

@apotonick
Copy link
Member

The link is omitted because you're using a "lonely collection", which solely consists of items in a collection. The whole point of a lonely collection is that there's no other elements in it.

@ichthala, why don't you use a normal representer? Try the following

class EmailAddressCollectionRepresenter < Roar::Decorator
  include Roar::Representer::JSON::HAL
  include Representable::JSON::Collection # NO!!! Don't include this - see next comment!

  collection: :email_addresses, decorator: EmailAddressRepresenter, class: Channel

  link :self do |opts|
    "#{opts[:path]}/email_addresses"
  end

  def email_addresses
    represented
  end
end

@ichthala
Copy link

Thanks for the help! That almost worked, but I had to do this:

class EmailAddressCollectionRepresenter < Roar::Decorator
  include Roar::Representer::JSON::HAL
  # include Representable::JSON::Collection

  collection: :email_addresses, decorator: EmailAddressRepresenter, class: Channel, exec_context: :decorator

  link :self do |opts|
    "#{opts[:path]}/email_addresses"
  end

  def email_addresses
    represented
  end
end

It looks like if you include Representable::JSON::Collection, it will omit any other properties besides the collection. Is that accurate?

@apotonick
Copy link
Member

Yeah, you're right! I'm an idiot, I was telling you not to use Collection and then left it in the code snippet... 😜

@ichthala
Copy link

Haha, alright, just making sure. But I'm confused -- is that module only for lonely collections? If so perhaps it could be renamed?

On Apr 29, 2015, at 6:29 PM, Nick Sutterer notifications@github.com wrote:

Yeah, you're right! I'm an idiot, I was telling you not to use Collection and then left it in the code snippet...


Reply to this email directly or view it on GitHub.

@apotonick
Copy link
Member

This module, yes, is only for lonely collections, and, no, I'm not gonna rename it, as it is well documented here: https://github.com/apotonick/representable/#lonely-collections 😝

Using the Collection module in arbitrary representers is not mentioned anywhere, what made you think this is how it works, if I may ask?

@apotonick apotonick reopened this Apr 30, 2015
@ichthala
Copy link

I didn't think that, I was including the module specifically to use items. Ultimately, I wanted to pass in an array of objects and have them be represented without having to wrap the array in another object just for the representer. items provided that pattern, and I was pleased, so I started building off of it and then went astray with the namespacing and links. It wasn't obvious (though in retrospect it should have been) that I could use collection and still not have to wrap the array. ...I hope that makes sense/answers your question. Thanks again for all the clarifications. ^_^

@apotonick
Copy link
Member

Maybe we should note in the README that as soon as you want "more" (e.g. hypermedia) in a lonely collection, you should switch to a non-lonely representer?

@ichthala
Copy link

Yeah, I think that would make it easier for people to quickly figure out
which feature they need.

On Wed, Apr 29, 2015 at 11:48 PM, Nick Sutterer notifications@github.com
wrote:

Maybe we should note in the README that as soon as you want "more" (e.g.
hypermedia) in a lonely collection, you should switch to a non-lonely
representer?


Reply to this email directly or view it on GitHub
#111 (comment).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants