daifuku 0.9.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 +7 -0
- data/.github/workflows/ruby.yml +31 -0
- data/.gitignore +56 -0
- data/.ruby-version +1 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +55 -0
- data/LICENSE +21 -0
- data/README.md +149 -0
- data/Rakefile +6 -0
- data/daifuku.gemspec +39 -0
- data/daifuku.rb +9 -0
- data/docs/images/code-completion.png +0 -0
- data/docs/images/transpile.png +0 -0
- data/docs/syntax.ja.md +143 -0
- data/example/LogDefinitions/common.md +20 -0
- data/example/LogDefinitions/recipe_search.md +19 -0
- data/example/iOS/Templates/CommonPayload.swift.erb +25 -0
- data/example/iOS/Templates/LogCategories.swift.erb +33 -0
- data/example/iOS/generate-log-classes.rb +212 -0
- data/lib/daifuku/compiler.rb +43 -0
- data/lib/daifuku/models.rb +171 -0
- data/lib/daifuku/parser.rb +76 -0
- data/lib/daifuku/renderer.rb +11 -0
- data/lib/daifuku/strdef_generator.rb +196 -0
- data/lib/daifuku/validator.rb +127 -0
- data/lib/daifuku/version.rb +3 -0
- data/lib/daifuku.rb +10 -0
- metadata +143 -0
@@ -0,0 +1,212 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __dir__)
|
5
|
+
require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
|
6
|
+
require 'daifuku'
|
7
|
+
require 'erb'
|
8
|
+
require 'active_support/inflector'
|
9
|
+
require 'fileutils'
|
10
|
+
|
11
|
+
ROOT_PATH = File.absolute_path(File.join(__dir__, '../'))
|
12
|
+
LOG_DEFINITIONS = File.join(ROOT_PATH, 'LogDefinitions')
|
13
|
+
TEMPLATE_DIR = File.join(ROOT_PATH, 'iOS/Templates')
|
14
|
+
GENERATED_CODE_DIR = File.join(ROOT_PATH, 'AutoGenerated')
|
15
|
+
SWIFT_TYPE_MAPS = {
|
16
|
+
'smallint' => 'Int16',
|
17
|
+
'integer' => 'Int',
|
18
|
+
'bigint' => 'Int64',
|
19
|
+
'real' => 'Float',
|
20
|
+
'double' => 'Double',
|
21
|
+
'boolean' => 'Bool',
|
22
|
+
'string' => 'String',
|
23
|
+
'date' => 'JSTDateColumn',
|
24
|
+
'timestampz' => 'ZonedTimestampColumn'
|
25
|
+
}
|
26
|
+
UNSUPPORTED_TYPES = %w(timestamp)
|
27
|
+
|
28
|
+
# Represents a Category consisting of multiple events
|
29
|
+
# ex) Category: RecipeDetail
|
30
|
+
class CategoryRepresentation
|
31
|
+
def initialize(category)
|
32
|
+
@category = category
|
33
|
+
end
|
34
|
+
|
35
|
+
def name
|
36
|
+
@category.name
|
37
|
+
end
|
38
|
+
|
39
|
+
def class_name
|
40
|
+
@category.name.camelize
|
41
|
+
end
|
42
|
+
|
43
|
+
def descriptions
|
44
|
+
@category.descriptions
|
45
|
+
end
|
46
|
+
|
47
|
+
def variable_name
|
48
|
+
@category.name.camelize(:lower)
|
49
|
+
end
|
50
|
+
|
51
|
+
def events
|
52
|
+
@events ||= @category.events.map { |_, event| EventRepresentation.new(event) }
|
53
|
+
end
|
54
|
+
|
55
|
+
def available_events
|
56
|
+
events.reject(&:obsolete?)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Represents a Event consisting of multiple columns
|
61
|
+
# ex) Event: tap_sample_view
|
62
|
+
class EventRepresentation
|
63
|
+
def initialize(event)
|
64
|
+
@event = event
|
65
|
+
end
|
66
|
+
|
67
|
+
def name
|
68
|
+
@event.name
|
69
|
+
end
|
70
|
+
|
71
|
+
def variable_name
|
72
|
+
@event.name.camelize(:lower)
|
73
|
+
end
|
74
|
+
|
75
|
+
def columns
|
76
|
+
@columns ||= @event.columns.reject(&:obsolete?).map { |column| ColumnRepresentation.new(column) }
|
77
|
+
end
|
78
|
+
|
79
|
+
def descriptions
|
80
|
+
@event.descriptions
|
81
|
+
end
|
82
|
+
|
83
|
+
def associated_types
|
84
|
+
columns.map(&:as_argument)&.join(', ')
|
85
|
+
end
|
86
|
+
|
87
|
+
def pattern_matches
|
88
|
+
columns.map { |column| column.variable_name.to_string }&.join(', ')
|
89
|
+
end
|
90
|
+
|
91
|
+
def availability_annotation
|
92
|
+
obsolete? ? '@available(*, unavailable) ' : ''
|
93
|
+
end
|
94
|
+
|
95
|
+
def obsolete?
|
96
|
+
@event.obsolete?
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Represents a column
|
101
|
+
class ColumnRepresentation
|
102
|
+
def initialize(column)
|
103
|
+
@column = column
|
104
|
+
end
|
105
|
+
|
106
|
+
def variable_name
|
107
|
+
@column.name.camelize(:lower)
|
108
|
+
end
|
109
|
+
|
110
|
+
def original_name
|
111
|
+
@column.name
|
112
|
+
end
|
113
|
+
|
114
|
+
def swift_type
|
115
|
+
convert_to_swift_type(@column)
|
116
|
+
end
|
117
|
+
|
118
|
+
def descriptions
|
119
|
+
@column.descriptions
|
120
|
+
end
|
121
|
+
|
122
|
+
def as_argument
|
123
|
+
"#{variable_name}: #{swift_type}"
|
124
|
+
end
|
125
|
+
|
126
|
+
def call_dump
|
127
|
+
caller = if @column.type.optional?
|
128
|
+
"#{variable_name}?"
|
129
|
+
else
|
130
|
+
variable_name.to_s
|
131
|
+
end
|
132
|
+
type = @column.type
|
133
|
+
if type.name == 'string' && !type.str_length.nil?
|
134
|
+
"#{caller}.validateLength(within: #{type.str_length}).dump()"
|
135
|
+
else
|
136
|
+
"#{caller}.dump()"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
|
142
|
+
def convert_to_swift_type(column)
|
143
|
+
type = column.type
|
144
|
+
raise "Code builder currently doesn't support #{type.name}." if UNSUPPORTED_TYPES.include?(type.name)
|
145
|
+
|
146
|
+
if type.custom?
|
147
|
+
swift_type ||= type.name # custom type
|
148
|
+
else
|
149
|
+
swift_type = SWIFT_TYPE_MAPS[type.name] # primitive types
|
150
|
+
end
|
151
|
+
if type.optional?
|
152
|
+
"#{swift_type}?"
|
153
|
+
else
|
154
|
+
swift_type
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Generating Swift files from Markdown files using Daifuku
|
160
|
+
class Generator
|
161
|
+
def initialize
|
162
|
+
check_category_names
|
163
|
+
@common_category = categories[COMMON_CATEGORY_NAME]
|
164
|
+
raise 'Could not found common category. Please define common.md' unless @common_category
|
165
|
+
end
|
166
|
+
|
167
|
+
def generate_common_payload!(destination)
|
168
|
+
FileUtils.mkdir_p(File.dirname(destination))
|
169
|
+
template = IO.read(File.join(TEMPLATE_DIR, 'CommonPayload.swift.erb'))
|
170
|
+
columns = @common_category.common_columns.reject(&:obsolete?).map { |column| ColumnRepresentation.new(column) }
|
171
|
+
result = render(template, columns: columns)
|
172
|
+
IO.write(destination, result)
|
173
|
+
end
|
174
|
+
|
175
|
+
def generate_all_log_categories!(destination)
|
176
|
+
FileUtils.mkdir_p(File.dirname(destination))
|
177
|
+
template = IO.read(File.join(TEMPLATE_DIR, 'LogCategories.swift.erb'))
|
178
|
+
categories_to_generate = categories.map { |_, category| CategoryRepresentation.new(category) }
|
179
|
+
result = render(template, categories: categories_to_generate)
|
180
|
+
IO.write(destination, result)
|
181
|
+
end
|
182
|
+
|
183
|
+
def check_category_names
|
184
|
+
# Daifuku uses the file name as the category name, the category name in the Markdown file is simply ignored.
|
185
|
+
# But for consistency, here we make sure they are the same.
|
186
|
+
Dir.glob(File.join(LOG_DEFINITIONS, '*.md')) do |path|
|
187
|
+
content = File.read(path)
|
188
|
+
md_category_name = /^#\s*(.+)/.match(content)
|
189
|
+
raise "Could not find category name in #{path}" unless md_category_name
|
190
|
+
|
191
|
+
category_name = md_category_name[1]
|
192
|
+
file_name = File.basename(path, '.md')
|
193
|
+
unless file_name == category_name
|
194
|
+
raise %(The file name "#{file_name}" does not match the category name "#{category_name}" in #{path})
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
private
|
200
|
+
|
201
|
+
def render(template, hash)
|
202
|
+
ERB.new(template, trim_mode: '-').result_with_hash(hash)
|
203
|
+
end
|
204
|
+
|
205
|
+
def categories
|
206
|
+
@categories ||= Daifuku::Compiler.new.compile(LOG_DEFINITIONS)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
generator = Generator.new
|
211
|
+
generator.generate_common_payload!(File.join(GENERATED_CODE_DIR, 'CommonPayload.swift'))
|
212
|
+
generator.generate_all_log_categories!(File.join(GENERATED_CODE_DIR, 'LogCategories.swift'))
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Daifuku
|
2
|
+
DEFAULT_NAME_LENGTH = 64
|
3
|
+
|
4
|
+
class Compiler
|
5
|
+
attr_reader :options
|
6
|
+
|
7
|
+
# Compile markdowns into Ruby objects.
|
8
|
+
# @param directory_path String Directory path containing log definitions.
|
9
|
+
# @param options [String: Object] Compiler options
|
10
|
+
# @return [String: Category]
|
11
|
+
def compile(directory_path, options = {})
|
12
|
+
@options = options
|
13
|
+
categories = Dir.entries(directory_path).map do |file|
|
14
|
+
if file.end_with?('.md')
|
15
|
+
category_name = File.basename(file, '.md')
|
16
|
+
path = File.join(directory_path, file)
|
17
|
+
# This situation implicates missing LANG, use UTF-8 by default.
|
18
|
+
enc = (Encoding.default_external == Encoding::US_ASCII) ? Encoding::UTF_8 : Encoding.default_external
|
19
|
+
str = File.read(path, encoding: enc)
|
20
|
+
parser.parse(str, category_name)
|
21
|
+
end
|
22
|
+
end.compact.map { |category| [category.name, category] }.to_h
|
23
|
+
validator.validate!(categories)
|
24
|
+
categories
|
25
|
+
end
|
26
|
+
|
27
|
+
def parser
|
28
|
+
@parser ||= Parser.new
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
def validator
|
33
|
+
@validator ||= Validator.new([MultipleTypesRule.new,
|
34
|
+
ReservedColumnsRule.new,
|
35
|
+
ShadowingRule.new,
|
36
|
+
NameLengthRule.new(name_length)])
|
37
|
+
end
|
38
|
+
|
39
|
+
def name_length
|
40
|
+
options[:name_length] || DEFAULT_NAME_LENGTH
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
module Daifuku
|
2
|
+
class Category
|
3
|
+
# @return String
|
4
|
+
attr_reader :name
|
5
|
+
# @return [String: Event]
|
6
|
+
attr_reader :events
|
7
|
+
# @return String
|
8
|
+
attr_reader :descriptions
|
9
|
+
# @return [Column]
|
10
|
+
attr_reader :common_columns
|
11
|
+
|
12
|
+
def initialize(name, events, descriptions, common_columns)
|
13
|
+
@name = name
|
14
|
+
@events = events
|
15
|
+
@descriptions = descriptions
|
16
|
+
@common_columns = common_columns
|
17
|
+
end
|
18
|
+
|
19
|
+
def dump
|
20
|
+
{
|
21
|
+
'name' => name,
|
22
|
+
'events' => events.to_h { |name, event| [name, event.dump] },
|
23
|
+
'descriptions' => descriptions,
|
24
|
+
'common_columns' => common_columns.map(&:dump),
|
25
|
+
}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Event
|
30
|
+
# @return String
|
31
|
+
attr_reader :name
|
32
|
+
# @return [Column]
|
33
|
+
attr_reader :columns
|
34
|
+
# @return String
|
35
|
+
attr_reader :descriptions
|
36
|
+
|
37
|
+
def obsolete?
|
38
|
+
@is_obsolete
|
39
|
+
end
|
40
|
+
|
41
|
+
def initialize(name, columns, descriptions, is_obsolete)
|
42
|
+
@name = name
|
43
|
+
@columns = columns
|
44
|
+
@descriptions = descriptions
|
45
|
+
@is_obsolete = is_obsolete
|
46
|
+
end
|
47
|
+
|
48
|
+
def dump
|
49
|
+
{
|
50
|
+
'name' => name,
|
51
|
+
'columns' => columns.map(&:dump),
|
52
|
+
'descriptions' => descriptions,
|
53
|
+
'is_obsolete' => obsolete?,
|
54
|
+
}
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class Column
|
59
|
+
# @return String
|
60
|
+
attr_reader :name
|
61
|
+
# @return Type
|
62
|
+
attr_reader :type
|
63
|
+
# @return [String]
|
64
|
+
attr_reader :descriptions
|
65
|
+
|
66
|
+
def obsolete?
|
67
|
+
@is_obsolete
|
68
|
+
end
|
69
|
+
|
70
|
+
def initialize(name, type, descriptions, is_obsolete = false)
|
71
|
+
@name = name
|
72
|
+
@type = type
|
73
|
+
@descriptions = descriptions
|
74
|
+
@is_obsolete = is_obsolete
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.parse(descriptor, descriptions)
|
78
|
+
# column_name: !bigint
|
79
|
+
# column_name: !bigint?
|
80
|
+
# column_name: !string 256
|
81
|
+
# column_name: !string? 256
|
82
|
+
# column_name: CustomType
|
83
|
+
# [obsolete] column_type: !bigint
|
84
|
+
if descriptor =~ /(\[obsolete\])?\s*(\w+):\s*(!?\w+\??(?:\s(\d+))?)/
|
85
|
+
obsolete = $1 != nil
|
86
|
+
name = $2
|
87
|
+
type = Type.parse($3)
|
88
|
+
Column.new(name, type, descriptions, obsolete)
|
89
|
+
else
|
90
|
+
raise "Could not parse column '#{descriptor}'"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def strdef
|
95
|
+
if type.name == 'string' && type.str_length
|
96
|
+
"#{name}: !#{type.name} #{type.str_length}"
|
97
|
+
elsif !type.custom?
|
98
|
+
"#{name}: !#{type.name}"
|
99
|
+
else
|
100
|
+
"#{name}: #{type.name}"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def dump
|
105
|
+
{'name' => name, 'type' => type.dump, 'descriptions' => descriptions}
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
class Type
|
110
|
+
AVAILABLE_BUILTIN_TYPES = %w(
|
111
|
+
smallint
|
112
|
+
integer
|
113
|
+
bigint
|
114
|
+
real
|
115
|
+
double
|
116
|
+
boolean
|
117
|
+
string
|
118
|
+
date
|
119
|
+
timestamp
|
120
|
+
timestamptz
|
121
|
+
)
|
122
|
+
# @return String
|
123
|
+
attr_reader :name
|
124
|
+
# Length for built-in String types
|
125
|
+
# This parameter returns nil when type is not String
|
126
|
+
# @return Integer?
|
127
|
+
attr_reader :str_length
|
128
|
+
|
129
|
+
def initialize(name, optional, custom, str_length = nil)
|
130
|
+
@name = name
|
131
|
+
@is_optional = optional
|
132
|
+
@is_custom = custom
|
133
|
+
@str_length = str_length
|
134
|
+
unless custom
|
135
|
+
raise "Invalid built-in type #{name}" unless AVAILABLE_BUILTIN_TYPES.include?(name)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# @return Boolean
|
140
|
+
def optional?
|
141
|
+
@is_optional
|
142
|
+
end
|
143
|
+
|
144
|
+
# Boolean value which indicates whether this type is user defined types or not.
|
145
|
+
# @return Boolean
|
146
|
+
def custom?
|
147
|
+
@is_custom
|
148
|
+
end
|
149
|
+
|
150
|
+
def self.parse(descriptor)
|
151
|
+
type_name, str_length = descriptor.split
|
152
|
+
is_optional = type_name.end_with?('?')
|
153
|
+
type_without_optional = type_name.gsub(/\?$/, '')
|
154
|
+
custom = !type_name.start_with?('!')
|
155
|
+
if custom
|
156
|
+
raise "Doesn't support str_length for custom types #{type_name}" unless str_length.nil?
|
157
|
+
Type.new(type_without_optional, is_optional, custom, nil)
|
158
|
+
else
|
159
|
+
built_in_type_name = type_without_optional.gsub(/^!/, '')
|
160
|
+
raise "type_name '#{built_in_type_name}' is not allowed" unless AVAILABLE_BUILTIN_TYPES.include?(built_in_type_name)
|
161
|
+
raise "length for '#{built_in_type_name}' is not supported" if built_in_type_name != 'string' && !str_length.nil?
|
162
|
+
|
163
|
+
Type.new(built_in_type_name, is_optional, custom, str_length&.to_i)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def dump
|
168
|
+
{'name' => name, 'optional' => optional?, 'is_custom' => custom?, 'str_length' => str_length}
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'redcarpet'
|
2
|
+
require 'nokogiri'
|
3
|
+
|
4
|
+
module Daifuku
|
5
|
+
|
6
|
+
class Parser
|
7
|
+
def parse(markdown_str, category_name)
|
8
|
+
html_str = markdown.render(markdown_str)
|
9
|
+
html = Nokogiri::HTML(html_str).css('body')
|
10
|
+
|
11
|
+
top_level_nodes = html.children
|
12
|
+
nodes_per_event = {}
|
13
|
+
common_columns_nodes = []
|
14
|
+
current_event_name = nil
|
15
|
+
top_level_nodes.each do |node|
|
16
|
+
case node.name
|
17
|
+
when 'h2'
|
18
|
+
current_event_name = node.content
|
19
|
+
when 'p', 'ul'
|
20
|
+
if current_event_name
|
21
|
+
nodes_per_event[current_event_name] ||= []
|
22
|
+
nodes_per_event[current_event_name] << node
|
23
|
+
else
|
24
|
+
common_columns_nodes << node
|
25
|
+
end
|
26
|
+
else
|
27
|
+
next
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
events = nodes_per_event.map { |name_with_annotation, nodes|
|
32
|
+
event = parse_event(name_with_annotation, nodes)
|
33
|
+
[event.name, event]
|
34
|
+
}.to_h
|
35
|
+
common_event = parse_event(nil, common_columns_nodes)
|
36
|
+
descriptions = common_event&.descriptions || []
|
37
|
+
Category.new(category_name, events, descriptions, common_event&.columns || [])
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def parse_event(event_name, nodes)
|
43
|
+
if event_name_match = event_name&.match(/\s*\[obsolete\]\s*(.*)/)
|
44
|
+
event_name = event_name_match[1]
|
45
|
+
is_obsolete = true
|
46
|
+
else
|
47
|
+
is_obsolete = false
|
48
|
+
end
|
49
|
+
descriptions = nodes.select { |node| node.name == 'p' }
|
50
|
+
.map { |node| node.content }
|
51
|
+
columns_node = nodes.select { |node| node.name == 'ul' }.first
|
52
|
+
columns = []
|
53
|
+
columns = parse_columns(columns_node) if columns_node
|
54
|
+
Event.new(event_name, columns, descriptions, is_obsolete)
|
55
|
+
end
|
56
|
+
|
57
|
+
def parse_columns(columns_node)
|
58
|
+
descriptor_nodes = columns_node.css('> li')
|
59
|
+
descriptor_nodes.map do |node|
|
60
|
+
descriptor = node.content
|
61
|
+
descriptions = node.css("ul li").map(&:content)
|
62
|
+
column = Column.parse(descriptor, descriptions)
|
63
|
+
raise "Could not parse '#{descriptor}'" unless column
|
64
|
+
column
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def markdown
|
69
|
+
@markdown ||= Redcarpet::Markdown.new(renderer, underline: false, emphasis: false)
|
70
|
+
end
|
71
|
+
|
72
|
+
def renderer
|
73
|
+
@renderer ||= Daifuku::Renderer.new
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
module Daifuku
|
2
|
+
# This class is used to generate .strdef file from .md files.
|
3
|
+
# .strdef file is used to define the structure of the log.
|
4
|
+
# https://techlife.cookpad.com/entry/2019/10/18/090000
|
5
|
+
# Since the `.strdef` generation for Redshift Spectrum is for Cookpad's internal reasons,
|
6
|
+
# we initially decided it would be more natural not to include it in the core of daifuku,
|
7
|
+
# but we added it as a reference implementation.
|
8
|
+
class StrdefGenerator
|
9
|
+
class StreamDefinition
|
10
|
+
def self.parse(strdef)
|
11
|
+
columns = {}
|
12
|
+
strdef.each_line do |line|
|
13
|
+
case line
|
14
|
+
when /\A\s*-\s+(\w+):\s*(!domain\s+\w+|!\w+)(?:\s+(\d+))?/
|
15
|
+
name = $1
|
16
|
+
type = $2.sub(/\s+/, ' ').sub(/\A!/, '')
|
17
|
+
length = $3
|
18
|
+
columns[name] = StreamColumn.new(name, type, length&.to_i)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
new(columns)
|
22
|
+
end
|
23
|
+
|
24
|
+
include Enumerable
|
25
|
+
|
26
|
+
def initialize(columns)
|
27
|
+
@columns = columns
|
28
|
+
end
|
29
|
+
|
30
|
+
def each_column(&block)
|
31
|
+
@columns.each_value(&block)
|
32
|
+
end
|
33
|
+
|
34
|
+
def [](name)
|
35
|
+
@columns[name]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class StreamColumn
|
40
|
+
def initialize(name, type, length = nil)
|
41
|
+
@name = name
|
42
|
+
@type = type
|
43
|
+
@length = length
|
44
|
+
end
|
45
|
+
|
46
|
+
attr_reader :name
|
47
|
+
attr_reader :type
|
48
|
+
attr_reader :length
|
49
|
+
|
50
|
+
def strdef
|
51
|
+
if length
|
52
|
+
"#{name}: !#{type} #{length}"
|
53
|
+
else
|
54
|
+
"#{name}: !#{type}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class LogTable
|
60
|
+
PREDEFINED = %w[id log_id event_category event_name]
|
61
|
+
|
62
|
+
def self.predefined_column?(name)
|
63
|
+
PREDEFINED.include?(name)
|
64
|
+
end
|
65
|
+
|
66
|
+
def initialize
|
67
|
+
@columns = {}
|
68
|
+
end
|
69
|
+
|
70
|
+
def add_common_column(column)
|
71
|
+
@columns[column.name] = LogColumn.new(column.name, column.type, true)
|
72
|
+
end
|
73
|
+
|
74
|
+
def add_column(column, location = '-')
|
75
|
+
if prev = @columns[column.name]
|
76
|
+
if prev.common?
|
77
|
+
raise "common column is being overwritten: #{column.name}: location=#{location}"
|
78
|
+
end
|
79
|
+
unless column.type.name == prev.type.name
|
80
|
+
raise "column type mismatch: name=#{column.name}, curr=#{column.type.name}, prev=#{prev.type.name}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
@columns[column.name] = LogColumn.new(column.name, column.type)
|
84
|
+
end
|
85
|
+
|
86
|
+
def [](name)
|
87
|
+
@columns[name]
|
88
|
+
end
|
89
|
+
|
90
|
+
def each_column(&block)
|
91
|
+
@columns.each_value(&block)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
class LogColumn
|
96
|
+
def initialize(name, type, is_common = false)
|
97
|
+
@name = name
|
98
|
+
@type = type
|
99
|
+
@is_common = is_common
|
100
|
+
end
|
101
|
+
|
102
|
+
attr_reader :name
|
103
|
+
attr_reader :type
|
104
|
+
|
105
|
+
def common?
|
106
|
+
@is_common
|
107
|
+
end
|
108
|
+
|
109
|
+
def strdef
|
110
|
+
if type.str_length
|
111
|
+
"#{name}: !#{type.name} #{type.str_length}"
|
112
|
+
else
|
113
|
+
"#{name}: !#{type.name}"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# @param definition_path A directory path it contains log definitions
|
119
|
+
def initialize(definition_path)
|
120
|
+
@definition_path = definition_path
|
121
|
+
end
|
122
|
+
|
123
|
+
# Generate whole strdef from current log definitions
|
124
|
+
# @return String
|
125
|
+
def generate
|
126
|
+
table = load_table_definition(@definition_path)
|
127
|
+
generate_strdef(table)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Generate strdef diff with existing strdef
|
131
|
+
# @param String current_strdef strdef to diff with
|
132
|
+
# @return String diff
|
133
|
+
def diff(current_strdef)
|
134
|
+
table = load_table_definition(@definition_path)
|
135
|
+
generate_strdef_diff(current_strdef, table)
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
def load_table_definition(path_prefix)
|
140
|
+
table = LogTable.new
|
141
|
+
|
142
|
+
categories = Daifuku::Compiler.new.compile(path_prefix)
|
143
|
+
|
144
|
+
if common = categories.delete('common')
|
145
|
+
common.common_columns.each do |column|
|
146
|
+
table.add_common_column(column)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
categories.each do |category_name, category|
|
151
|
+
category.common_columns.each do |column|
|
152
|
+
table.add_column column, "#{category_name}:common"
|
153
|
+
end
|
154
|
+
category.events.each do |event_name, event|
|
155
|
+
event.columns.each do |column|
|
156
|
+
table.add_column column, category_name
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
table
|
162
|
+
end
|
163
|
+
|
164
|
+
def generate_strdef(table)
|
165
|
+
lines = ["columns:"]
|
166
|
+
table.each_column do |column|
|
167
|
+
lines << " - #{column.strdef}"
|
168
|
+
end
|
169
|
+
lines.join("\n") + "\n"
|
170
|
+
end
|
171
|
+
|
172
|
+
def generate_strdef_diff(strdef, table)
|
173
|
+
strdef = StreamDefinition.parse(strdef)
|
174
|
+
|
175
|
+
lines = []
|
176
|
+
found = false
|
177
|
+
table.each_column do |column|
|
178
|
+
unless strdef[column.name]
|
179
|
+
lines << "- #{column.strdef}"
|
180
|
+
found = true
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
unless found
|
185
|
+
$stderr.puts %Q(INFO: no new column added)
|
186
|
+
end
|
187
|
+
|
188
|
+
strdef.each_column do |column|
|
189
|
+
if not table[column.name] and not LogTable.predefined_column?(column.name)
|
190
|
+
$stderr.puts %Q(INFO: column "#{column.name}" is no longer generated)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
lines.join("\n") + "\n"
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|