bespoke 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/bespoke.gemspec +26 -0
- data/lib/bespoke.rb +39 -0
- data/lib/bespoke/dsl.rb +9 -0
- data/lib/bespoke/exportable.rb +54 -0
- data/lib/bespoke/indexed_collection.rb +45 -0
- data/lib/bespoke/version.rb +3 -0
- data/readme.md +125 -0
- data/spec/bespoke_spec.rb +112 -0
- data/spec/exportable_spec.rb +18 -0
- data/spec/indexed_collection_spec.rb +7 -0
- data/spec/spec_helper.rb +3 -0
- metadata +141 -0
data/bespoke.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/bespoke/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.name = "bespoke"
|
6
|
+
gem.summary = "Bespoke does in-memory object joins using mustache templates"
|
7
|
+
gem.description = "Bespoke does in-memory object joins using mustache templates"
|
8
|
+
gem.authors = ['Duane Johnson']
|
9
|
+
gem.email = ['duane@instructure.com']
|
10
|
+
|
11
|
+
gem.files = %w[bespoke.gemspec readme.md]
|
12
|
+
gem.files += Dir.glob("lib/**/*")
|
13
|
+
gem.files += Dir.glob("spec/**/*")
|
14
|
+
|
15
|
+
gem.test_files = Dir.glob("spec/**/*")
|
16
|
+
gem.require_paths = ["lib"]
|
17
|
+
gem.version = Bespoke::VERSION
|
18
|
+
gem.required_ruby_version = '>= 1.9.0'
|
19
|
+
|
20
|
+
gem.add_development_dependency "bundler", ">= 1.0.0"
|
21
|
+
gem.add_development_dependency "rspec", "~> 2.6"
|
22
|
+
|
23
|
+
gem.add_dependency 'rake'
|
24
|
+
gem.add_dependency 'docile'
|
25
|
+
gem.add_dependency 'mustache'
|
26
|
+
end
|
data/lib/bespoke.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require "bespoke/version"
|
2
|
+
require "bespoke/indexed_collection"
|
3
|
+
require "bespoke/exportable"
|
4
|
+
|
5
|
+
class Bespoke
|
6
|
+
attr_reader :collection, :exports
|
7
|
+
|
8
|
+
def initialize(hash)
|
9
|
+
@collection = IndexedCollection.new
|
10
|
+
hash["index"].each_pair do |name, column|
|
11
|
+
@collection.index name, column
|
12
|
+
end
|
13
|
+
@exports = {}
|
14
|
+
hash["export"].each_pair do |output_name, exportable_configs|
|
15
|
+
outputs = @exports[output_name] = []
|
16
|
+
exportable_configs.each do |config|
|
17
|
+
config.each_pair do |collection_name, attrs|
|
18
|
+
outputs << (export = Exportable.new(collection_name))
|
19
|
+
(attrs["fields"] || {}).each_pair do |field, template|
|
20
|
+
export.field field, template
|
21
|
+
end
|
22
|
+
(attrs["joins"] || {}).each_pair do |join, template|
|
23
|
+
export.join join, template
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def add(type, object)
|
31
|
+
@collection.add(type, object)
|
32
|
+
end
|
33
|
+
|
34
|
+
def export(name, &block)
|
35
|
+
@exports[name].each do |e|
|
36
|
+
e.export(@collection.collections, &block)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/bespoke/dsl.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'csv'
|
2
|
+
require 'docile'
|
3
|
+
require 'mustache'
|
4
|
+
|
5
|
+
class Bespoke
|
6
|
+
class Error < StandardError; end
|
7
|
+
MissingTable = Class.new(Error)
|
8
|
+
MissingJoin = Class.new(Error)
|
9
|
+
|
10
|
+
class Exportable
|
11
|
+
attr_accessor :name, :fields, :joins
|
12
|
+
|
13
|
+
def initialize(name)
|
14
|
+
@name = name.to_sym
|
15
|
+
@fields = {}
|
16
|
+
@joins = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def headers
|
20
|
+
@fields.keys
|
21
|
+
end
|
22
|
+
|
23
|
+
def field(name, template_string)
|
24
|
+
fields[name] = Mustache::Template.new(template_string)
|
25
|
+
end
|
26
|
+
|
27
|
+
def join(name, key)
|
28
|
+
joins[name] = key
|
29
|
+
end
|
30
|
+
|
31
|
+
def export(hashes={}, &block)
|
32
|
+
raise "hashes missing #{@name.inspect} (of: #{hashes.keys.inspect})" unless hashes.has_key?(@name)
|
33
|
+
hashes[@name].map do |main_key, row|
|
34
|
+
context = { @name => row }
|
35
|
+
@joins.each_pair do |join_name, key|
|
36
|
+
if other_table = hashes[join_name.to_sym]
|
37
|
+
if other_table.has_key?(row[key])
|
38
|
+
context[join_name.to_sym] = other_table[row[key]]
|
39
|
+
else
|
40
|
+
raise MissingJoin, "Expected foreign key #{key} with value #{row[key]} in table #{join_name}"
|
41
|
+
end
|
42
|
+
else
|
43
|
+
raise MissingTable, "Expected #{join_name}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
fields.map do |name, template|
|
47
|
+
Mustache.render(template, context)
|
48
|
+
end.tap do |output_row|
|
49
|
+
yield output_row if block_given?
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'docile'
|
2
|
+
|
3
|
+
class Bespoke
|
4
|
+
class IndexedCollection
|
5
|
+
attr_reader :collections
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@index_columns = {}
|
9
|
+
@collections = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def proc_for_key(key)
|
13
|
+
return Proc.new{ |x| nil } unless key
|
14
|
+
key = key.first if key.is_a?(Array) and key.size == 1
|
15
|
+
if key.is_a?(Array)
|
16
|
+
Proc.new{ |x| key.map{ |k| (x[k] rescue x.send(k)) } }
|
17
|
+
else
|
18
|
+
Proc.new{ |x| (x[key] rescue x.send(key)) }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def index(collection_name, index_key_method=nil, &block)
|
23
|
+
col_sym = collection_name.to_sym
|
24
|
+
@index_columns[col_sym] = block || proc_for_key(index_key_method)
|
25
|
+
@collections[col_sym] = {}
|
26
|
+
end
|
27
|
+
|
28
|
+
def add(collection_name, object)
|
29
|
+
col_sym = collection_name.to_sym
|
30
|
+
key_from_object = @index_columns[col_sym]
|
31
|
+
key = key_from_object.call(object)
|
32
|
+
begin
|
33
|
+
@collections[col_sym][key] = object
|
34
|
+
rescue NoMethodError
|
35
|
+
raise "Can't find collection #{col_sym} with key #{key}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def find(collection_name, key)
|
40
|
+
if collection = @collections[collection_name.to_sym]
|
41
|
+
collection[key]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/readme.md
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
Bespoke
|
2
|
+
=======
|
3
|
+
|
4
|
+
Getting Started
|
5
|
+
---------------
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
config = {
|
9
|
+
"index" => {
|
10
|
+
"student" => ["id"],
|
11
|
+
"staff" => ["id"],
|
12
|
+
"school" => ["id"]
|
13
|
+
},
|
14
|
+
"export" => {
|
15
|
+
"users" => [
|
16
|
+
{
|
17
|
+
"student" => {
|
18
|
+
"fields" => {
|
19
|
+
"user_id" => "{{student.id}}",
|
20
|
+
"name" => "{{student.first_name}} {{student.last_name}}"
|
21
|
+
}
|
22
|
+
}
|
23
|
+
},
|
24
|
+
{
|
25
|
+
"staff" => {
|
26
|
+
"fields" => {
|
27
|
+
"user_id" => "{{staff.id}}",
|
28
|
+
"name" => "{{school.district}} Professor {{staff.last_name}}"
|
29
|
+
},
|
30
|
+
"joins" => {
|
31
|
+
"school" => "school_id"
|
32
|
+
}
|
33
|
+
}
|
34
|
+
}
|
35
|
+
],
|
36
|
+
"schools" => [
|
37
|
+
{
|
38
|
+
"school" => {
|
39
|
+
"fields" => {
|
40
|
+
"school_id" => "{{school.id}}",
|
41
|
+
"district" => "D:{{school.district}}"
|
42
|
+
},
|
43
|
+
"joins" => {
|
44
|
+
"school" => "school_id"
|
45
|
+
}
|
46
|
+
}
|
47
|
+
}
|
48
|
+
]
|
49
|
+
}
|
50
|
+
}
|
51
|
+
|
52
|
+
data = {
|
53
|
+
"student" => [
|
54
|
+
{ "id" => 1, "first_name" => "Eric", "last_name" => "Adams" },
|
55
|
+
{ "id" => 2, "first_name" => "Duane", "last_name" => "Johnson" },
|
56
|
+
{ "id" => 3, "first_name" => "Ken", "last_name" => "Romney" }
|
57
|
+
],
|
58
|
+
"staff" => [
|
59
|
+
{ "id" => 1, "last_name" => "Baxter", "school_id" => 1 },
|
60
|
+
{ "id" => 2, "last_name" => "Summer", "school_id" => 2 }
|
61
|
+
],
|
62
|
+
"school" => [
|
63
|
+
{ "id" => 1, "district" => "North" },
|
64
|
+
{ "id" => 2, "district" => "East" },
|
65
|
+
{ "id" => 3, "district" => "South" }
|
66
|
+
]
|
67
|
+
}
|
68
|
+
|
69
|
+
bespoke = Bespoke.new(config)
|
70
|
+
|
71
|
+
data.each_pair do |type, rows|
|
72
|
+
rows.each do |row|
|
73
|
+
bespoke.add type, row
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
rows = []
|
78
|
+
bespoke.export("users") do |row|
|
79
|
+
rows << row
|
80
|
+
end
|
81
|
+
|
82
|
+
# rows:
|
83
|
+
# ["1", "Eric Adams"]
|
84
|
+
# ["2", "Duane Johnson"]
|
85
|
+
# ["3", "Ken Romney"]
|
86
|
+
# ["1", "North Professor Baxter"]
|
87
|
+
# ["2", "East Professor Summer"]
|
88
|
+
|
89
|
+
```
|
90
|
+
|
91
|
+
DSL
|
92
|
+
---
|
93
|
+
|
94
|
+
Note that there is also an easy-to-use DSL if you don't want to use a json config. Use ```indexed_collection``` to declare an IndexedCollection and ```exportable``` to declare an Exportable.
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
require 'bespoke/dsl'
|
98
|
+
|
99
|
+
indexed = indexed_collection do
|
100
|
+
index :student, :id
|
101
|
+
index :staff, :id
|
102
|
+
index :school, :id
|
103
|
+
end
|
104
|
+
|
105
|
+
exports = {
|
106
|
+
users: [
|
107
|
+
exportable(:student) {
|
108
|
+
field :user_id, "{{student.id}}"
|
109
|
+
field :name, "{{student.first_name}} {{student.last_name}}"
|
110
|
+
},
|
111
|
+
exportable(:staff) {
|
112
|
+
field :user_id, "{{staff.id}}"
|
113
|
+
field :name, "{{school.district}} Professor {{staff.last_name}}"
|
114
|
+
}
|
115
|
+
],
|
116
|
+
schools: [
|
117
|
+
exportable(:school) {
|
118
|
+
field :school_id, "{{school.id}}"
|
119
|
+
field :district, "D:{{school.district}}"
|
120
|
+
|
121
|
+
join :school, :school_id
|
122
|
+
}
|
123
|
+
]
|
124
|
+
}
|
125
|
+
```
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
describe Bespoke do
|
4
|
+
let(:data) {
|
5
|
+
{
|
6
|
+
"student" => [
|
7
|
+
{ "id" => 1, "first_name" => "Eric", "last_name" => "Adams" },
|
8
|
+
{ "id" => 2, "first_name" => "Duane", "last_name" => "Johnson" },
|
9
|
+
{ "id" => 3, "first_name" => "Ken", "last_name" => "Romney" }
|
10
|
+
],
|
11
|
+
"staff" => [
|
12
|
+
{ "id" => 1, "last_name" => "Baxter", "school_id" => 1 },
|
13
|
+
{ "id" => 2, "last_name" => "Summer", "school_id" => 2 }
|
14
|
+
],
|
15
|
+
"school" => [
|
16
|
+
{ "id" => 1, "district" => "North" },
|
17
|
+
{ "id" => 2, "district" => "East" },
|
18
|
+
{ "id" => 3, "district" => "South" }
|
19
|
+
]
|
20
|
+
}
|
21
|
+
}
|
22
|
+
let(:config) {
|
23
|
+
{
|
24
|
+
"index" => {
|
25
|
+
"student" => ["id"],
|
26
|
+
"staff" => ["id"],
|
27
|
+
"school" => ["id"]
|
28
|
+
},
|
29
|
+
"export" => {
|
30
|
+
"users" => [
|
31
|
+
{
|
32
|
+
"student" => {
|
33
|
+
"fields" => {
|
34
|
+
"user_id" => "{{student.id}}",
|
35
|
+
"name" => "{{student.first_name}} {{student.last_name}}"
|
36
|
+
}
|
37
|
+
}
|
38
|
+
},
|
39
|
+
{
|
40
|
+
"staff" => {
|
41
|
+
"fields" => {
|
42
|
+
"user_id" => "{{staff.id}}",
|
43
|
+
"name" => "{{school.district}} Professor {{staff.last_name}}"
|
44
|
+
},
|
45
|
+
"joins" => {
|
46
|
+
"school" => "school_id"
|
47
|
+
}
|
48
|
+
}
|
49
|
+
}
|
50
|
+
],
|
51
|
+
"schools" => [
|
52
|
+
{
|
53
|
+
"school" => {
|
54
|
+
"fields" => {
|
55
|
+
"school_id" => "{{school.id}}",
|
56
|
+
"district" => "D:{{school.district}}"
|
57
|
+
}
|
58
|
+
}
|
59
|
+
}
|
60
|
+
]
|
61
|
+
}
|
62
|
+
}
|
63
|
+
}
|
64
|
+
let(:bespoke) { Bespoke.new(config) }
|
65
|
+
|
66
|
+
it "initializes" do
|
67
|
+
Bespoke.new(config)
|
68
|
+
end
|
69
|
+
|
70
|
+
context "with loaded data" do
|
71
|
+
before do
|
72
|
+
data.each_pair do |type, rows|
|
73
|
+
rows.each do |row|
|
74
|
+
bespoke.add type, row
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
it "creates a collection in memory" do
|
80
|
+
bespoke.collection.collections.should == {
|
81
|
+
:student => {
|
82
|
+
1 => {"id"=>1, "first_name"=>"Eric", "last_name"=>"Adams"},
|
83
|
+
2 => {"id"=>2, "first_name"=>"Duane", "last_name"=>"Johnson"},
|
84
|
+
3 => {"id"=>3, "first_name"=>"Ken", "last_name"=>"Romney"}
|
85
|
+
},
|
86
|
+
:staff => {
|
87
|
+
1 => {"id"=>1, "last_name"=>"Baxter", "school_id"=>1},
|
88
|
+
2 => {"id"=>2, "last_name"=>"Summer", "school_id"=>2}
|
89
|
+
},
|
90
|
+
:school => {
|
91
|
+
1 => {"id"=>1, "district"=>"North"},
|
92
|
+
2 => {"id"=>2, "district"=>"East"},
|
93
|
+
3 => {"id"=>3, "district"=>"South"}
|
94
|
+
}
|
95
|
+
}
|
96
|
+
end
|
97
|
+
|
98
|
+
it "exports" do
|
99
|
+
rows = []
|
100
|
+
bespoke.export("users") do |row|
|
101
|
+
rows << row
|
102
|
+
end
|
103
|
+
rows.should == [
|
104
|
+
["1", "Eric Adams"],
|
105
|
+
["2", "Duane Johnson"],
|
106
|
+
["3", "Ken Romney"],
|
107
|
+
["1", "North Professor Baxter"],
|
108
|
+
["2", "East Professor Summer"]
|
109
|
+
]
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
describe Bespoke::Exportable do
|
4
|
+
let(:export) { Bespoke::Exportable.new(:test) }
|
5
|
+
|
6
|
+
it "initializes" do
|
7
|
+
Bespoke::Exportable.new(:test)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "exports templated rows" do
|
11
|
+
export.field(:column, "{{test.one}}-{{test.two}}")
|
12
|
+
data = export.export({:test => {
|
13
|
+
1 => {:one => 1, :two => 2},
|
14
|
+
2 => {:one => 5, :two => 10}
|
15
|
+
}})
|
16
|
+
data.should == [["1-2"], ["5-10"]]
|
17
|
+
end
|
18
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bespoke
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Duane Johnson
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-12-31 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.0.0
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 1.0.0
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rspec
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '2.6'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '2.6'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rake
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: docile
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: mustache
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :runtime
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
description: Bespoke does in-memory object joins using mustache templates
|
95
|
+
email:
|
96
|
+
- duane@instructure.com
|
97
|
+
executables: []
|
98
|
+
extensions: []
|
99
|
+
extra_rdoc_files: []
|
100
|
+
files:
|
101
|
+
- bespoke.gemspec
|
102
|
+
- readme.md
|
103
|
+
- lib/bespoke/dsl.rb
|
104
|
+
- lib/bespoke/exportable.rb
|
105
|
+
- lib/bespoke/indexed_collection.rb
|
106
|
+
- lib/bespoke/version.rb
|
107
|
+
- lib/bespoke.rb
|
108
|
+
- spec/bespoke_spec.rb
|
109
|
+
- spec/exportable_spec.rb
|
110
|
+
- spec/indexed_collection_spec.rb
|
111
|
+
- spec/spec_helper.rb
|
112
|
+
homepage:
|
113
|
+
licenses: []
|
114
|
+
post_install_message:
|
115
|
+
rdoc_options: []
|
116
|
+
require_paths:
|
117
|
+
- lib
|
118
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
119
|
+
none: false
|
120
|
+
requirements:
|
121
|
+
- - ! '>='
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: 1.9.0
|
124
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
125
|
+
none: false
|
126
|
+
requirements:
|
127
|
+
- - ! '>='
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: '0'
|
130
|
+
requirements: []
|
131
|
+
rubyforge_project:
|
132
|
+
rubygems_version: 1.8.23
|
133
|
+
signing_key:
|
134
|
+
specification_version: 3
|
135
|
+
summary: Bespoke does in-memory object joins using mustache templates
|
136
|
+
test_files:
|
137
|
+
- spec/bespoke_spec.rb
|
138
|
+
- spec/exportable_spec.rb
|
139
|
+
- spec/indexed_collection_spec.rb
|
140
|
+
- spec/spec_helper.rb
|
141
|
+
has_rdoc:
|