bespoke 0.1.2 → 0.2.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.
@@ -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