Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Add "sibling" method to collection (issue #174) #183

Closed
wants to merge 3 commits into from

3 participants

Savater Sebastien Nathan Esquenazi David Sommers
Savater Sebastien

Hi,

Let's take an example.
I need to get the following (JSON) hash to paginate my data.

{  total: 250,
   page: 3,
   total_pages: 25
   data: [
     { id: 1, name: "Foo", ...},
     { id: 2, name: "Bar", ...}
  ]
}

Server-side. I use kaminari and I can now make that :

collection @items => :data

sibling :total_count => :total, :num_pages => :total_pages, :current_page => :page
attributes :id, :name

"Siblings" are methods to which the collection responds.

I guess my pull request is a first approach, and

Nathan Esquenazi
Owner

This is an interesting idea and I appreciate the pull request. Still trying to decide if I like the syntax. Adding another major type to the API like this is not something I want to do lightly. I can see the use for something like this though that appends nodes to the root level of a response.

David Sommers
Collaborator

Is the sibling method is separate from the refactoring of the result method correct?

Personally, I know that each of the to_[format] methods had a few lines of non-DRY code here and there but creating a mega result method with lots of conditionals makes it much harder to read. If anything, I would suggest separating out the sibling patch from the result refactoring and maybe we could approach that another way.

As for controlling the root, I've been doing this:

object false

child(@inspiration => :inspiration) do
  extends("v1/inspirations/show")
end

node(:foo) { @bar }

which gives me something like:

{
  foo: 'whatever bar is'
  inspiration: [ { stuff }, { more_stuff } ]
}
Nathan Esquenazi
Owner

Yeah that is the approach I have been taking too. Also the result refactoring seemed confusing to me as well. I prefer keeping that logic separate to the formats IMO

Savater Sebastien

Thanks for the feedbacks

I also used the same method as @databyte suggested (#174)
But I felt like I was cheating and that's the reason of this pull request : to have a cleaner alternative.

The major problem about this patch is that the method collection_root_name is called in every to_[format] methods, not one time at one place. It seems to be linked to the xml output.

I tried to make the patch the lighter possible : apply the siblings one time at one place, without rebuilding the whole architecture of the engine. That's the reason of the refactoring.

However I must concede that refactoring is my obsessive compulsive disorder. So ok, I can suggest that in a separate commit/request and can clean up this pull request to make the changes clearer.

Now I guess that an other discussion could be about the DSL method itself.
Maybe we can introduce a more common method to add root nodes :

root_node count => @items.total_count
root_node page => @page
...

Then "siblings" (or any better name) can be a shortcut using the root node method.

David Sommers
Collaborator

I think it does make sense to attempt a collection of root nodes without having to use object false as Jbuilder allows for it easily.

We should find a DSL we like and then maybe @blakink can just tweak your implementation and tests for it. I'm not for "sibling" per-se because I think it may get confused with child.

What if it worked similar to child, glue and node.

collection @users do
  node(:total_count) { |m| @user.posts.count }
end

attributes :id, :foo, :bar

So anything going within each object of collection is outside of the block and within the block are the items that get bolted onto collection itself. Thoughts?

Nathan Esquenazi
Owner

One alternative is basically support a clearer scoping for collection/object (backwards compatible or not?):

collection @users do
  attributes :id, :foo, :bar
end

node :total_pages do
  @users.total_pages
end

node :current_page do 
  @users.current_page
end

would put the node outside the collection at the root. I want to be careful to not introduce many more commands in the api. I think object, collection, child, node, attribute, glue is already a lot for people to understand. I think being able to do as you propose databyte makes sense too. What do you think @blakink

Nathan Esquenazi
Owner

What do you guys think about that tho. Making collection optionally into a block with its own dsl rules similar to how child works. Is that too odd to allow things outside the collection block being at the root level (if collection doesn't have a block given)? I can see it working both ways, but it could be confusing. These questions is why I haven't tried to tackle this pull request yet. Not sure of the least confusing syntax.

We could also release RABL 0.6.0 breaking changes and basically requires things to be scoped into their objects or collections:

object @post do
  attributes :id, :name
end

looks pretty intuitive. Perhaps the dsl rules should have been wrapped in object/collection blocks all along. Then working outside at the root just becomes:

collection @posts do
  attributes :id, :name
end

node :total_pages do
  @users.total_pages
end
David Sommers
Collaborator

I like what you're suggesting there but of course you have the issue of breaking changes.

The best route would be to allow both and throw a deprecation warning in 0.6.0 until a 1.0.0 release that works "the new way".

The logic could be wrapped around object/collection such that if it doesn't have a block, then use the old way - otherwise go the new route. That's simplifying it a bit but going with the new DSL, you can't really use object without a block unless you're calling object false. In the instance you are using object false, maybe the new DSL doesn't require to do that either. Seems silly to say object false instead of just assuming object is set false until told otherwise.

Then the triggers for the old format are using object false or object/collection without a block.

Maybe @radar has an opinion too.

Nathan Esquenazi
Owner

Exactly the new proposed changes are actually very syntax simplifying in a lot of ways and more consistent with the rest of rabl. No more object => false, easy addition to the root nodes, and just generally easier to grok I think.

Personally I am not afraid to make breaking changes but I agree basically we should be smart and in 0.6.0 issue deprecation warnings. Treat it the preferred way if a block is yielded, otherwise act in the old way (everything is considered in the block). I think we can do this, if we all agree this is a preferred syntax. Curious on your opinion too @achiu.

I am going to open another issue dedicated to this idea.

David Sommers databyte closed this
Ismael Abreu ismaelga referenced this pull request from a commit
Commit has since been removed from the repository and is no longer available.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Mar 9, 2012
  1. Savater Sebastien

    Add siblings

    inkstak authored
  2. Savater Sebastien

    Add tests

    inkstak authored
Commits on Mar 16, 2012
  1. Savater Sebastien
This page is out of date. Refresh to see the latest.
Showing with 86 additions and 23 deletions.
  1. +44 −23 lib/rabl/engine.rb
  2. +42 −0 test/engine_test.rb
67 lib/rabl/engine.rb
View
@@ -41,32 +41,42 @@ def to_hash(options={})
end
end
+ def result(options={})
+ format = @_options[:format].to_s
+ format = 'msgpack' if format == 'mpac'
+
+ include_root = Rabl.configuration.send("include_#{format}_root")
+ options = options.reverse_merge(:root => include_root, :child_root => include_root)
+
+ root_name = collection_root_name
+ root_name = data_name(@_data) if format == 'bson' && !root_name &&
+ is_collection?(@_data) && @_data.is_a?(Array)
+
+ data = to_hash(options)
+ data = { root_name => data } if root_name
+
+ data.merge! Hash[@_options[:siblings].map { |k,v| [v, @_data.send(k)] }] if data.is_a?(Hash)
+
+ data
+ end
+
# Returns a json representation of the data object
# to_json(:root => true)
def to_json(options={})
- include_root = Rabl.configuration.include_json_root
- options = options.reverse_merge(:root => include_root, :child_root => include_root)
- result = collection_root_name ? { collection_root_name => to_hash(options) } : to_hash(options)
- format_json(result)
+ format_json result(options)
end
# Returns a msgpack representation of the data object
# to_msgpack(:root => true)
def to_msgpack(options={})
- include_root = Rabl.configuration.include_msgpack_root
- options = options.reverse_merge(:root => include_root, :child_root => include_root)
- result = collection_root_name ? { collection_root_name => to_hash(options) } : to_hash(options)
- Rabl.configuration.msgpack_engine.pack result
+ Rabl.configuration.msgpack_engine.pack result(options)
end
alias_method :to_mpac, :to_msgpack
# Returns a plist representation of the data object
# to_plist(:root => true)
def to_plist(options={})
- include_root = Rabl.configuration.include_plist_root
- options = options.reverse_merge(:root => include_root, :child_root => include_root)
- result = defined?(@_collection_name) ? { @_collection_name => to_hash(options) } : to_hash(options)
- Rabl.configuration.plist_engine.dump(result)
+ Rabl.configuration.plist_engine.dump result(options)
end
# Returns an xml representation of the data object
@@ -81,16 +91,7 @@ def to_xml(options={})
# Returns a bson representation of the data object
# to_bson(:root => true)
def to_bson(options={})
- include_root = Rabl.configuration.include_bson_root
- options = options.reverse_merge(:root => include_root, :child_root => include_root)
- result = if collection_root_name
- { collection_root_name => to_hash(options) }
- elsif is_collection?(@_data) && @_data.is_a?(Array)
- { data_name(@_data) => to_hash(options) }
- else
- to_hash(options)
- end
- Rabl.configuration.bson_engine.serialize(result).to_s
+ Rabl.configuration.bson_engine.serialize(result(options)).to_s
end
# Sets the object to be used as the data source for this template
@@ -110,8 +111,27 @@ def collection(data, options={})
@_collection_name = options[:root] if options[:root]
@_collection_name ||= data.values.first if data.respond_to?(:each_pair)
@_object_root_name = options[:object_root] if options.has_key?(:object_root)
- self.object(data_object(data).to_a) if data
+
+ # I don't convert the data object (to_a).
+ # The user must be aware of what he pass
+ # and we need to keep the original object for "siblings" methods
+ # self.object(data_object(data).to_a) if data
+
+ self.object(data_object(data)) if data
+ end
+
+ # Indicates a method to call on collection that should be included in the json output
+ # attribute :foo, :as => "bar"
+ # attribute :foo => :bar
+ def sibling *args
+ if args.first.is_a?(Hash)
+ args.first.each_pair { |k,v| self.sibling(k, :as => v) }
+ else
+ options = args.extract_options!
+ args.each { |name| @_options[:siblings][name] = options[:as] || name }
+ end
end
+ alias_method :siblings, :sibling
# Indicates an attribute or method should be included in the json output
# attribute :foo, :as => "bar"
@@ -214,6 +234,7 @@ def copy_instance_variables_from(object, exclude = []) #:nodoc:
# Resets the options parsed from a rabl template.
def reset_options!
+ @_options[:siblings] = {}
@_options[:attributes] = {}
@_options[:node] = []
@_options[:child] = []
42 test/engine_test.rb
View
@@ -234,6 +234,48 @@
end.equals "{\"user\":{\"name\":\"leo\",\"city\":\"LA\",\"age\":12}}".split('').sort
end
+ context "#sibling" do
+ asserts "that it adds a sibling method in output" do
+ template = rabl %{
+ collection @users, :root => :users
+ sibling :size
+ }
+ scope = Object.new
+ scope.instance_variable_set :@users, [User.new, User.new]
+ template.render(scope)
+ end.equals %{{"users":[{"user":{}},{"user":{}}],"size":2}}
+
+ asserts "that it can add sibling with :as option" do
+ template = rabl %{
+ collection @users, :root => :users
+ sibling :size, :as => 'total_count'
+ }
+ scope = Object.new
+ scope.instance_variable_set :@users, [User.new, User.new]
+ template.render(scope)
+ end.equals %{{"users":[{"user":{}},{"user":{}}],"total_count":2}}
+
+ asserts "that it can add sibling as hash" do
+ template = rabl %{
+ collection @users, :root => :users
+ sibling :size => 'total_count'
+ }
+ scope = Object.new
+ scope.instance_variable_set :@users, [User.new, User.new]
+ template.render(scope)
+ end.equals %{{"users":[{"user":{}},{"user":{}}],"total_count":2}}
+
+ asserts "that it doesn't add sibling methods on a simple array" do
+ template = rabl %{
+ collection @users
+ sibling :size
+ }
+ scope = Object.new
+ scope.instance_variable_set :@users, [User.new, User.new]
+ template.render(scope)
+ end.equals %{[{"user":{}},{"user":{}}]}
+ end
+
teardown do
Rabl.reset_configuration!
end
Something went wrong with that request. Please try again.