Skip to content
This repository was archived by the owner on Dec 1, 2023. It is now read-only.

Repository files navigation

Compositor

Gem VersionBuild statusCode Climate

A Composite Design Pattern with a neat DSL for constructing trees of objects in order to render them as a Hash, and subsequently JSON. Used by Wanelo to generate all JSON API responses by compositing multiple objects together in API responses, converting to a plain ruby Hash (or an Array) and then using OJ gem to convert Hash to JSON.

The performance of this approach to generate JSON was faster than using RABL in our limited testing, although if performance is not an issue for you, RABL is still an excellent choice, and served us well for almost a year.

Installation

Add this line to your application's Gemfile:

gem 'compositor'

And then execute:

$ bundle

Or install it yourself as:

$ gem install compositor

Usage

For each model that needs a hash/json representation you need to create a ruby class that subclasses Composite::Leaf, adds some custom state that's important for rendering that object in addition to context, implements a proper constructor (see example), and finally implement the main rendering #to_hash method (used as the "operation" in the Composite pattern terminology).

The context variable is a reference to an object holding necessary helpers for generating JSON, for example Rails Controllers expose a view_context instance, which contains helper methods necessary to generate application URLs.

Outside of Rails application, context can be any other object holding application helpers or state. All subclasses of Compositor::Leaf such as UserCompositor inherit context attribute and accessors, and so can use the context in generating URLs, or calling any other application helpers.

We recommend that you place your Compositor classes in eg app/compositors/* directory, which defines one (or more than one) compositor(s) per model class you will be rendering.

# File: app/compositors/user_compositor.rbclassUserCompositor < Compositor::Leafattr_accessor:userdefinitialize(context,user,attrs={})super(context,attrs)self.user=userenddefto_hash{id: user.id,username: user.username,location: user.location,bio: user.bio,url: user.url,image_url: context.image_path(user.avatar),# using context to generate URL path from routes
...
}endend

You could create this class directly, as in

uc=UserCompositor.new(view_context,user,{})uc.to_hash# => returns a Hash representationuc.to_json# => calls to_hash, and then renders JSON

But constructing trees of objects that represent a complex API responses requires a lot more than that, such as constructing lists (arrays) or maps (hashes) of objects, and deciding which order they appear, and whether each inner Hash comes with a "root" element, such as :product => { :id => 1, ... } where :product is the root element.

So here is how to create a list of users in this way, but explicitly declaring classes:

compositor=ListCompositor.new(view_context,:collection=>@users.map{ |user|
UserCompositor.new(view_context,user,{:root=>true})},:root=>:users)

When calling to_hash on the top level compositor, we get:

:users=>{:user=>{id: 1234,
username: "kigster",location: "San Francisco",bio: "",url: "",image_url: "http://cdn-app.domain.com/kigster/avatar/200.jpg"},:user=>{id: 1235,username: "johnny",location: "Sunnyvale",bio: "",url: "",image_url: "http://cdn-app.domain.com/johnny/avatar/200.jpg"}}

So this is how you can assemple multiple compositors together without the DSL.

But the real power of this gem is in the additional DSL class, that dramatically simplifies definition of complex responses, as described below.

Using the DSL

UserCompositor class, when defined, automatically adds a user method to the DSL class, which effictively instantiates the new UserCompositor instance, passing the context into it automatically.

Using built-in MapCompositor and ListCompositor we can construct multiple objects into a larger hierarchy.

In the example below, an application is assumed to define StoreCompositorand ProductCompositor classes similar to UserCompositor, but wrapping Store and Product ActiveRecord models.

compositor=Compositor::DSL.create(context)domapdostore@store,root: :storeuser@user,root: :userlistcollection: @products,root: :productsdo |p|
productpendendend# now we can call to_hash or to_json on the compositor:putscompositor.to_hash# =>{:store=>{id: 12354,name: "amazon.com",url: "http://www.amazon.com",
..
},:user=>{id: 1234,username: "kigster",location: "San Francisco",bio: "",url: "",image_url: "http://cdn-app.domain.com/kigster/avatar/200.jpg"},:products=>[{id: 1234,:name=>"Awesome Product", ... },{id: 4325,:name=>"Another Awesome Product", ... }}}

Inside the list definition above, @products is a collection of Products, ActiveRecord objects, and the block maps each to a Compositor using product method, registered by ProductCompositor.

Instance Variables

One thing to note, is that when Compositor::DSL is used, the gem copies all instance variables from the context into the DSL instance, so in the above example instance variable @user was defined on view_context (by Rails, which copies them from the Controller instance), and so became automatically available inside DSL. Note that all instance variables must be defined before the DSL instance is created.

Method Names in the DSL

Compositor will extract the full name of the class and place that name into the DSL. For example, MyModule::UserCompositor will define a my_module_user method in the DSL. To create a method called user, the standard convention is to define a class in the global namespace called UserCompositor.

If you prefer to have your own Compositor class hierarchy, or just compositors that should not be added to the DSL, you can name the classes starting with Abstract, such as MyModule::AbstractCompositor.

Performance

Note of caution: despite the fact that typical DSL generation can take mere 50-100 microseconds, defining complex responses with DSL does carry a performance penantly of about 50% (we measured it!). Which generally means that generating multiple Composite objects in a loop using the DSL is probably not recommended, but doing it once per web/API request is completely reasonable.

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Added some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

Maintainers

Konstantin Gredeskoul (@kigster) and Paul Henry (@letuboy)

(c) 2013, All rights reserved, distributed under MIT license.

About

Composite pattern with DSL for constructing trees of objects in to render them in JSON.

Resources

Stars

19 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - wanelo/compositor: Composite pattern with DSL for constructing trees of objects in to render them in JSON. · GitHub
Skip to content
This repository was archived by the owner on Dec 1, 2023. It is now read-only.

Repository files navigation

Compositor

Gem VersionBuild statusCode Climate

A Composite Design Pattern with a neat DSL for constructing trees of objects in order to render them as a Hash, and subsequently JSON. Used by Wanelo to generate all JSON API responses by compositing multiple objects together in API responses, converting to a plain ruby Hash (or an Array) and then using OJ gem to convert Hash to JSON.

The performance of this approach to generate JSON was faster than using RABL in our limited testing, although if performance is not an issue for you, RABL is still an excellent choice, and served us well for almost a year.

Installation

Add this line to your application's Gemfile:

gem 'compositor'

And then execute:

$ bundle

Or install it yourself as:

$ gem install compositor

Usage

For each model that needs a hash/json representation you need to create a ruby class that subclasses Composite::Leaf, adds some custom state that's important for rendering that object in addition to context, implements a proper constructor (see example), and finally implement the main rendering #to_hash method (used as the "operation" in the Composite pattern terminology).

The context variable is a reference to an object holding necessary helpers for generating JSON, for example Rails Controllers expose a view_context instance, which contains helper methods necessary to generate application URLs.

Outside of Rails application, context can be any other object holding application helpers or state. All subclasses of Compositor::Leaf such as UserCompositor inherit context attribute and accessors, and so can use the context in generating URLs, or calling any other application helpers.

We recommend that you place your Compositor classes in eg app/compositors/* directory, which defines one (or more than one) compositor(s) per model class you will be rendering.

# File: app/compositors/user_compositor.rbclassUserCompositor < Compositor::Leafattr_accessor:userdefinitialize(context,user,attrs={})super(context,attrs)self.user=userenddefto_hash{id: user.id,username: user.username,location: user.location,bio: user.bio,url: user.url,image_url: context.image_path(user.avatar),# using context to generate URL path from routes
...
}endend

You could create this class directly, as in

uc=UserCompositor.new(view_context,user,{})uc.to_hash# => returns a Hash representationuc.to_json# => calls to_hash, and then renders JSON

But constructing trees of objects that represent a complex API responses requires a lot more than that, such as constructing lists (arrays) or maps (hashes) of objects, and deciding which order they appear, and whether each inner Hash comes with a "root" element, such as :product => { :id => 1, ... } where :product is the root element.

So here is how to create a list of users in this way, but explicitly declaring classes:

compositor=ListCompositor.new(view_context,:collection=>@users.map{ |user|
UserCompositor.new(view_context,user,{:root=>true})},:root=>:users)

When calling to_hash on the top level compositor, we get:

:users=>{:user=>{id: 1234,
username: "kigster",location: "San Francisco",bio: "",url: "",image_url: "http://cdn-app.domain.com/kigster/avatar/200.jpg"},:user=>{id: 1235,username: "johnny",location: "Sunnyvale",bio: "",url: "",image_url: "http://cdn-app.domain.com/johnny/avatar/200.jpg"}}

So this is how you can assemple multiple compositors together without the DSL.

But the real power of this gem is in the additional DSL class, that dramatically simplifies definition of complex responses, as described below.

Using the DSL

UserCompositor class, when defined, automatically adds a user method to the DSL class, which effictively instantiates the new UserCompositor instance, passing the context into it automatically.

Using built-in MapCompositor and ListCompositor we can construct multiple objects into a larger hierarchy.

In the example below, an application is assumed to define StoreCompositorand ProductCompositor classes similar to UserCompositor, but wrapping Store and Product ActiveRecord models.

compositor=Compositor::DSL.create(context)domapdostore@store,root: :storeuser@user,root: :userlistcollection: @products,root: :productsdo |p|
productpendendend# now we can call to_hash or to_json on the compositor:putscompositor.to_hash# =>{:store=>{id: 12354,name: "amazon.com",url: "http://www.amazon.com",
..
},:user=>{id: 1234,username: "kigster",location: "San Francisco",bio: "",url: "",image_url: "http://cdn-app.domain.com/kigster/avatar/200.jpg"},:products=>[{id: 1234,:name=>"Awesome Product", ... },{id: 4325,:name=>"Another Awesome Product", ... }}}

Inside the list definition above, @products is a collection of Products, ActiveRecord objects, and the block maps each to a Compositor using product method, registered by ProductCompositor.

Instance Variables

One thing to note, is that when Compositor::DSL is used, the gem copies all instance variables from the context into the DSL instance, so in the above example instance variable @user was defined on view_context (by Rails, which copies them from the Controller instance), and so became automatically available inside DSL. Note that all instance variables must be defined before the DSL instance is created.

Method Names in the DSL

Compositor will extract the full name of the class and place that name into the DSL. For example, MyModule::UserCompositor will define a my_module_user method in the DSL. To create a method called user, the standard convention is to define a class in the global namespace called UserCompositor.

If you prefer to have your own Compositor class hierarchy, or just compositors that should not be added to the DSL, you can name the classes starting with Abstract, such as MyModule::AbstractCompositor.

Performance

Note of caution: despite the fact that typical DSL generation can take mere 50-100 microseconds, defining complex responses with DSL does carry a performance penantly of about 50% (we measured it!). Which generally means that generating multiple Composite objects in a loop using the DSL is probably not recommended, but doing it once per web/API request is completely reasonable.

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Added some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

Maintainers

Konstantin Gredeskoul (@kigster) and Paul Henry (@letuboy)

(c) 2013, All rights reserved, distributed under MIT license.

About

Composite pattern with DSL for constructing trees of objects in to render them in JSON.

Resources

Stars

19 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - wanelo/compositor: Composite pattern with DSL for constructing trees of objects in to render them in JSON. · GitHub
Skip to content
This repository was archived by the owner on Dec 1, 2023. It is now read-only.

Repository files navigation

Compositor

Gem VersionBuild statusCode Climate

A Composite Design Pattern with a neat DSL for constructing trees of objects in order to render them as a Hash, and subsequently JSON. Used by Wanelo to generate all JSON API responses by compositing multiple objects together in API responses, converting to a plain ruby Hash (or an Array) and then using OJ gem to convert Hash to JSON.

The performance of this approach to generate JSON was faster than using RABL in our limited testing, although if performance is not an issue for you, RABL is still an excellent choice, and served us well for almost a year.

Installation

Add this line to your application's Gemfile:

gem 'compositor'

And then execute:

$ bundle

Or install it yourself as:

$ gem install compositor

Usage

For each model that needs a hash/json representation you need to create a ruby class that subclasses Composite::Leaf, adds some custom state that's important for rendering that object in addition to context, implements a proper constructor (see example), and finally implement the main rendering #to_hash method (used as the "operation" in the Composite pattern terminology).

The context variable is a reference to an object holding necessary helpers for generating JSON, for example Rails Controllers expose a view_context instance, which contains helper methods necessary to generate application URLs.

Outside of Rails application, context can be any other object holding application helpers or state. All subclasses of Compositor::Leaf such as UserCompositor inherit context attribute and accessors, and so can use the context in generating URLs, or calling any other application helpers.

We recommend that you place your Compositor classes in eg app/compositors/* directory, which defines one (or more than one) compositor(s) per model class you will be rendering.

# File: app/compositors/user_compositor.rbclassUserCompositor < Compositor::Leafattr_accessor:userdefinitialize(context,user,attrs={})super(context,attrs)self.user=userenddefto_hash{id: user.id,username: user.username,location: user.location,bio: user.bio,url: user.url,image_url: context.image_path(user.avatar),# using context to generate URL path from routes
...
}endend

You could create this class directly, as in

uc=UserCompositor.new(view_context,user,{})uc.to_hash# => returns a Hash representationuc.to_json# => calls to_hash, and then renders JSON

But constructing trees of objects that represent a complex API responses requires a lot more than that, such as constructing lists (arrays) or maps (hashes) of objects, and deciding which order they appear, and whether each inner Hash comes with a "root" element, such as :product => { :id => 1, ... } where :product is the root element.

So here is how to create a list of users in this way, but explicitly declaring classes:

compositor=ListCompositor.new(view_context,:collection=>@users.map{ |user|
UserCompositor.new(view_context,user,{:root=>true})},:root=>:users)

When calling to_hash on the top level compositor, we get:

:users=>{:user=>{id: 1234,
username: "kigster",location: "San Francisco",bio: "",url: "",image_url: "http://cdn-app.domain.com/kigster/avatar/200.jpg"},:user=>{id: 1235,username: "johnny",location: "Sunnyvale",bio: "",url: "",image_url: "http://cdn-app.domain.com/johnny/avatar/200.jpg"}}

So this is how you can assemple multiple compositors together without the DSL.

But the real power of this gem is in the additional DSL class, that dramatically simplifies definition of complex responses, as described below.

Using the DSL

UserCompositor class, when defined, automatically adds a user method to the DSL class, which effictively instantiates the new UserCompositor instance, passing the context into it automatically.

Using built-in MapCompositor and ListCompositor we can construct multiple objects into a larger hierarchy.

In the example below, an application is assumed to define StoreCompositorand ProductCompositor classes similar to UserCompositor, but wrapping Store and Product ActiveRecord models.

compositor=Compositor::DSL.create(context)domapdostore@store,root: :storeuser@user,root: :userlistcollection: @products,root: :productsdo |p|
productpendendend# now we can call to_hash or to_json on the compositor:putscompositor.to_hash# =>{:store=>{id: 12354,name: "amazon.com",url: "http://www.amazon.com",
..
},:user=>{id: 1234,username: "kigster",location: "San Francisco",bio: "",url: "",image_url: "http://cdn-app.domain.com/kigster/avatar/200.jpg"},:products=>[{id: 1234,:name=>"Awesome Product", ... },{id: 4325,:name=>"Another Awesome Product", ... }}}

Inside the list definition above, @products is a collection of Products, ActiveRecord objects, and the block maps each to a Compositor using product method, registered by ProductCompositor.

Instance Variables

One thing to note, is that when Compositor::DSL is used, the gem copies all instance variables from the context into the DSL instance, so in the above example instance variable @user was defined on view_context (by Rails, which copies them from the Controller instance), and so became automatically available inside DSL. Note that all instance variables must be defined before the DSL instance is created.

Method Names in the DSL

Compositor will extract the full name of the class and place that name into the DSL. For example, MyModule::UserCompositor will define a my_module_user method in the DSL. To create a method called user, the standard convention is to define a class in the global namespace called UserCompositor.

If you prefer to have your own Compositor class hierarchy, or just compositors that should not be added to the DSL, you can name the classes starting with Abstract, such as MyModule::AbstractCompositor.

Performance

Note of caution: despite the fact that typical DSL generation can take mere 50-100 microseconds, defining complex responses with DSL does carry a performance penantly of about 50% (we measured it!). Which generally means that generating multiple Composite objects in a loop using the DSL is probably not recommended, but doing it once per web/API request is completely reasonable.

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Added some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

Maintainers

Konstantin Gredeskoul (@kigster) and Paul Henry (@letuboy)

(c) 2013, All rights reserved, distributed under MIT license.

About

Composite pattern with DSL for constructing trees of objects in to render them in JSON.

Resources

Stars

19 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - wanelo/compositor: Composite pattern with DSL for constructing trees of objects in to render them in JSON. · GitHub
Skip to content
This repository was archived by the owner on Dec 1, 2023. It is now read-only.

Repository files navigation

Compositor

Gem VersionBuild statusCode Climate

A Composite Design Pattern with a neat DSL for constructing trees of objects in order to render them as a Hash, and subsequently JSON. Used by Wanelo to generate all JSON API responses by compositing multiple objects together in API responses, converting to a plain ruby Hash (or an Array) and then using OJ gem to convert Hash to JSON.

The performance of this approach to generate JSON was faster than using RABL in our limited testing, although if performance is not an issue for you, RABL is still an excellent choice, and served us well for almost a year.

Installation

Add this line to your application's Gemfile:

gem 'compositor'

And then execute:

$ bundle

Or install it yourself as:

$ gem install compositor

Usage

For each model that needs a hash/json representation you need to create a ruby class that subclasses Composite::Leaf, adds some custom state that's important for rendering that object in addition to context, implements a proper constructor (see example), and finally implement the main rendering #to_hash method (used as the "operation" in the Composite pattern terminology).

The context variable is a reference to an object holding necessary helpers for generating JSON, for example Rails Controllers expose a view_context instance, which contains helper methods necessary to generate application URLs.

Outside of Rails application, context can be any other object holding application helpers or state. All subclasses of Compositor::Leaf such as UserCompositor inherit context attribute and accessors, and so can use the context in generating URLs, or calling any other application helpers.

We recommend that you place your Compositor classes in eg app/compositors/* directory, which defines one (or more than one) compositor(s) per model class you will be rendering.

# File: app/compositors/user_compositor.rbclassUserCompositor < Compositor::Leafattr_accessor:userdefinitialize(context,user,attrs={})super(context,attrs)self.user=userenddefto_hash{id: user.id,username: user.username,location: user.location,bio: user.bio,url: user.url,image_url: context.image_path(user.avatar),# using context to generate URL path from routes
...
}endend

You could create this class directly, as in

uc=UserCompositor.new(view_context,user,{})uc.to_hash# => returns a Hash representationuc.to_json# => calls to_hash, and then renders JSON

But constructing trees of objects that represent a complex API responses requires a lot more than that, such as constructing lists (arrays) or maps (hashes) of objects, and deciding which order they appear, and whether each inner Hash comes with a "root" element, such as :product => { :id => 1, ... } where :product is the root element.

So here is how to create a list of users in this way, but explicitly declaring classes:

compositor=ListCompositor.new(view_context,:collection=>@users.map{ |user|
UserCompositor.new(view_context,user,{:root=>true})},:root=>:users)

When calling to_hash on the top level compositor, we get:

:users=>{:user=>{id: 1234,
username: "kigster",location: "San Francisco",bio: "",url: "",image_url: "http://cdn-app.domain.com/kigster/avatar/200.jpg"},:user=>{id: 1235,username: "johnny",location: "Sunnyvale",bio: "",url: "",image_url: "http://cdn-app.domain.com/johnny/avatar/200.jpg"}}

So this is how you can assemple multiple compositors together without the DSL.

But the real power of this gem is in the additional DSL class, that dramatically simplifies definition of complex responses, as described below.

Using the DSL

UserCompositor class, when defined, automatically adds a user method to the DSL class, which effictively instantiates the new UserCompositor instance, passing the context into it automatically.

Using built-in MapCompositor and ListCompositor we can construct multiple objects into a larger hierarchy.

In the example below, an application is assumed to define StoreCompositorand ProductCompositor classes similar to UserCompositor, but wrapping Store and Product ActiveRecord models.

compositor=Compositor::DSL.create(context)domapdostore@store,root: :storeuser@user,root: :userlistcollection: @products,root: :productsdo |p|
productpendendend# now we can call to_hash or to_json on the compositor:putscompositor.to_hash# =>{:store=>{id: 12354,name: "amazon.com",url: "http://www.amazon.com",
..
},:user=>{id: 1234,username: "kigster",location: "San Francisco",bio: "",url: "",image_url: "http://cdn-app.domain.com/kigster/avatar/200.jpg"},:products=>[{id: 1234,:name=>"Awesome Product", ... },{id: 4325,:name=>"Another Awesome Product", ... }}}

Inside the list definition above, @products is a collection of Products, ActiveRecord objects, and the block maps each to a Compositor using product method, registered by ProductCompositor.

Instance Variables

One thing to note, is that when Compositor::DSL is used, the gem copies all instance variables from the context into the DSL instance, so in the above example instance variable @user was defined on view_context (by Rails, which copies them from the Controller instance), and so became automatically available inside DSL. Note that all instance variables must be defined before the DSL instance is created.

Method Names in the DSL

Compositor will extract the full name of the class and place that name into the DSL. For example, MyModule::UserCompositor will define a my_module_user method in the DSL. To create a method called user, the standard convention is to define a class in the global namespace called UserCompositor.

If you prefer to have your own Compositor class hierarchy, or just compositors that should not be added to the DSL, you can name the classes starting with Abstract, such as MyModule::AbstractCompositor.

Performance

Note of caution: despite the fact that typical DSL generation can take mere 50-100 microseconds, defining complex responses with DSL does carry a performance penantly of about 50% (we measured it!). Which generally means that generating multiple Composite objects in a loop using the DSL is probably not recommended, but doing it once per web/API request is completely reasonable.

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Added some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

Maintainers

Konstantin Gredeskoul (@kigster) and Paul Henry (@letuboy)

(c) 2013, All rights reserved, distributed under MIT license.

About

Composite pattern with DSL for constructing trees of objects in to render them in JSON.

Resources

Stars

19 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - wanelo/compositor: Composite pattern with DSL for constructing trees of objects in to render them in JSON. · GitHub
Skip to content
This repository was archived by the owner on Dec 1, 2023. It is now read-only.

Repository files navigation

Compositor

Gem VersionBuild statusCode Climate

A Composite Design Pattern with a neat DSL for constructing trees of objects in order to render them as a Hash, and subsequently JSON. Used by Wanelo to generate all JSON API responses by compositing multiple objects together in API responses, converting to a plain ruby Hash (or an Array) and then using OJ gem to convert Hash to JSON.

The performance of this approach to generate JSON was faster than using RABL in our limited testing, although if performance is not an issue for you, RABL is still an excellent choice, and served us well for almost a year.

Installation

Add this line to your application's Gemfile:

gem 'compositor'

And then execute:

$ bundle

Or install it yourself as:

$ gem install compositor

Usage

For each model that needs a hash/json representation you need to create a ruby class that subclasses Composite::Leaf, adds some custom state that's important for rendering that object in addition to context, implements a proper constructor (see example), and finally implement the main rendering #to_hash method (used as the "operation" in the Composite pattern terminology).

The context variable is a reference to an object holding necessary helpers for generating JSON, for example Rails Controllers expose a view_context instance, which contains helper methods necessary to generate application URLs.

Outside of Rails application, context can be any other object holding application helpers or state. All subclasses of Compositor::Leaf such as UserCompositor inherit context attribute and accessors, and so can use the context in generating URLs, or calling any other application helpers.

We recommend that you place your Compositor classes in eg app/compositors/* directory, which defines one (or more than one) compositor(s) per model class you will be rendering.

# File: app/compositors/user_compositor.rbclassUserCompositor < Compositor::Leafattr_accessor:userdefinitialize(context,user,attrs={})super(context,attrs)self.user=userenddefto_hash{id: user.id,username: user.username,location: user.location,bio: user.bio,url: user.url,image_url: context.image_path(user.avatar),# using context to generate URL path from routes
...
}endend

You could create this class directly, as in

uc=UserCompositor.new(view_context,user,{})uc.to_hash# => returns a Hash representationuc.to_json# => calls to_hash, and then renders JSON

But constructing trees of objects that represent a complex API responses requires a lot more than that, such as constructing lists (arrays) or maps (hashes) of objects, and deciding which order they appear, and whether each inner Hash comes with a "root" element, such as :product => { :id => 1, ... } where :product is the root element.

So here is how to create a list of users in this way, but explicitly declaring classes:

compositor=ListCompositor.new(view_context,:collection=>@users.map{ |user|
UserCompositor.new(view_context,user,{:root=>true})},:root=>:users)

When calling to_hash on the top level compositor, we get:

:users=>{:user=>{id: 1234,
username: "kigster",location: "San Francisco",bio: "",url: "",image_url: "http://cdn-app.domain.com/kigster/avatar/200.jpg"},:user=>{id: 1235,username: "johnny",location: "Sunnyvale",bio: "",url: "",image_url: "http://cdn-app.domain.com/johnny/avatar/200.jpg"}}

So this is how you can assemple multiple compositors together without the DSL.

But the real power of this gem is in the additional DSL class, that dramatically simplifies definition of complex responses, as described below.

Using the DSL

UserCompositor class, when defined, automatically adds a user method to the DSL class, which effictively instantiates the new UserCompositor instance, passing the context into it automatically.

Using built-in MapCompositor and ListCompositor we can construct multiple objects into a larger hierarchy.

In the example below, an application is assumed to define StoreCompositorand ProductCompositor classes similar to UserCompositor, but wrapping Store and Product ActiveRecord models.

compositor=Compositor::DSL.create(context)domapdostore@store,root: :storeuser@user,root: :userlistcollection: @products,root: :productsdo |p|
productpendendend# now we can call to_hash or to_json on the compositor:putscompositor.to_hash# =>{:store=>{id: 12354,name: "amazon.com",url: "http://www.amazon.com",
..
},:user=>{id: 1234,username: "kigster",location: "San Francisco",bio: "",url: "",image_url: "http://cdn-app.domain.com/kigster/avatar/200.jpg"},:products=>[{id: 1234,:name=>"Awesome Product", ... },{id: 4325,:name=>"Another Awesome Product", ... }}}

Inside the list definition above, @products is a collection of Products, ActiveRecord objects, and the block maps each to a Compositor using product method, registered by ProductCompositor.

Instance Variables

One thing to note, is that when Compositor::DSL is used, the gem copies all instance variables from the context into the DSL instance, so in the above example instance variable @user was defined on view_context (by Rails, which copies them from the Controller instance), and so became automatically available inside DSL. Note that all instance variables must be defined before the DSL instance is created.

Method Names in the DSL

Compositor will extract the full name of the class and place that name into the DSL. For example, MyModule::UserCompositor will define a my_module_user method in the DSL. To create a method called user, the standard convention is to define a class in the global namespace called UserCompositor.

If you prefer to have your own Compositor class hierarchy, or just compositors that should not be added to the DSL, you can name the classes starting with Abstract, such as MyModule::AbstractCompositor.

Performance

Note of caution: despite the fact that typical DSL generation can take mere 50-100 microseconds, defining complex responses with DSL does carry a performance penantly of about 50% (we measured it!). Which generally means that generating multiple Composite objects in a loop using the DSL is probably not recommended, but doing it once per web/API request is completely reasonable.

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Added some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

Maintainers

Konstantin Gredeskoul (@kigster) and Paul Henry (@letuboy)

(c) 2013, All rights reserved, distributed under MIT license.

About

Composite pattern with DSL for constructing trees of objects in to render them in JSON.

Resources

Stars

19 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - wanelo/compositor: Composite pattern with DSL for constructing trees of objects in to render them in JSON. · GitHub
Skip to content
This repository was archived by the owner on Dec 1, 2023. It is now read-only.

Repository files navigation

Compositor

Gem VersionBuild statusCode Climate

A Composite Design Pattern with a neat DSL for constructing trees of objects in order to render them as a Hash, and subsequently JSON. Used by Wanelo to generate all JSON API responses by compositing multiple objects together in API responses, converting to a plain ruby Hash (or an Array) and then using OJ gem to convert Hash to JSON.

The performance of this approach to generate JSON was faster than using RABL in our limited testing, although if performance is not an issue for you, RABL is still an excellent choice, and served us well for almost a year.

Installation

Add this line to your application's Gemfile:

gem 'compositor'

And then execute:

$ bundle

Or install it yourself as:

$ gem install compositor

Usage

For each model that needs a hash/json representation you need to create a ruby class that subclasses Composite::Leaf, adds some custom state that's important for rendering that object in addition to context, implements a proper constructor (see example), and finally implement the main rendering #to_hash method (used as the "operation" in the Composite pattern terminology).

The context variable is a reference to an object holding necessary helpers for generating JSON, for example Rails Controllers expose a view_context instance, which contains helper methods necessary to generate application URLs.

Outside of Rails application, context can be any other object holding application helpers or state. All subclasses of Compositor::Leaf such as UserCompositor inherit context attribute and accessors, and so can use the context in generating URLs, or calling any other application helpers.

We recommend that you place your Compositor classes in eg app/compositors/* directory, which defines one (or more than one) compositor(s) per model class you will be rendering.

# File: app/compositors/user_compositor.rbclassUserCompositor < Compositor::Leafattr_accessor:userdefinitialize(context,user,attrs={})super(context,attrs)self.user=userenddefto_hash{id: user.id,username: user.username,location: user.location,bio: user.bio,url: user.url,image_url: context.image_path(user.avatar),# using context to generate URL path from routes
...
}endend

You could create this class directly, as in

uc=UserCompositor.new(view_context,user,{})uc.to_hash# => returns a Hash representationuc.to_json# => calls to_hash, and then renders JSON

But constructing trees of objects that represent a complex API responses requires a lot more than that, such as constructing lists (arrays) or maps (hashes) of objects, and deciding which order they appear, and whether each inner Hash comes with a "root" element, such as :product => { :id => 1, ... } where :product is the root element.

So here is how to create a list of users in this way, but explicitly declaring classes:

compositor=ListCompositor.new(view_context,:collection=>@users.map{ |user|
UserCompositor.new(view_context,user,{:root=>true})},:root=>:users)

When calling to_hash on the top level compositor, we get:

:users=>{:user=>{id: 1234,
username: "kigster",location: "San Francisco",bio: "",url: "",image_url: "http://cdn-app.domain.com/kigster/avatar/200.jpg"},:user=>{id: 1235,username: "johnny",location: "Sunnyvale",bio: "",url: "",image_url: "http://cdn-app.domain.com/johnny/avatar/200.jpg"}}

So this is how you can assemple multiple compositors together without the DSL.

But the real power of this gem is in the additional DSL class, that dramatically simplifies definition of complex responses, as described below.

Using the DSL

UserCompositor class, when defined, automatically adds a user method to the DSL class, which effictively instantiates the new UserCompositor instance, passing the context into it automatically.

Using built-in MapCompositor and ListCompositor we can construct multiple objects into a larger hierarchy.

In the example below, an application is assumed to define StoreCompositorand ProductCompositor classes similar to UserCompositor, but wrapping Store and Product ActiveRecord models.

compositor=Compositor::DSL.create(context)domapdostore@store,root: :storeuser@user,root: :userlistcollection: @products,root: :productsdo |p|
productpendendend# now we can call to_hash or to_json on the compositor:putscompositor.to_hash# =>{:store=>{id: 12354,name: "amazon.com",url: "http://www.amazon.com",
..
},:user=>{id: 1234,username: "kigster",location: "San Francisco",bio: "",url: "",image_url: "http://cdn-app.domain.com/kigster/avatar/200.jpg"},:products=>[{id: 1234,:name=>"Awesome Product", ... },{id: 4325,:name=>"Another Awesome Product", ... }}}

Inside the list definition above, @products is a collection of Products, ActiveRecord objects, and the block maps each to a Compositor using product method, registered by ProductCompositor.

Instance Variables

One thing to note, is that when Compositor::DSL is used, the gem copies all instance variables from the context into the DSL instance, so in the above example instance variable @user was defined on view_context (by Rails, which copies them from the Controller instance), and so became automatically available inside DSL. Note that all instance variables must be defined before the DSL instance is created.

Method Names in the DSL

Compositor will extract the full name of the class and place that name into the DSL. For example, MyModule::UserCompositor will define a my_module_user method in the DSL. To create a method called user, the standard convention is to define a class in the global namespace called UserCompositor.

If you prefer to have your own Compositor class hierarchy, or just compositors that should not be added to the DSL, you can name the classes starting with Abstract, such as MyModule::AbstractCompositor.

Performance

Note of caution: despite the fact that typical DSL generation can take mere 50-100 microseconds, defining complex responses with DSL does carry a performance penantly of about 50% (we measured it!). Which generally means that generating multiple Composite objects in a loop using the DSL is probably not recommended, but doing it once per web/API request is completely reasonable.

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Added some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

Maintainers

Konstantin Gredeskoul (@kigster) and Paul Henry (@letuboy)

(c) 2013, All rights reserved, distributed under MIT license.

About

Composite pattern with DSL for constructing trees of objects in to render them in JSON.

Resources

Stars

19 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - wanelo/compositor: Composite pattern with DSL for constructing trees of objects in to render them in JSON. · GitHub
Skip to content
This repository was archived by the owner on Dec 1, 2023. It is now read-only.

Repository files navigation

Compositor

Gem VersionBuild statusCode Climate

A Composite Design Pattern with a neat DSL for constructing trees of objects in order to render them as a Hash, and subsequently JSON. Used by Wanelo to generate all JSON API responses by compositing multiple objects together in API responses, converting to a plain ruby Hash (or an Array) and then using OJ gem to convert Hash to JSON.

The performance of this approach to generate JSON was faster than using RABL in our limited testing, although if performance is not an issue for you, RABL is still an excellent choice, and served us well for almost a year.

Installation

Add this line to your application's Gemfile:

gem 'compositor'

And then execute:

$ bundle

Or install it yourself as:

$ gem install compositor

Usage

For each model that needs a hash/json representation you need to create a ruby class that subclasses Composite::Leaf, adds some custom state that's important for rendering that object in addition to context, implements a proper constructor (see example), and finally implement the main rendering #to_hash method (used as the "operation" in the Composite pattern terminology).

The context variable is a reference to an object holding necessary helpers for generating JSON, for example Rails Controllers expose a view_context instance, which contains helper methods necessary to generate application URLs.

Outside of Rails application, context can be any other object holding application helpers or state. All subclasses of Compositor::Leaf such as UserCompositor inherit context attribute and accessors, and so can use the context in generating URLs, or calling any other application helpers.

We recommend that you place your Compositor classes in eg app/compositors/* directory, which defines one (or more than one) compositor(s) per model class you will be rendering.

# File: app/compositors/user_compositor.rbclassUserCompositor < Compositor::Leafattr_accessor:userdefinitialize(context,user,attrs={})super(context,attrs)self.user=userenddefto_hash{id: user.id,username: user.username,location: user.location,bio: user.bio,url: user.url,image_url: context.image_path(user.avatar),# using context to generate URL path from routes
...
}endend

You could create this class directly, as in

uc=UserCompositor.new(view_context,user,{})uc.to_hash# => returns a Hash representationuc.to_json# => calls to_hash, and then renders JSON

But constructing trees of objects that represent a complex API responses requires a lot more than that, such as constructing lists (arrays) or maps (hashes) of objects, and deciding which order they appear, and whether each inner Hash comes with a "root" element, such as :product => { :id => 1, ... } where :product is the root element.

So here is how to create a list of users in this way, but explicitly declaring classes:

compositor=ListCompositor.new(view_context,:collection=>@users.map{ |user|
UserCompositor.new(view_context,user,{:root=>true})},:root=>:users)

When calling to_hash on the top level compositor, we get:

:users=>{:user=>{id: 1234,
username: "kigster",location: "San Francisco",bio: "",url: "",image_url: "http://cdn-app.domain.com/kigster/avatar/200.jpg"},:user=>{id: 1235,username: "johnny",location: "Sunnyvale",bio: "",url: "",image_url: "http://cdn-app.domain.com/johnny/avatar/200.jpg"}}

So this is how you can assemple multiple compositors together without the DSL.

But the real power of this gem is in the additional DSL class, that dramatically simplifies definition of complex responses, as described below.

Using the DSL

UserCompositor class, when defined, automatically adds a user method to the DSL class, which effictively instantiates the new UserCompositor instance, passing the context into it automatically.

Using built-in MapCompositor and ListCompositor we can construct multiple objects into a larger hierarchy.

In the example below, an application is assumed to define StoreCompositorand ProductCompositor classes similar to UserCompositor, but wrapping Store and Product ActiveRecord models.

compositor=Compositor::DSL.create(context)domapdostore@store,root: :storeuser@user,root: :userlistcollection: @products,root: :productsdo |p|
productpendendend# now we can call to_hash or to_json on the compositor:putscompositor.to_hash# =>{:store=>{id: 12354,name: "amazon.com",url: "http://www.amazon.com",
..
},:user=>{id: 1234,username: "kigster",location: "San Francisco",bio: "",url: "",image_url: "http://cdn-app.domain.com/kigster/avatar/200.jpg"},:products=>[{id: 1234,:name=>"Awesome Product", ... },{id: 4325,:name=>"Another Awesome Product", ... }}}

Inside the list definition above, @products is a collection of Products, ActiveRecord objects, and the block maps each to a Compositor using product method, registered by ProductCompositor.

Instance Variables

One thing to note, is that when Compositor::DSL is used, the gem copies all instance variables from the context into the DSL instance, so in the above example instance variable @user was defined on view_context (by Rails, which copies them from the Controller instance), and so became automatically available inside DSL. Note that all instance variables must be defined before the DSL instance is created.

Method Names in the DSL

Compositor will extract the full name of the class and place that name into the DSL. For example, MyModule::UserCompositor will define a my_module_user method in the DSL. To create a method called user, the standard convention is to define a class in the global namespace called UserCompositor.

If you prefer to have your own Compositor class hierarchy, or just compositors that should not be added to the DSL, you can name the classes starting with Abstract, such as MyModule::AbstractCompositor.

Performance

Note of caution: despite the fact that typical DSL generation can take mere 50-100 microseconds, defining complex responses with DSL does carry a performance penantly of about 50% (we measured it!). Which generally means that generating multiple Composite objects in a loop using the DSL is probably not recommended, but doing it once per web/API request is completely reasonable.

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Added some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

Maintainers

Konstantin Gredeskoul (@kigster) and Paul Henry (@letuboy)

(c) 2013, All rights reserved, distributed under MIT license.

About

Composite pattern with DSL for constructing trees of objects in to render them in JSON.

Resources

Stars

19 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); GitHub - wanelo/compositor: Composite pattern with DSL for constructing trees of objects in to render them in JSON. · GitHub
Skip to content
This repository was archived by the owner on Dec 1, 2023. It is now read-only.

Repository files navigation

Compositor

Gem VersionBuild statusCode Climate

A Composite Design Pattern with a neat DSL for constructing trees of objects in order to render them as a Hash, and subsequently JSON. Used by Wanelo to generate all JSON API responses by compositing multiple objects together in API responses, converting to a plain ruby Hash (or an Array) and then using OJ gem to convert Hash to JSON.

The performance of this approach to generate JSON was faster than using RABL in our limited testing, although if performance is not an issue for you, RABL is still an excellent choice, and served us well for almost a year.

Installation

Add this line to your application's Gemfile:

gem 'compositor'

And then execute:

$ bundle

Or install it yourself as:

$ gem install compositor

Usage

For each model that needs a hash/json representation you need to create a ruby class that subclasses Composite::Leaf, adds some custom state that's important for rendering that object in addition to context, implements a proper constructor (see example), and finally implement the main rendering #to_hash method (used as the "operation" in the Composite pattern terminology).

The context variable is a reference to an object holding necessary helpers for generating JSON, for example Rails Controllers expose a view_context instance, which contains helper methods necessary to generate application URLs.

Outside of Rails application, context can be any other object holding application helpers or state. All subclasses of Compositor::Leaf such as UserCompositor inherit context attribute and accessors, and so can use the context in generating URLs, or calling any other application helpers.

We recommend that you place your Compositor classes in eg app/compositors/* directory, which defines one (or more than one) compositor(s) per model class you will be rendering.

# File: app/compositors/user_compositor.rbclassUserCompositor < Compositor::Leafattr_accessor:userdefinitialize(context,user,attrs={})super(context,attrs)self.user=userenddefto_hash{id: user.id,username: user.username,location: user.location,bio: user.bio,url: user.url,image_url: context.image_path(user.avatar),# using context to generate URL path from routes
...
}endend

You could create this class directly, as in

uc=UserCompositor.new(view_context,user,{})uc.to_hash# => returns a Hash representationuc.to_json# => calls to_hash, and then renders JSON

But constructing trees of objects that represent a complex API responses requires a lot more than that, such as constructing lists (arrays) or maps (hashes) of objects, and deciding which order they appear, and whether each inner Hash comes with a "root" element, such as :product => { :id => 1, ... } where :product is the root element.

So here is how to create a list of users in this way, but explicitly declaring classes:

compositor=ListCompositor.new(view_context,:collection=>@users.map{ |user|
UserCompositor.new(view_context,user,{:root=>true})},:root=>:users)

When calling to_hash on the top level compositor, we get:

:users=>{:user=>{id: 1234,
username: "kigster",location: "San Francisco",bio: "",url: "",image_url: "http://cdn-app.domain.com/kigster/avatar/200.jpg"},:user=>{id: 1235,username: "johnny",location: "Sunnyvale",bio: "",url: "",image_url: "http://cdn-app.domain.com/johnny/avatar/200.jpg"}}

So this is how you can assemple multiple compositors together without the DSL.

But the real power of this gem is in the additional DSL class, that dramatically simplifies definition of complex responses, as described below.

Using the DSL

UserCompositor class, when defined, automatically adds a user method to the DSL class, which effictively instantiates the new UserCompositor instance, passing the context into it automatically.

Using built-in MapCompositor and ListCompositor we can construct multiple objects into a larger hierarchy.

In the example below, an application is assumed to define StoreCompositorand ProductCompositor classes similar to UserCompositor, but wrapping Store and Product ActiveRecord models.

compositor=Compositor::DSL.create(context)domapdostore@store,root: :storeuser@user,root: :userlistcollection: @products,root: :productsdo |p|
productpendendend# now we can call to_hash or to_json on the compositor:putscompositor.to_hash# =>{:store=>{id: 12354,name: "amazon.com",url: "http://www.amazon.com",
..
},:user=>{id: 1234,username: "kigster",location: "San Francisco",bio: "",url: "",image_url: "http://cdn-app.domain.com/kigster/avatar/200.jpg"},:products=>[{id: 1234,:name=>"Awesome Product", ... },{id: 4325,:name=>"Another Awesome Product", ... }}}

Inside the list definition above, @products is a collection of Products, ActiveRecord objects, and the block maps each to a Compositor using product method, registered by ProductCompositor.

Instance Variables

One thing to note, is that when Compositor::DSL is used, the gem copies all instance variables from the context into the DSL instance, so in the above example instance variable @user was defined on view_context (by Rails, which copies them from the Controller instance), and so became automatically available inside DSL. Note that all instance variables must be defined before the DSL instance is created.

Method Names in the DSL

Compositor will extract the full name of the class and place that name into the DSL. For example, MyModule::UserCompositor will define a my_module_user method in the DSL. To create a method called user, the standard convention is to define a class in the global namespace called UserCompositor.

If you prefer to have your own Compositor class hierarchy, or just compositors that should not be added to the DSL, you can name the classes starting with Abstract, such as MyModule::AbstractCompositor.

Performance

Note of caution: despite the fact that typical DSL generation can take mere 50-100 microseconds, defining complex responses with DSL does carry a performance penantly of about 50% (we measured it!). Which generally means that generating multiple Composite objects in a loop using the DSL is probably not recommended, but doing it once per web/API request is completely reasonable.

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Added some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

Maintainers

Konstantin Gredeskoul (@kigster) and Paul Henry (@letuboy)

(c) 2013, All rights reserved, distributed under MIT license.

About

Composite pattern with DSL for constructing trees of objects in to render them in JSON.

Resources

Stars

19 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages