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.
@@ -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: