Skip to content
This repository was archived by the owner on Dec 1, 2023. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ was defined on ```view_context``` (by Rails, which copies them from the Controll
so became automatically available inside DSL. Note that all instance variables must be
defined *before* the DSL instance is created.

### Method Name Collisions in the DSL
### Method Name Collisions and Override in the DSL

Because DSL uses only the last word of the class name (eg, ```user``` for a class named ```MyModule::UserCompositor```),
there is a possibility of name collisions. In order to prevent that, Compositor will detect if a DSL method is already
Expand All @@ -182,6 +182,46 @@ defined and throw exception if another class tries to redefine it.
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```.

The option does exist to Override the DSL method name for named leafs. This will allow you to for example have two or more
User classes defined for composites in a hierarchy, each with a different logical name. For example:

```ruby
# Will get the 'user' dsl method name
class User < Compositor::Leaf
# methods omitted
end

# Will get v0_user dsl method name
module Api
module V0
class User < Compositor::NamedLeaf("v0_user")
# methods omitted
end
end
end

# Will get v1_user method name
module Api
module V1
class User < Compositor::NamedLeaf("v1_user")
# methods omitted
end
end
end
```

Please note, if you try to use the same named leaf logical name an error will occur. For example:

```ruby
class Foo < Compositor::NamedLeaf("baz")
# methods omitted
end

class Bar < Compositor::NamedLeaf("baz")
# methods omitted
end
```

### Performance

Note of caution: despite the fact that typical DSL generation can take mere 50-100 microseconds, defining complex responses
Expand Down
12 changes: 8 additions & 4 deletions lib/compositor/base.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module Compositor
class MethodAlreadyDefinedError < RuntimeError; end
class MethodAlreadyDefinedError < RuntimeError;
end

class Base
attr_reader :attrs
Expand Down Expand Up @@ -37,21 +38,24 @@ def root_class_name
self.class.root_class_name(self.class)
end


def self.root_class_name(klazz)
return klazz.dsl_override if klazz.respond_to?(:dsl_override)
return nil if klazz.name.nil? # Anonymous classes will have nil name, we do not want to have them included
klazz.name.gsub(/(.*::)|(Compositor$)/, '').underscore
end

def self.inherited(subclass)
method_name = root_class_name(subclass)
unless method_name.eql?("base") || method_name.start_with?("abstract")
unless (method_name.nil? || method_name.eql?("base") || method_name.start_with?("abstract"))
# check if it's already defined
if Compositor::DSL.instance_methods.include?(method_name.to_sym)
raise MethodAlreadyDefinedError.new("Method #{method_name} is already defined on the DSL class.")
end
Compositor::DSL.send(:define_method, method_name) do |*args, &block|
subclass.
new(@context, *args).
dsl(self, &block)
new(@context, *args).
dsl(self, &block)
end
end
end
Expand Down
27 changes: 27 additions & 0 deletions lib/compositor/leaf.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,31 @@ def dsl(dsl)
end
end
end

# Create a named leaf, To override the dsl name at the class level
# Note we use an interstitial anonymous class since the DSL builder relies
# on self.inherited. Could get more in terms of flexibility if this used
# module inheritance instead. But this works fine with snytax borrows from
# Camping and Sequal.
#
# Example:
#
#module Motley
# class Crew < Compositor::NamedLeaf("motley_crew")
# end
#end
#
#module TwoLive
# class Crew < Compositor::NamedLeaf("two_live_crew")
# end
#end


def self.NamedLeaf name
Class.new(Compositor::Leaf) do
eigenclass = class << self; self; end;
eigenclass.send(:define_method, :dsl_override) { name }
end
end

end
43 changes: 43 additions & 0 deletions spec/compositor/base_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,48 @@ class AbstractUserCompositor < Compositor::Leaf
Compositor::DSL.instance_methods.should_not include(:abstract_user)
end

describe "#override_root_class_name" do

it "override does work" do
lambda {

class Crew < Compositor::NamedLeaf "crew"
end

module Motley
class Crew < Compositor::NamedLeaf "motley_crew"
end
end

module TwoLive
class Crew < Compositor::NamedLeaf "two_live_crew"
end
end

module WorldClassWrecking
class Crew < Compositor::NamedLeaf "world_class_wrecking_crew"
end
end
}.call

[:crew, :motley_crew, :world_class_wrecking_crew].each do |dsl_name|
Compositor::DSL.instance_methods.should include(dsl_name)
end
end

it "Fails with same override" do
expect do
lambda {
class Gang < Compositor::NamedLeaf "gang"
end

module KoolAndThe
class Gang < Compositor::NamedLeaf "gang"
end
end
}.call
end.to raise_error
end
end
end
end
63 changes: 49 additions & 14 deletions spec/compositor/hash_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@

it 'returns the generated map' do
expected = {
tests: {
num1: {number: 1},
num2: {number: 2},
num3: {number: 3}
}
tests: {
num1: {number: 1},
num2: {number: 2},
num3: {number: 3}
}
}

dsl = Compositor::DSL.create(context)
Expand All @@ -24,15 +24,15 @@

it 'returns the generated deeply nested map without explicit receiver' do
expected = {
tests: {
num1: {number: 1},
num2: {number: 2},
num3: {number: 3},
stuff: [
{number: 10},
{number: 11}
]
}
tests: {
num1: {number: 1},
num2: {number: 2},
num3: {number: 3},
stuff: [
{number: 10},
{number: 11}
]
}
}

dsl = Compositor::DSL.create(context)
Expand All @@ -58,4 +58,39 @@
{}.should == dsl.to_hash
end
end

describe "Same top level class names" do
it "supports both classes" do
expected = {
:tests =>
{
:employee => {
:name => "squiggy",
:salary => 1000,
:status => "baller"
},
:peasent => {
:name => "bob",
:age => 12
},
:internal_ballers => [
{:name => "squiggy", :salary => 1000000, :status => "baller"},
{:name => "squiggy", :salary => 2000000, :status => "baller"}
]
}
}

dsl_hash = Compositor::DSL.create(context) do
map root: :tests do
internal_person 1000, root: :employee
api_person root: :peasent
list :root => :internal_ballers do
internal_person 1000000
internal_person 2000000
end
end
end.to_hash
expected.should == dsl_hash
end
end
end
57 changes: 57 additions & 0 deletions spec/support/sample_dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,60 @@ def to_hash
}
end
end

module PublicApi
class Person < Compositor::NamedLeaf("api_person")
def to_hash
with_root_element do
{
name: "bob",
age: 12
}
end
end
end
end

module PrivateApi
class Person < Compositor::NamedLeaf("internal_person")
attr_accessor :salary

def initialize(view_context, salary, attrs = {})
super(view_context, {salary: salary}.merge!(attrs))
end

def to_hash
with_root_element do
{
name: "squiggy",
salary: salary,
status: "baller"
}
end
end
end
end


module Api
module V0
class User < Compositor::NamedLeaf("v0_user")
def to_hash
# hash of stuff for v0 rep of user
end
end
end
end

module Api
module V1
class User < Compositor::NamedLeaf("v1_user")
def to_hash
# hash of stuff v1 rep of user
end
end
end
end