ad_hoc_template 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/Gemfile +1 -1
- data/README.md +1 -2
- data/ad_hoc_template.gemspec +1 -1
- data/lib/ad_hoc_template/command_line_interface.rb +94 -7
- data/lib/ad_hoc_template/parser.rb +160 -0
- data/lib/ad_hoc_template/record_reader.rb +281 -0
- data/lib/ad_hoc_template/version.rb +1 -1
- data/lib/ad_hoc_template.rb +29 -176
- data/spec/ad_hoc_template_spec.rb +308 -141
- data/spec/command_line_interface_spec.rb +393 -34
- data/spec/parser_spec.rb +399 -0
- data/spec/record_reader_spec.rb +377 -0
- metadata +11 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 476f29030bdd0bf5050ef7e961affd45a4a64667
|
4
|
+
data.tar.gz: 04901a83516ee55be4a9f887d4c050f12cabbd4b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 04719a11683310dc6ff684782ea384f1c2a55056ec99d1ab845660f763c7a90ca000e7fa75670dc9537bfbfd047563e41ad24a9ba709de0b438d8562da83288d
|
7
|
+
data.tar.gz: bd458ab6de4d391f4a4ea7fafce0412841f46f449c4b88e8a37b08c53c08ed38b48ea9fb099bdb14a1f12323d7595843c04c222a977480246d83973361681bf5
|
data/.gitignore
CHANGED
data/Gemfile
CHANGED
@@ -2,7 +2,7 @@ source 'https://rubygems.org'
|
|
2
2
|
|
3
3
|
# Specify your gem's dependencies in ad_hoc_template.gemspec
|
4
4
|
|
5
|
-
gem 'pseudohikiparser',
|
5
|
+
gem 'pseudohikiparser', '0.0.5.develop'
|
6
6
|
|
7
7
|
group :development do
|
8
8
|
gem "bundler", "~> 1.3"
|
data/README.md
CHANGED
@@ -63,7 +63,7 @@ the second paragraph in block
|
|
63
63
|
2. Execute the following at the command line:
|
64
64
|
|
65
65
|
```
|
66
|
-
ad_hoc_template template.txt sample_data.txt
|
66
|
+
$ ad_hoc_template template.txt sample_data.txt
|
67
67
|
```
|
68
68
|
|
69
69
|
Then you will get the following result:
|
@@ -77,7 +77,6 @@ the value of sub_key2 is value1-2
|
|
77
77
|
the value of sub_key1 is value2-1
|
78
78
|
the value of sub_key2 is value2-2
|
79
79
|
|
80
|
-
|
81
80
|
the first line of block
|
82
81
|
the second line of block
|
83
82
|
|
data/ad_hoc_template.gemspec
CHANGED
@@ -17,7 +17,7 @@ Gem::Specification.new do |spec|
|
|
17
17
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
19
|
spec.require_paths = ["lib"]
|
20
|
-
spec.add_runtime_dependency "pseudohikiparser", "0.0.
|
20
|
+
spec.add_runtime_dependency "pseudohikiparser", "0.0.5.develop"
|
21
21
|
|
22
22
|
spec.add_development_dependency "bundler", "~> 1.3"
|
23
23
|
spec.add_development_dependency "rake", "~> 10.1"
|
@@ -5,11 +5,36 @@ require 'optparse'
|
|
5
5
|
|
6
6
|
module AdHocTemplate
|
7
7
|
class CommandLineInterface
|
8
|
-
attr_accessor :output_filename, :template_data, :record_data
|
8
|
+
attr_accessor :output_filename, :template_data, :record_data, :tag_type, :data_format
|
9
|
+
|
10
|
+
TAG_RE_TO_TYPE = {
|
11
|
+
/\Ad(efault)?/i => :default,
|
12
|
+
/\Ac(urly_brackets)?/i => :curly_brackets,
|
13
|
+
/\As(quare_brackets)?/i => :square_brackets,
|
14
|
+
/\Axml_like1/i => :xml_like1,
|
15
|
+
/\Axml_like2/i => :xml_like2,
|
16
|
+
}
|
17
|
+
|
18
|
+
FORMAT_RE_TO_FORMAT = {
|
19
|
+
/\Ad(efault)?/i => :default,
|
20
|
+
/\Ay(a?ml)?/i => :yaml,
|
21
|
+
/\Aj(son)?/i => :json,
|
22
|
+
/\Ac(sv)?/i => :csv,
|
23
|
+
/\At(sv)?/i => :tsv,
|
24
|
+
}
|
25
|
+
|
26
|
+
FILE_EXTENTIONS = {
|
27
|
+
/\.ya?ml\Z/i => :yaml,
|
28
|
+
/\.json\Z/i => :json,
|
29
|
+
/\.csv\Z/i => :csv,
|
30
|
+
/\.tsv\Z/i => :tsv,
|
31
|
+
}
|
9
32
|
|
10
33
|
def initialize
|
11
|
-
@
|
34
|
+
@tag_formatter = AdHocTemplate::DefaultTagFormatter.new
|
12
35
|
@output_filename = nil
|
36
|
+
@tag_type = :default
|
37
|
+
@data_format = nil
|
13
38
|
end
|
14
39
|
|
15
40
|
def set_encoding(given_opt)
|
@@ -19,7 +44,7 @@ module AdHocTemplate
|
|
19
44
|
end
|
20
45
|
|
21
46
|
def parse_command_line_options
|
22
|
-
OptionParser.new do |opt|
|
47
|
+
OptionParser.new("USAGE: #{File.basename($0)} [OPTION]... TEMPLATE_FILE DATA_FILE") do |opt|
|
23
48
|
opt.on("-E [ex[:in]]", "--encoding [=ex[:in]]",
|
24
49
|
"Specify the default external and internal character encodings (same as the option of MRI") do |given_opt|
|
25
50
|
self.set_encoding(given_opt)
|
@@ -30,7 +55,27 @@ module AdHocTemplate
|
|
30
55
|
@output_filename = File.expand_path(output_file)
|
31
56
|
end
|
32
57
|
|
33
|
-
|
58
|
+
opt.on("-t [tag_type]", "--tag-type [=tag_type]",
|
59
|
+
"Choose a template tag type: default, curly_brackets or square_brackets") do |given_type|
|
60
|
+
choose_tag_type(given_type)
|
61
|
+
end
|
62
|
+
|
63
|
+
opt.on("-d [data_format]", "--data-format [=data_format]",
|
64
|
+
"Specify the format of input data: default, yaml, json, csv or tsv") do |data_format|
|
65
|
+
choose_data_format(data_format)
|
66
|
+
end
|
67
|
+
|
68
|
+
opt.on("-u [tag_config.yaml]","--user-defined-tag [=tag_config.yaml]",
|
69
|
+
"Configure a user-defined tag. The configuration file is in YAML format.") do |tag_config_yaml|
|
70
|
+
register_user_defined_tag_type(tag_config_yaml)
|
71
|
+
end
|
72
|
+
|
73
|
+
opt.parse!
|
74
|
+
end
|
75
|
+
|
76
|
+
unless @data_format
|
77
|
+
guessed_format = ARGV.length < 2 ? :default : guess_file_format(ARGV[1])
|
78
|
+
@data_format = guessed_format || :default
|
34
79
|
end
|
35
80
|
end
|
36
81
|
|
@@ -46,7 +91,8 @@ module AdHocTemplate
|
|
46
91
|
end
|
47
92
|
|
48
93
|
def convert
|
49
|
-
AdHocTemplate
|
94
|
+
AdHocTemplate.convert(@record_data, @template_data, @tag_type,
|
95
|
+
@data_format, @tag_formatter)
|
50
96
|
end
|
51
97
|
|
52
98
|
def open_output
|
@@ -62,9 +108,50 @@ module AdHocTemplate
|
|
62
108
|
def execute
|
63
109
|
parse_command_line_options
|
64
110
|
read_input_files
|
65
|
-
open_output
|
66
|
-
|
111
|
+
open_output {|out| out.print convert }
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def choose_tag_type(given_type)
|
117
|
+
if_any_regex_match(TAG_RE_TO_TYPE, given_type,
|
118
|
+
"The given type is not found. The default tag is chosen.") do |re, tag_type|
|
119
|
+
@tag_type = tag_type
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def choose_data_format(data_format)
|
124
|
+
if_any_regex_match(FORMAT_RE_TO_FORMAT, data_format,
|
125
|
+
"The given format is not found. The default format is chosen.") do |re, format|
|
126
|
+
@data_format = [:csv, :tsv].include?(format) ? make_csv_option(data_format, format) : format
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def register_user_defined_tag_type(tag_config_yaml)
|
131
|
+
config = File.read(File.expand_path(tag_config_yaml))
|
132
|
+
@tag_type = Parser.register_user_defined_tag_type(config)
|
133
|
+
end
|
134
|
+
|
135
|
+
def make_csv_option(data_format, format)
|
136
|
+
iteration_label = data_format.sub(/\A(csv|tsv):?/, "")
|
137
|
+
iteration_label.empty? ? format : { format => iteration_label }
|
138
|
+
end
|
139
|
+
|
140
|
+
def guess_file_format(filename)
|
141
|
+
if_any_regex_match(FILE_EXTENTIONS, filename) do |ext_re, format|
|
142
|
+
return format
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def if_any_regex_match(regex_table, target, failure_message=nil)
|
147
|
+
regex_table.each do |re, paired_value|
|
148
|
+
if re =~ target
|
149
|
+
yield re, paired_value
|
150
|
+
return
|
151
|
+
end
|
67
152
|
end
|
153
|
+
STDERR.puts failure_message if failure_message
|
154
|
+
nil
|
68
155
|
end
|
69
156
|
end
|
70
157
|
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "pseudohiki/inlineparser"
|
4
|
+
require "htmlelement"
|
5
|
+
|
6
|
+
module AdHocTemplate
|
7
|
+
class Parser < TreeStack
|
8
|
+
class TagNode < Parser::Node
|
9
|
+
attr_reader :type
|
10
|
+
|
11
|
+
def push(node=TreeStack::Node.new)
|
12
|
+
node[0] = assign_type(node[0]) if self.empty?
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
def assign_type(first_leaf)
|
17
|
+
if not first_leaf.kind_of? String or /\A\s/ =~ first_leaf
|
18
|
+
return first_leaf.sub(/\A(?:\r?\n|\r)/, "")
|
19
|
+
end
|
20
|
+
@type, first_leaf_content = split_by_newline_or_spaces(first_leaf)
|
21
|
+
@type = '#'.freeze + @type if kind_of? IterationTagNode
|
22
|
+
first_leaf_content||""
|
23
|
+
end
|
24
|
+
|
25
|
+
def split_by_newline_or_spaces(first_leaf)
|
26
|
+
sep = /\A\S*(?:\r?\n|\r)/ =~ first_leaf ? /(?:\r?\n|\r)/ : /\s+/
|
27
|
+
first_leaf.split(sep, 2)
|
28
|
+
end
|
29
|
+
private :assign_type, :split_by_newline_or_spaces
|
30
|
+
|
31
|
+
def contains_any_value_assigned_tag_node?(record)
|
32
|
+
self.select {|n| n.kind_of?(TagNode) }.each do |node|
|
33
|
+
if node.kind_of? IterationTagNode
|
34
|
+
return true if any_value_assigned_to_iteration_tag?(node, record)
|
35
|
+
else
|
36
|
+
val = record[node.join.strip]
|
37
|
+
return true if val and not val.empty?
|
38
|
+
end
|
39
|
+
end
|
40
|
+
false
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def empty_sub_records?(record, node)
|
46
|
+
sub_records = record[node.type]
|
47
|
+
return true if sub_records.nil? or sub_records.empty?
|
48
|
+
sub_records.each do |rec|
|
49
|
+
return false if rec.values.find {|val| val and not val.empty? }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def any_value_assigned_to_iteration_tag?(tag_node, record)
|
54
|
+
if tag_node.type
|
55
|
+
not empty_sub_records?(record, tag_node)
|
56
|
+
else
|
57
|
+
tag_node.contains_any_value_assigned_tag_node?(record)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class IterationTagNode < TagNode; end
|
63
|
+
class Leaf < Parser::Leaf; end
|
64
|
+
|
65
|
+
class TagType
|
66
|
+
attr_reader :head, :tail, :token_pat, :remove_iteration_indent
|
67
|
+
attr_reader :iteration_start, :iteration_end
|
68
|
+
@types = {}
|
69
|
+
|
70
|
+
def self.[](tag_name)
|
71
|
+
@types[tag_name]
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.register(tag_name=:default, tag=["<%", "%>"], iteration_tag=["<%#", "#%>"],
|
75
|
+
remove_iteration_indent=false)
|
76
|
+
@types[tag_name] = new(tag, iteration_tag, remove_iteration_indent)
|
77
|
+
end
|
78
|
+
|
79
|
+
def initialize(tag, iteration_tag, remove_iteration_indent)
|
80
|
+
assign_type(tag, iteration_tag)
|
81
|
+
@token_pat = PseudoHiki.compile_token_pat(@head.keys, @tail.keys)
|
82
|
+
@remove_iteration_indent = remove_iteration_indent
|
83
|
+
end
|
84
|
+
|
85
|
+
def assign_type(tag, iteration_tag)
|
86
|
+
@iteration_start, @iteration_end = iteration_tag
|
87
|
+
@head, @tail = {}, {}
|
88
|
+
[
|
89
|
+
[TagNode, tag],
|
90
|
+
[IterationTagNode, iteration_tag]
|
91
|
+
].each do |node_type, head_tail|
|
92
|
+
head, tail = head_tail
|
93
|
+
@head[head] = node_type
|
94
|
+
@tail[tail] = node_type
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
register
|
99
|
+
register(:square_brackets, ["[[", "]]"], ["[[#", "#]]"])
|
100
|
+
register(:curly_brackets, ["{{", "}}"], ["{{#", "#}}"])
|
101
|
+
register(:xml_like1, ["<!--%", "%-->"], ["<iterate>", "</iterate>"], true)
|
102
|
+
register(:xml_like2, ["<fill>", "</fill>"], ["<iterate>", "</iterate>"], true)
|
103
|
+
register(:xml_comment_like, ["<!--%", "%-->"], ["<!--%iterate%-->", "<!--%/iterate%-->"], true)
|
104
|
+
end
|
105
|
+
|
106
|
+
class UserDefinedTagTypeConfigError < StandardError; end
|
107
|
+
|
108
|
+
def self.parse(str, tag_name=:default)
|
109
|
+
if TagType[tag_name].remove_iteration_indent
|
110
|
+
str = remove_indent_before_iteration_tags(str, TagType[tag_name])
|
111
|
+
end
|
112
|
+
new(str, TagType[tag_name]).parse.tree
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.remove_indent_before_iteration_tags(template_source, tag_type)
|
116
|
+
[
|
117
|
+
tag_type.iteration_start,
|
118
|
+
tag_type.iteration_end
|
119
|
+
].inject(template_source) do |s, tag|
|
120
|
+
s.gsub(/^([ \t]+#{Regexp.escape(tag)}(?:\r?\n|\r))/) { $1.lstrip }
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def self.register_user_defined_tag_type(config_source)
|
125
|
+
config = YAML.load(config_source)
|
126
|
+
%w(tag_name tag iteration_tag).each do |item|
|
127
|
+
config[item] || raise(UserDefinedTagTypeConfigError,
|
128
|
+
"\"#{item}\" should be defined.")
|
129
|
+
end
|
130
|
+
TagType.register(registered_tag_name = config["tag_name"].to_sym,
|
131
|
+
config["tag"],
|
132
|
+
config["iteration_tag"],
|
133
|
+
config["remove_indent"] || false)
|
134
|
+
registered_tag_name
|
135
|
+
end
|
136
|
+
|
137
|
+
def initialize(str, tag)
|
138
|
+
@tag = tag
|
139
|
+
str = remove_trailing_newline_of_iteration_end_tag(str, @tag.iteration_end)
|
140
|
+
@tokens = PseudoHiki.split_into_tokens(str, @tag.token_pat)
|
141
|
+
super()
|
142
|
+
end
|
143
|
+
|
144
|
+
def parse
|
145
|
+
while token = @tokens.shift
|
146
|
+
next if @tag.tail[token] == current_node.class and self.pop
|
147
|
+
next if @tag.head[token] and self.push @tag.head[token].new
|
148
|
+
self.push Leaf.create(token)
|
149
|
+
end
|
150
|
+
|
151
|
+
self
|
152
|
+
end
|
153
|
+
|
154
|
+
private
|
155
|
+
|
156
|
+
def remove_trailing_newline_of_iteration_end_tag(str, iteration_end_tag)
|
157
|
+
str.gsub(/#{Regexp.escape(iteration_end_tag)}(?:\r?\n|\r)/, iteration_end_tag)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,281 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'json'
|
5
|
+
require 'csv'
|
6
|
+
|
7
|
+
module AdHocTemplate
|
8
|
+
module RecordReader
|
9
|
+
module YAMLReader
|
10
|
+
def self.read_record(yaml_data)
|
11
|
+
YAML.load(yaml_data)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.to_yaml(config_data)
|
15
|
+
data = RecordReader.read_record(config_data)
|
16
|
+
YAML.dump(data)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module JSONReader
|
21
|
+
def self.read_record(json_data)
|
22
|
+
JSON.parse(json_data)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.to_json(config_data)
|
26
|
+
data = RecordReader.read_record(config_data)
|
27
|
+
JSON.dump(data)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module CSVReader
|
32
|
+
def self.read_record(csv_data, config={ csv: nil })
|
33
|
+
label, sep = parse_config(config)
|
34
|
+
header, *data = CSV.new(csv_data, col_sep: sep).to_a
|
35
|
+
records = data.map {|row| convert_to_hash(header, row) }
|
36
|
+
if label
|
37
|
+
{ '#' + label => records }
|
38
|
+
elsif records.length == 1
|
39
|
+
records[0]
|
40
|
+
else
|
41
|
+
records
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.convert_to_hash(header, row_array)
|
46
|
+
{}.tap do |record|
|
47
|
+
header.zip(row_array).each do |key, value|
|
48
|
+
record[key] = value
|
49
|
+
end
|
50
|
+
end
|
51
|
+
# if RUBY_VERSION >= 2.1.0: header.zip(row_array).to_h
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.parse_config(config)
|
55
|
+
case config
|
56
|
+
when Symbol
|
57
|
+
format, label = config, nil
|
58
|
+
when String
|
59
|
+
format, label = :csv, config
|
60
|
+
when Hash
|
61
|
+
format, label = config.to_a[0]
|
62
|
+
end
|
63
|
+
field_sep = format == :tsv ? "\t" : CSV::DEFAULT_OPTIONS[:col_sep]
|
64
|
+
return label, field_sep
|
65
|
+
end
|
66
|
+
|
67
|
+
private_class_method :convert_to_hash
|
68
|
+
end
|
69
|
+
|
70
|
+
SEPARATOR = /:\s*/o
|
71
|
+
BLOCK_HEAD = /\A\/\/@/o
|
72
|
+
ITERATION_HEAD = /\A\/\/@#/o
|
73
|
+
EMPTY_LINE = /\A(?:\r?\n|\r)\Z/o
|
74
|
+
ITERATION_MARK = /\A#/o
|
75
|
+
READERS_RE = {
|
76
|
+
key_value: SEPARATOR,
|
77
|
+
iteration: ITERATION_HEAD,
|
78
|
+
block: BLOCK_HEAD,
|
79
|
+
empty_line: EMPTY_LINE,
|
80
|
+
}
|
81
|
+
|
82
|
+
class ReaderState
|
83
|
+
attr_accessor :current_block_label
|
84
|
+
|
85
|
+
def initialize(config={}, stack=[])
|
86
|
+
@stack = stack
|
87
|
+
@configs = [config]
|
88
|
+
setup_reader
|
89
|
+
end
|
90
|
+
|
91
|
+
def push(reader)
|
92
|
+
@stack.push reader
|
93
|
+
end
|
94
|
+
|
95
|
+
def pop
|
96
|
+
@stack.pop unless @stack.length == 1
|
97
|
+
end
|
98
|
+
|
99
|
+
def setup_stack(line)
|
100
|
+
@stack[-1].setup_stack(line)
|
101
|
+
end
|
102
|
+
|
103
|
+
def current_reader
|
104
|
+
@stack[-1]
|
105
|
+
end
|
106
|
+
|
107
|
+
def read(line)
|
108
|
+
@stack[-1].read(line)
|
109
|
+
end
|
110
|
+
|
111
|
+
def push_new_record
|
112
|
+
new_record = {}
|
113
|
+
@configs.push new_record
|
114
|
+
new_record
|
115
|
+
end
|
116
|
+
|
117
|
+
def pop_current_record
|
118
|
+
@configs.pop
|
119
|
+
end
|
120
|
+
|
121
|
+
def current_record
|
122
|
+
@configs[-1]
|
123
|
+
end
|
124
|
+
|
125
|
+
def parsed_record
|
126
|
+
@configs[0]
|
127
|
+
end
|
128
|
+
|
129
|
+
def read_record(lines)
|
130
|
+
lines = lines.each_line.to_a if lines.kind_of? String
|
131
|
+
lines.each do |line|
|
132
|
+
setup_stack(line)
|
133
|
+
read(line)
|
134
|
+
end
|
135
|
+
remove_trailing_empty_lines_from_last_block!
|
136
|
+
parsed_record
|
137
|
+
end
|
138
|
+
|
139
|
+
def last_block_value
|
140
|
+
current_record[current_block_label]
|
141
|
+
end
|
142
|
+
|
143
|
+
def remove_trailing_empty_lines_from_last_block!
|
144
|
+
if current_reader.kind_of? BlockReader
|
145
|
+
last_block_value.sub!(/(#{$/})+\Z/, $/)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
def setup_reader
|
152
|
+
Reader.setup_reader(self)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
class Reader
|
157
|
+
def self.setup_reader(stack)
|
158
|
+
readers = {}
|
159
|
+
{
|
160
|
+
base: BaseReader,
|
161
|
+
key_value: KeyValueReader,
|
162
|
+
block: BlockReader,
|
163
|
+
iteration: IterationReader,
|
164
|
+
}.each do |k, v|
|
165
|
+
readers[k] = v.new(stack, readers)
|
166
|
+
end
|
167
|
+
stack.push readers[:base]
|
168
|
+
readers
|
169
|
+
end
|
170
|
+
|
171
|
+
def initialize(stack, readers)
|
172
|
+
@stack = stack
|
173
|
+
@readers = readers
|
174
|
+
end
|
175
|
+
|
176
|
+
def pop_stack
|
177
|
+
@stack.pop
|
178
|
+
end
|
179
|
+
|
180
|
+
def read(line)
|
181
|
+
end
|
182
|
+
|
183
|
+
private
|
184
|
+
|
185
|
+
def push_reader_if_match(line, readers)
|
186
|
+
readers.each do |reader|
|
187
|
+
return @stack.push(@readers[reader]) if READERS_RE[reader] === line
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def setup_new_block(line, initial_value)
|
192
|
+
label = line.sub(BLOCK_HEAD, "").chomp
|
193
|
+
@stack.current_record[label] ||= initial_value
|
194
|
+
@stack.current_block_label = label
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
|
199
|
+
class BaseReader < Reader
|
200
|
+
def setup_stack(line)
|
201
|
+
push_reader_if_match(line, [:iteration, :block, :key_value])
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
class KeyValueReader < Reader
|
206
|
+
def setup_stack(line)
|
207
|
+
case line
|
208
|
+
when EMPTY_LINE, ITERATION_HEAD, BLOCK_HEAD
|
209
|
+
pop_stack
|
210
|
+
end
|
211
|
+
push_reader_if_match(line, [:iteration, :block])
|
212
|
+
end
|
213
|
+
|
214
|
+
def read(line)
|
215
|
+
key, value = line.split(SEPARATOR, 2)
|
216
|
+
@stack.current_record[key] = value.chomp
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
class BlockReader < Reader
|
221
|
+
def setup_stack(line)
|
222
|
+
case line
|
223
|
+
when ITERATION_HEAD, BLOCK_HEAD
|
224
|
+
@stack.remove_trailing_empty_lines_from_last_block!
|
225
|
+
pop_stack
|
226
|
+
end
|
227
|
+
push_reader_if_match(line, [:iteration, :block])
|
228
|
+
end
|
229
|
+
|
230
|
+
def read(line)
|
231
|
+
block_value = @stack.last_block_value
|
232
|
+
case line
|
233
|
+
when BLOCK_HEAD
|
234
|
+
setup_new_block(line, String.new)
|
235
|
+
when EMPTY_LINE
|
236
|
+
block_value << line unless block_value.empty?
|
237
|
+
else
|
238
|
+
block_value << line
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
class IterationReader < Reader
|
244
|
+
def setup_stack(line)
|
245
|
+
case line
|
246
|
+
when ITERATION_HEAD
|
247
|
+
@stack.pop_current_record
|
248
|
+
when BLOCK_HEAD
|
249
|
+
@stack.pop_current_record
|
250
|
+
pop_stack
|
251
|
+
@stack.push @readers[:block]
|
252
|
+
when SEPARATOR
|
253
|
+
@stack.pop_current_record
|
254
|
+
@stack.last_block_value.push @stack.push_new_record
|
255
|
+
@stack.push @readers[:key_value]
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
def read(line)
|
260
|
+
case line
|
261
|
+
when ITERATION_HEAD
|
262
|
+
setup_new_block(line, [])
|
263
|
+
@stack.push_new_record
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def self.read_record(input, source_format=:default)
|
269
|
+
case source_format
|
270
|
+
when :default
|
271
|
+
ReaderState.new.read_record(input)
|
272
|
+
when :yaml
|
273
|
+
YAMLReader.read_record(input)
|
274
|
+
when :json
|
275
|
+
JSONReader.read_record(input)
|
276
|
+
when :csv, :tsv, Hash
|
277
|
+
CSVReader.read_record(input, source_format)
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|