bespoke 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -19,8 +19,11 @@ Gem::Specification.new do |gem|
19
19
 
20
20
  gem.add_development_dependency "bundler", ">= 1.0.0"
21
21
  gem.add_development_dependency "rspec", "~> 2.6"
22
+ gem.add_development_dependency "debugger"
22
23
 
23
24
  gem.add_dependency 'rake'
24
25
  gem.add_dependency 'docile'
25
26
  gem.add_dependency 'mustache'
27
+ gem.add_dependency 'sqlite3'
28
+ gem.add_dependency 'sequel'
26
29
  end
@@ -1,39 +1,6 @@
1
1
  require "bespoke/version"
2
- require "bespoke/indexed_collection"
3
- require "bespoke/exportable"
4
2
 
5
- class Bespoke
6
- attr_reader :collection, :exports
7
-
8
- def initialize(hash, logger=nil)
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, logger))
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
3
+ require "bespoke/template"
4
+ require "bespoke/xsltproc"
5
+ require "bespoke/projection"
6
+ require "bespoke/join"
@@ -1,9 +1,6 @@
1
1
  require "bespoke"
2
+ require "docile"
2
3
 
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
4
+ def projection(name, xpath_root=nil, &block)
5
+ Docile.dsl_eval(Bespoke::Projection.new(name, xpath_root), &block)
6
+ end
@@ -0,0 +1,57 @@
1
+ require 'csv'
2
+ require 'sequel'
3
+ require 'logger'
4
+
5
+ module Bespoke
6
+ class Join
7
+ def initialize(projections, log=nil)
8
+ @projections = projections
9
+ @log = log || Logger.new($stderr)
10
+ end
11
+
12
+ def database
13
+ return @database if @database
14
+ @database = Sequel.sqlite
15
+ create_tables
16
+ @database
17
+ end
18
+
19
+ def load_csv(tablename, filepath)
20
+ table = database[tablename.to_sym]
21
+ CSV.foreach(filepath, headers: true) do |row|
22
+ begin
23
+ table.insert(row.to_hash)
24
+ rescue Sequel::UniqueConstraintViolation => e
25
+ @log.warn "Unable to insert row #{row.inspect}: #{e}"
26
+ end
27
+ end
28
+ end
29
+
30
+ def query(sql)
31
+ database[sql]
32
+ end
33
+
34
+ protected
35
+
36
+ def create_tables
37
+ @projections.each { |prj| create_table(prj) }
38
+ end
39
+
40
+ def create_table(prj)
41
+ database.drop_table?(prj.name)
42
+ database.create_table(prj.name) do
43
+ prj.fields.each do |field|
44
+ if field.name.to_s == "ref_id"
45
+ column :ref_id, :string, :primary_key => true
46
+ else
47
+ column field.name, field.type
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ end
54
+ end
55
+
56
+
57
+
@@ -0,0 +1,35 @@
1
+ module Bespoke
2
+ class TypedField
3
+ attr_reader :name, :type, :xpath
4
+
5
+ # @name The name of the column or field
6
+ # @type The Sqlite3 type of the data
7
+ # @xpath The xpath query that retrieves the data from XML
8
+ def initialize(name, type, xpath)
9
+ @name = name
10
+ @type = type
11
+ @xpath = xpath
12
+ end
13
+ end
14
+
15
+ class Projection
16
+ attr_reader :name, :fields, :xpath_root
17
+
18
+ def initialize(name, xpath_root=nil)
19
+ @name = name.to_s
20
+ @xpath_root = xpath_root
21
+ @fields = []
22
+ end
23
+
24
+ def field(name, type=:string, xpath=nil)
25
+ @fields << TypedField.new(name, type, xpath)
26
+ end
27
+
28
+ def xpath_hash
29
+ pairs = @fields.map do |field|
30
+ [field.name, field.xpath]
31
+ end.flatten(1)
32
+ Hash[*pairs]
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,45 @@
1
+ require 'mustache'
2
+ require 'tempfile'
3
+
4
+ module Bespoke
5
+ class Template
6
+ attr_reader :template_file
7
+
8
+ def initialize(template_file)
9
+ @template_file = template_file
10
+ end
11
+
12
+ def render(object_type, projection)
13
+ fields = projection_fields(projection)
14
+ Mustache.render(template,
15
+ :object_type => object_type,
16
+ :projection => fields)
17
+ end
18
+
19
+ protected
20
+ def template
21
+ @template ||= Mustache::Template.new(File.read(@template_file))
22
+ end
23
+
24
+ def default_function
25
+ 'escapeQuote'
26
+ end
27
+
28
+ def projection_pair(value)
29
+ value = Array(value)
30
+ [value.first, value[1] || default_function]
31
+ end
32
+
33
+ def projection_fields(projection)
34
+ projection.each_with_index.map do |(field, value), i|
35
+ selector, function = projection_pair(value)
36
+ {
37
+ :field => field.to_s,
38
+ :function => function,
39
+ :selector => selector,
40
+ :last => i == (projection.size - 1)
41
+ }
42
+ end
43
+ end
44
+ end
45
+ end
@@ -1,3 +1,3 @@
1
- class Bespoke
2
- VERSION = '0.1.2' unless defined?(Bespoke::VERSION)
1
+ module Bespoke
2
+ VERSION = '0.2.0' unless defined?(Bespoke::VERSION)
3
3
  end
@@ -0,0 +1,83 @@
1
+ require 'logger'
2
+ require 'open3'
3
+ require 'stringio'
4
+
5
+ module Bespoke
6
+ class XSLTProc
7
+ def initialize(xslt, logger=nil)
8
+ @xslt_file = write_tmp_file(xslt)
9
+ @logger = logger || Logger.new(STDERR)
10
+ end
11
+
12
+ def write_tmp_file(xslt)
13
+ file = Tempfile.new('bespoke')
14
+ file.write(xslt)
15
+ return file.path
16
+ ensure
17
+ file.close
18
+ end
19
+
20
+ def transform(text)
21
+ out_file = StringIO.new("", 'w')
22
+ in_file = StringIO.new(text, 'r')
23
+ transform_file(in_file, out_file)
24
+ out_file.string
25
+ end
26
+
27
+ def transform_file(in_file, out_file)
28
+ cmd = ["xsltproc"]
29
+ cmd << @xslt_file
30
+
31
+ if in_file.respond_to?(:read)
32
+ cmd << "-" # use stdin
33
+ else
34
+ cmd << in_file
35
+ end
36
+
37
+ need_to_close_out_file = false
38
+ if out_file.is_a?(String)
39
+ need_to_close_out_file = true
40
+ out_file_handle = File.open(out_file, "a+")
41
+ elsif out_file.respond_to?(:write)
42
+ out_file_handle = out_file
43
+ else
44
+ raise "Need a file handle or file name for data transform destination"
45
+ end
46
+
47
+ threads = []
48
+ Open3.popen3(*cmd) do |stdin, stdout, stderr, wait_thr|
49
+ if in_file.respond_to?(:read)
50
+ # Send data
51
+ threads << Thread.new do
52
+ stdin.write in_file.read(2**16) until in_file.eof?
53
+ stdin.close
54
+ end
55
+ else
56
+ # No need to send data, we're passing in the filename
57
+ stdin.close
58
+ end
59
+
60
+ # Capture data
61
+ threads << Thread.new do
62
+ until (line = stdout.gets).nil? do
63
+ out_file_handle.write line
64
+ end
65
+ end
66
+
67
+ # Capture error messages
68
+ threads << Thread.new do
69
+ until (line = stderr.gets).nil? do
70
+ @logger.warn line
71
+ end
72
+ end
73
+
74
+ threads.each{ |t| t.join }
75
+ wait_thr.join
76
+ end
77
+ rescue SystemCallError => e
78
+ @logger.error e
79
+ ensure
80
+ out_file_handle.close if need_to_close_out_file
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,12 @@
1
+ <books>
2
+ <book>
3
+ <id>1</id>
4
+ <title>One</title>
5
+ <author>Deity</author>
6
+ </book>
7
+ <book>
8
+ <id>2</id>
9
+ <title>Two</title>
10
+ <author>Lovers</author>
11
+ </book>
12
+ </books>
@@ -0,0 +1,17 @@
1
+ <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2
+
3
+ <xsl:output method="text" standalone="no"/>
4
+
5
+ <xsl:param name="delim" select="','" />
6
+ <xsl:param name="quote" select="'&quot;'" />
7
+ <xsl:param name="break" select="'&#xA;'" />
8
+
9
+ <xsl:template match="/books">
10
+ <xsl:for-each select="book">
11
+ <xsl:value-of select="$quote" /><xsl:value-of select="id"/><xsl:value-of select="$quote" /><xsl:value-of select="$delim" />
12
+ <xsl:value-of select="$quote" /><xsl:value-of select="title"/><xsl:value-of select="$quote" /><xsl:value-of select="$delim" />
13
+ <xsl:value-of select="$quote" /><xsl:value-of select="author"/><xsl:value-of select="$quote" /><xsl:value-of select="$break" />
14
+ </xsl:for-each>
15
+ </xsl:template>
16
+
17
+ </xsl:stylesheet>
@@ -0,0 +1,3 @@
1
+ id,name
2
+ 1,bitcoin
3
+ 2,nxt
@@ -0,0 +1,101 @@
1
+ <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:sif="http://www.sifinfo.org/infrastructure/2.x" xmlns:csv="csv:csv">
2
+ <xsl:output method="text" encoding="utf-8"/>
3
+
4
+ <xsl:param name="delim" select="','"/>
5
+ <xsl:param name="quote" select="'&quot;'"/>
6
+ <xsl:param name="break" select="'&#xA;'"/>
7
+
8
+ <xsl:template match="/">
9
+ <xsl:apply-templates select="//sif:StudentPersonal"/>
10
+ </xsl:template>
11
+
12
+
13
+ <xsl:template match="sif:StudentPersonal">
14
+ <xsl:value-of select="$quote"/>
15
+ <xsl:call-template name="escapeQuote">
16
+ <xsl:with-param name="text" select="@RefId"/>
17
+ </xsl:call-template>
18
+ <xsl:value-of select="$quote"/>
19
+ <xsl:value-of select="$delim"/>
20
+ <xsl:value-of select="$quote"/>
21
+ <xsl:call-template name="escapeQuote">
22
+ <xsl:with-param name="text" select="sif:LocalId"/>
23
+ </xsl:call-template>
24
+ <xsl:value-of select="$quote"/>
25
+ <xsl:value-of select="$delim"/>
26
+ <xsl:value-of select="$quote"/>
27
+ <xsl:call-template name="escapeQuote">
28
+ <xsl:with-param name="text" select="sif:Name/sif:FirstName"/>
29
+ </xsl:call-template>
30
+ <xsl:value-of select="$quote"/>
31
+ <xsl:value-of select="$delim"/>
32
+ <xsl:value-of select="$quote"/>
33
+ <xsl:call-template name="escapeQuote">
34
+ <xsl:with-param name="text" select="sif:Name[@Type='04']/sif:LastName"/>
35
+ </xsl:call-template>
36
+ <xsl:value-of select="$quote"/>
37
+ <xsl:value-of select="$delim"/>
38
+ <xsl:value-of select="$quote"/>
39
+ <xsl:call-template name="split-get-first">
40
+ <xsl:with-param name="text" select="sif:EmailList/sif:Email[@Type='Primary']"/>
41
+ </xsl:call-template>
42
+ <xsl:value-of select="$quote"/>
43
+ <xsl:value-of select="$break"/>
44
+ </xsl:template>
45
+
46
+ <!-- Function that prefixes any quotes (") in a string with a backslash -->
47
+ <xsl:template match="text/text()" name="escapeQuote">
48
+ <xsl:param name="text" select="."/>
49
+
50
+ <xsl:if test="string-length($text) > 0">
51
+ <xsl:value-of select="substring-before(concat($text, $quote), $quote)"/>
52
+
53
+ <xsl:if test="contains($text, $quote)">
54
+ <xsl:text>\</xsl:text><xsl:value-of select="$quote"/>
55
+ <xsl:call-template name="escapeQuote">
56
+ <xsl:with-param name="text" select="substring-after($text, $quote)"/>
57
+ </xsl:call-template>
58
+ </xsl:if>
59
+ </xsl:if>
60
+ </xsl:template>
61
+
62
+ <!-- Function to split on commas (or any separator), returning a list of elements -->
63
+ <xsl:template match="text/text()" name="split">
64
+ <xsl:param name="text" select="."/>
65
+ <xsl:param name="separator" select="','"/>
66
+ <xsl:choose>
67
+ <xsl:when test="not(contains($text, $separator))">
68
+ <xsl:call-template name="escapeQuote">
69
+ <xsl:with-param name="text" select="normalize-space($text)"/>
70
+ </xsl:call-template>
71
+ </xsl:when>
72
+ <xsl:otherwise>
73
+ <item>
74
+ <xsl:value-of select="normalize-space(substring-before($text, $separator))"/>
75
+ </item>
76
+ <xsl:call-template name="split">
77
+ <xsl:with-param name="text" select="substring-after($text, $separator)"/>
78
+ </xsl:call-template>
79
+ </xsl:otherwise>
80
+ </xsl:choose>
81
+ </xsl:template>
82
+
83
+ <!-- Function like "split", but returns just the first split element -->
84
+ <xsl:template match="text/text()" name="split-get-first">
85
+ <xsl:param name="text" select="."/>
86
+ <xsl:param name="separator" select="','"/>
87
+ <xsl:choose>
88
+ <xsl:when test="not(contains($text, $separator))">
89
+ <item>
90
+ <xsl:value-of select="normalize-space($text)"/>
91
+ </item>
92
+ </xsl:when>
93
+ <xsl:otherwise>
94
+ <xsl:call-template name="escapeQuote">
95
+ <xsl:with-param name="text" select="normalize-space(substring-before($text, $separator))"/>
96
+ </xsl:call-template>
97
+ </xsl:otherwise>
98
+ </xsl:choose>
99
+ </xsl:template>
100
+
101
+ </xsl:stylesheet>
@@ -0,0 +1,85 @@
1
+ <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:sif="http://www.sifinfo.org/infrastructure/2.x" xmlns:csv="csv:csv">
2
+ <xsl:output method="text" encoding="utf-8"/>
3
+
4
+ <xsl:param name="delim" select="','"/>
5
+ <xsl:param name="quote" select="'&quot;'"/>
6
+ <xsl:param name="break" select="'&#xA;'"/>
7
+
8
+ <xsl:template match="/">
9
+ <xsl:apply-templates select="//{{object_type}}"/>
10
+ </xsl:template>
11
+
12
+
13
+ <xsl:template match="{{object_type}}">
14
+ {{!-- Loop through each projection field to generate an xslt transform for the column --}}
15
+ {{#projection}}
16
+ <xsl:value-of select="$quote"/>
17
+ <xsl:call-template name="{{function}}">
18
+ <xsl:with-param name="text" select="{{selector}}"/>
19
+ </xsl:call-template>
20
+ <xsl:value-of select="$quote"/>
21
+ {{^last}}
22
+ <xsl:value-of select="$delim"/>
23
+ {{/last}}
24
+ {{#last}}
25
+ <xsl:value-of select="$break"/>
26
+ {{/last}}
27
+ {{/projection}}
28
+ </xsl:template>
29
+
30
+ <!-- Function that prefixes any quotes (") in a string with a backslash -->
31
+ <xsl:template match="text/text()" name="escapeQuote">
32
+ <xsl:param name="text" select="."/>
33
+
34
+ <xsl:if test="string-length($text) > 0">
35
+ <xsl:value-of select="substring-before(concat($text, $quote), $quote)"/>
36
+
37
+ <xsl:if test="contains($text, $quote)">
38
+ <xsl:text>\</xsl:text><xsl:value-of select="$quote"/>
39
+ <xsl:call-template name="escapeQuote">
40
+ <xsl:with-param name="text" select="substring-after($text, $quote)"/>
41
+ </xsl:call-template>
42
+ </xsl:if>
43
+ </xsl:if>
44
+ </xsl:template>
45
+
46
+ <!-- Function to split on commas (or any separator), returning a list of elements -->
47
+ <xsl:template match="text/text()" name="split">
48
+ <xsl:param name="text" select="."/>
49
+ <xsl:param name="separator" select="','"/>
50
+ <xsl:choose>
51
+ <xsl:when test="not(contains($text, $separator))">
52
+ <xsl:call-template name="escapeQuote">
53
+ <xsl:with-param name="text" select="normalize-space($text)"/>
54
+ </xsl:call-template>
55
+ </xsl:when>
56
+ <xsl:otherwise>
57
+ <item>
58
+ <xsl:value-of select="normalize-space(substring-before($text, $separator))"/>
59
+ </item>
60
+ <xsl:call-template name="split">
61
+ <xsl:with-param name="text" select="substring-after($text, $separator)"/>
62
+ </xsl:call-template>
63
+ </xsl:otherwise>
64
+ </xsl:choose>
65
+ </xsl:template>
66
+
67
+ <!-- Function like "split", but returns just the first split element -->
68
+ <xsl:template match="text/text()" name="split-get-first">
69
+ <xsl:param name="text" select="."/>
70
+ <xsl:param name="separator" select="','"/>
71
+ <xsl:choose>
72
+ <xsl:when test="not(contains($text, $separator))">
73
+ <item>
74
+ <xsl:value-of select="normalize-space($text)"/>
75
+ </item>
76
+ </xsl:when>
77
+ <xsl:otherwise>
78
+ <xsl:call-template name="escapeQuote">
79
+ <xsl:with-param name="text" select="normalize-space(substring-before($text, $separator))"/>
80
+ </xsl:call-template>
81
+ </xsl:otherwise>
82
+ </xsl:choose>
83
+ </xsl:template>
84
+
85
+ </xsl:stylesheet>
@@ -0,0 +1,4 @@
1
+ id,one_id,description
2
+ 1,1,Cryptocurrency
3
+ 2,1,Investment
4
+ 3,2,Alternate Cryptocurrency
@@ -0,0 +1,44 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe Bespoke::Join do
4
+ let(:projections) {
5
+ [
6
+ projection(:one) {
7
+ field :id, :integer
8
+ field :name, :string
9
+ },
10
+ projection(:two) {
11
+ field :id, :integer
12
+ field :one_id, :integer
13
+ field :description, :string
14
+ }
15
+ ]
16
+ }
17
+ let(:join) { Bespoke::Join.new(projections) }
18
+
19
+ context "table 'one'" do
20
+ before { join.load_csv('one', fixture('one.csv')) }
21
+
22
+ it "loads csv" do
23
+ join.database[:one].count.should == 2
24
+ end
25
+
26
+ context "and table 'two'" do
27
+ before { join.load_csv('two', fixture('two.csv')) }
28
+
29
+ it "loads csv" do
30
+ join.database[:two].count.should == 3
31
+ end
32
+
33
+ it "joins" do
34
+ sql = "SELECT name, description FROM one LEFT JOIN two ON one.id = two.one_id"
35
+ join.query(sql).map { |row| row }.
36
+ should == [
37
+ {:name=>"bitcoin", :description=>"Cryptocurrency"},
38
+ {:name=>"bitcoin", :description=>"Investment"},
39
+ {:name=>"nxt", :description=>"Alternate Cryptocurrency"}
40
+ ]
41
+ end
42
+ end
43
+ end
44
+ end
@@ -1,3 +1,8 @@
1
1
  $LOAD_PATH << File.expand_path(File.join(*%w[ .. lib ]), File.dirname(__FILE__))
2
2
 
3
- require 'bespoke'
3
+ require "bespoke"
4
+ require "bespoke/dsl"
5
+
6
+ def fixture(*fileparts)
7
+ File.join(File.dirname(__FILE__), 'fixtures', *fileparts)
8
+ end
@@ -0,0 +1,36 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe Bespoke::Template do
4
+ let(:projection) {
5
+ {
6
+ :ref_id => "@RefId",
7
+ :local_id => "sif:LocalId",
8
+ :first_name => "sif:Name/sif:FirstName",
9
+ :last_name => "sif:Name[@Type='04']/sif:LastName",
10
+ :email => ["sif:EmailList/sif:Email[@Type='Primary']", "split-get-first"]
11
+ }
12
+ }
13
+ let(:xslt_mustache) { fixture('sif.xslt.mustache') }
14
+ let(:xslt) { fixture('sif.student_personal.xslt') }
15
+ let(:tbuild) { Bespoke::Template.new(xslt_mustache) }
16
+ let(:fields) { tbuild.send(:projection_fields, projection) }
17
+
18
+ it "transforms" do
19
+ tbuild.render("sif:StudentPersonal", projection).should == File.read(xslt)
20
+ end
21
+
22
+ it "last element is the only element where 'last' is true" do
23
+ first = fields[0..-2].map{ |f| f[:last] }
24
+ first.all?{ |x| x == false }.should be_true
25
+ fields[-1][:last].should be_true
26
+ end
27
+
28
+ it "has optional function field" do
29
+ fields[-1].should == {
30
+ :field => "email",
31
+ :function => "split-get-first",
32
+ :selector => "sif:EmailList/sif:Email[@Type='Primary']",
33
+ :last => true
34
+ }
35
+ end
36
+ end
@@ -0,0 +1,24 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe Bespoke::XSLTProc do
4
+ let(:xslt) { File.read(fixture('books_to_csv.xslt')) }
5
+ let(:xml_file) { fixture('books.xml') }
6
+ let(:trans) { Bespoke::XSLTProc.new(xslt) }
7
+ let(:data) { %("1","One","Deity"\n"2","Two","Lovers"\n) }
8
+
9
+ it "transforms" do
10
+ text = File.read(xml_file)
11
+ trans.transform(text).should == data
12
+ end
13
+
14
+ it "can append to files" do
15
+ tmp = Tempfile.new('test')
16
+ 2.times do
17
+ File.open(tmp.path, "a+") do |out_file|
18
+ trans.transform_file(xml_file, out_file)
19
+ end
20
+ end
21
+ output = File.read(tmp.path)
22
+ output.should == data * 2
23
+ end
24
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bespoke
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-02-10 00:00:00.000000000 Z
12
+ date: 2014-04-04 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -43,6 +43,22 @@ dependencies:
43
43
  - - ~>
44
44
  - !ruby/object:Gem::Version
45
45
  version: '2.6'
46
+ - !ruby/object:Gem::Dependency
47
+ name: debugger
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
46
62
  - !ruby/object:Gem::Dependency
47
63
  name: rake
48
64
  requirement: !ruby/object:Gem::Requirement
@@ -91,6 +107,38 @@ dependencies:
91
107
  - - ! '>='
92
108
  - !ruby/object:Gem::Version
93
109
  version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: sqlite3
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ - !ruby/object:Gem::Dependency
127
+ name: sequel
128
+ requirement: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ! '>='
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ type: :runtime
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ! '>='
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
94
142
  description: Bespoke does in-memory object joins using mustache templates
95
143
  email:
96
144
  - duane@instructure.com
@@ -101,14 +149,22 @@ files:
101
149
  - bespoke.gemspec
102
150
  - readme.md
103
151
  - lib/bespoke/dsl.rb
104
- - lib/bespoke/exportable.rb
105
- - lib/bespoke/indexed_collection.rb
152
+ - lib/bespoke/join.rb
153
+ - lib/bespoke/projection.rb
154
+ - lib/bespoke/template.rb
106
155
  - lib/bespoke/version.rb
156
+ - lib/bespoke/xsltproc.rb
107
157
  - lib/bespoke.rb
108
- - spec/bespoke_spec.rb
109
- - spec/exportable_spec.rb
110
- - spec/indexed_collection_spec.rb
158
+ - spec/fixtures/books.xml
159
+ - spec/fixtures/books_to_csv.xslt
160
+ - spec/fixtures/one.csv
161
+ - spec/fixtures/sif.student_personal.xslt
162
+ - spec/fixtures/sif.xslt.mustache
163
+ - spec/fixtures/two.csv
164
+ - spec/join_spec.rb
111
165
  - spec/spec_helper.rb
166
+ - spec/template_spec.rb
167
+ - spec/xsltproc_spec.rb
112
168
  homepage:
113
169
  licenses: []
114
170
  post_install_message:
@@ -134,8 +190,14 @@ signing_key:
134
190
  specification_version: 3
135
191
  summary: Bespoke does in-memory object joins using mustache templates
136
192
  test_files:
137
- - spec/bespoke_spec.rb
138
- - spec/exportable_spec.rb
139
- - spec/indexed_collection_spec.rb
193
+ - spec/fixtures/books.xml
194
+ - spec/fixtures/books_to_csv.xslt
195
+ - spec/fixtures/one.csv
196
+ - spec/fixtures/sif.student_personal.xslt
197
+ - spec/fixtures/sif.xslt.mustache
198
+ - spec/fixtures/two.csv
199
+ - spec/join_spec.rb
140
200
  - spec/spec_helper.rb
201
+ - spec/template_spec.rb
202
+ - spec/xsltproc_spec.rb
141
203
  has_rdoc:
@@ -1,82 +0,0 @@
1
- require 'csv'
2
- require 'docile'
3
- require 'mustache'
4
- require 'logger'
5
-
6
- class Bespoke
7
- class Error < StandardError; end
8
- MissingTable = Class.new(Error)
9
-
10
- class Exportable
11
- attr_accessor :name, :fields, :joins
12
-
13
- def initialize(name, logger=nil)
14
- @name = name.to_sym
15
- @fields = {}
16
- @joins = {}
17
- @helpers = {}
18
- @logger = logger || Logger.new(STDERR)
19
- end
20
-
21
- class HelperClass
22
- def initialize(helpers, context)
23
- @helpers, @context = helpers, context
24
- end
25
-
26
- def has_key?(key)
27
- @helpers.has_key?(key) ||
28
- @context.has_key?(key)
29
- end
30
-
31
- def [](key)
32
- if @helpers.has_key?(key)
33
- @helpers[key].call(@context)
34
- elsif @context.has_key?(key)
35
- @context[key]
36
- else
37
- super(key)
38
- end
39
- end
40
- end
41
-
42
- def headers
43
- @fields.keys
44
- end
45
-
46
- def helper(name, &block)
47
- @helpers[name] = block
48
- end
49
-
50
- def field(name, template_string)
51
- fields[name] = Mustache::Template.new(template_string)
52
- end
53
-
54
- def join(name, key)
55
- joins[name] = key
56
- end
57
-
58
- def export(hashes={}, &block)
59
- raise "hashes missing #{@name.inspect} (of: #{hashes.keys.inspect})" unless hashes.has_key?(@name)
60
- hashes[@name].map do |main_key, row|
61
- context = { @name => row }
62
- context[:helper] = HelperClass.new(@helpers, context)
63
- @joins.each_pair do |join_name, key|
64
- if other_table = hashes[join_name.to_sym]
65
- if other_table.has_key?(row[key])
66
- context[join_name.to_sym] = other_table[row[key]]
67
- else
68
- @logger.warn "Expected foreign key #{key} with value #{row[key]} in table #{join_name} (#{row.inspect})"
69
- end
70
- else
71
- raise MissingTable, "Expected #{join_name}"
72
- end
73
- end
74
- fields.map do |name, template|
75
- Mustache.render(template, context)
76
- end.tap do |output_row|
77
- yield output_row if block_given?
78
- end
79
- end
80
- end
81
- end
82
- end
@@ -1,45 +0,0 @@
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
@@ -1,112 +0,0 @@
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
@@ -1,74 +0,0 @@
1
- require_relative 'spec_helper'
2
- require 'logger'
3
- require 'stringio'
4
-
5
- describe Bespoke::Exportable do
6
- let(:logger) { nil }
7
- let(:export) { Bespoke::Exportable.new(:test, logger) }
8
- let(:test_data) {
9
- {
10
- 1 => {:one => 1, :two => 2},
11
- 2 => {:one => 5, :two => 10}
12
- }
13
- }
14
-
15
- before do
16
- export.field(:column, "{{test.one}}-{{test.two}}")
17
- end
18
-
19
- it "initializes" do
20
- export
21
- end
22
-
23
- it "exports templated rows" do
24
- data = export.export(:test => test_data)
25
- data.should == [["1-2"], ["5-10"]]
26
- end
27
-
28
- context "helper methods" do
29
- it "can be declared" do
30
- export.field(:helped, "{{helper.helper_method}}")
31
- export.helper(:helper_method) { |row| "help!" }
32
-
33
- data = export.export(:test => test_data)
34
- data.should == [["1-2", "help!"], ["5-10", "help!"]]
35
- end
36
-
37
- it "has access to row data" do
38
- export.field(:helped, "{{helper.fallback}}")
39
- export.helper(:fallback) do |row|
40
- row[:test][:one] || row[:test][:two]
41
- end
42
-
43
- data = export.export(:test =>
44
- {
45
- 1 => {:two => 2},
46
- 2 => {:one => 5, :two => 10}
47
- }
48
- )
49
- data.should == [["-2", "2"], ["5-10", "5"]]
50
- end
51
- end
52
-
53
- context "logging" do
54
- let(:output) { StringIO.new }
55
- let(:logger) { Logger.new(output) }
56
-
57
- it "logs missing join data" do
58
- export.field(:column, "{{test.id}}-{{test.value}}")
59
- export.join(:user, :id)
60
- data = export.export({
61
- :user => {
62
- 1 => {:test_id => 1, :name => "Duane"},
63
- 2 => {:test_id => 2, :name => "Ken"},
64
- },
65
- :test => {
66
- 1 => {:id => 1, :value => 2},
67
- 2 => {:id => 5, :value => 10}
68
- }
69
- })
70
- log_msg = output.string.split(' : ')[1]
71
- log_msg.should == "Expected foreign key id with value 5 in table user ({:id=>5, :value=>10})\n"
72
- end
73
- end
74
- end
@@ -1,7 +0,0 @@
1
- require_relative 'spec_helper'
2
-
3
- describe Bespoke::IndexedCollection do
4
- it "initializes" do
5
- Bespoke::IndexedCollection.new
6
- end
7
- end