daifuku 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|