bespoke 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -0,0 +1,9 @@
1
+ require "bespoke"
2
+
3
+ def indexed_collection(&block)
4
+ Docile.dsl_eval(Bespoke::IndexedCollection.new, &block)
5
+ end
6
+
7
+ def exportable(name, &block)
8
+ Docile.dsl_eval(Bespoke::Exportable.new(name), &block)
9
+ end
@@ -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
@@ -0,0 +1,3 @@
1
+ class Bespoke
2
+ VERSION = '0.1.0' unless defined?(Bespoke::VERSION)
3
+ end
@@ -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
@@ -0,0 +1,7 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe Bespoke::IndexedCollection do
4
+ it "initializes" do
5
+ Bespoke::IndexedCollection.new
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ $LOAD_PATH << File.expand_path(File.join(*%w[ .. lib ]), File.dirname(__FILE__))
2
+
3
+ require 'bespoke'
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: