ad_hoc_template 0.0.1 → 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.
- 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
|