k_log 0.0.17 → 0.0.27

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c4551d1ecfce7599e235a86b566c53b9f35a117b9bf6ab1f9ea6a3b4805faeff
4
- data.tar.gz: 633eba7eea7ed71275ed144230392882f88b48697ecc48c054cf12cbcee1a1bb
3
+ metadata.gz: 2035ee429ccfbb7cd0307d243e9b8bd303f44444d1b356c28ad079bc8219de8f
4
+ data.tar.gz: c7eeff9ee82d1514859052d264cfcb06216ad939f3c215c34dfee6f64ef0d21f
5
5
  SHA512:
6
- metadata.gz: 68d4bb47ff1833c00a9a953744de24c04ae6d6418f4cf62587b24a9ac9b8ef589f3c2e4fd64a933e7a9c29d5a2bb031b40a28a2d7aa0c4b2375bc24e7d180246
7
- data.tar.gz: 6dae6d21f7f1a229018d796bfd1b2a92f6e9cd93017d7e6f337772b952fa9248a19a60b4feed7c74217fd86d85299d55eb9e5e8f41a650efb822595fd325c32d
6
+ metadata.gz: f12d2c0fae01798339647d3905fefe8e5e63049d074965c04b878e3ba4df7b8571cba637877395f8fa033d2c780d53bfc4aaa2da605af647908fdb7e29bd1e3a
7
+ data.tar.gz: 3d780ecca23bac05227ff96629c464f3ecf2ed61f908a6f28145e9b00786fae625e37d751210ccaaf03d613e1683bbfbb15cf009d35e57909ac3110488b34b0f
data/.rubocop.yml CHANGED
@@ -40,6 +40,8 @@ Layout/LineLength:
40
40
  # Ignores annotate output
41
41
  IgnoredPatterns: ['\A# \*\*']
42
42
  IgnoreCopDirectives: true
43
+ Exclude:
44
+ - "**/spec/**/*"
43
45
 
44
46
  Lint/UnusedMethodArgument:
45
47
  AllowUnusedKeywordArguments: true
@@ -52,12 +54,25 @@ Style/BlockComments:
52
54
  Include:
53
55
  - "**/spec/*"
54
56
 
57
+ Layout/EndOfLine:
58
+ EnforcedStyle: lf
59
+
55
60
  # My Preferences - Start
56
61
  Metrics/ClassLength:
57
62
  Enabled: false
58
63
  Metrics/ModuleLength:
59
64
  Exclude:
60
65
  - "**/spec/**/*"
66
+ Metrics/CyclomaticComplexity:
67
+ Exclude:
68
+ - "lib/k_log/log_structure.rb"
69
+ Metrics/PerceivedComplexity:
70
+ Exclude:
71
+ - "lib/k_log/log_structure.rb"
72
+ Metrics/AbcSize:
73
+ Exclude:
74
+ - "lib/k_log/log_structure.rb"
75
+
61
76
  Naming/MemoizedInstanceVariableName:
62
77
  Enabled: false
63
78
  Naming/VariableNumber:
data/Gemfile CHANGED
@@ -2,23 +2,18 @@
2
2
 
3
3
  source 'https://rubygems.org'
4
4
 
5
- # Specify your gem's dependencies in handlebars_helpers.gemspec
6
5
  gemspec
7
6
 
8
- # group :development do
9
- # # Currently conflicts with GitHub actions and so I remove it on push
10
- # # pry on steroids
11
- # gem 'jazz_fingers'
12
- # gem 'pry-coolline', github: 'owst/pry-coolline', branch: 'support_new_pry_config_api'
13
- # end
14
-
15
7
  group :development, :test do
8
+ gem 'dry-struct', '~> 1'
9
+
16
10
  gem 'guard-bundler'
17
11
  gem 'guard-rspec'
18
12
  gem 'guard-rubocop'
19
13
  gem 'rake', '~> 12.0'
20
14
  gem 'rake-compiler', require: false
21
15
  gem 'rspec', '~> 3.0'
16
+ gem 'rspec-collection_matchers'
22
17
  gem 'rubocop'
23
18
  gem 'rubocop-rake', require: false
24
19
  gem 'rubocop-rspec', require: false
data/k_log.gemspec CHANGED
@@ -38,6 +38,7 @@ Gem::Specification.new do |spec|
38
38
  spec.require_paths = ['lib']
39
39
  # spec.extensions = ['ext/k_log/extconf.rb']
40
40
 
41
+ spec.add_dependency 'k_util', '~> 0'
41
42
  spec.add_dependency 'table_print', '~> 1.5.7'
42
43
  # spec.add_dependency 'tty-box', '~> 0.5.0'
43
44
  end
@@ -25,6 +25,15 @@ module KLog
25
25
  log.heading('Heading')
26
26
  log.subheading('Sub Heading')
27
27
  log.section_heading('Section Heading')
28
+
29
+ data = OpenStruct.new
30
+ data.title = 'Software Architect'
31
+ data.age = 45
32
+ data.name = 'David'
33
+ data.names = %w[David Bill]
34
+ data.status = :debug
35
+ data.statuses = %i[debug info blah]
36
+ log.open_struct(data, section_heading: 'Display Open Struct')
28
37
  end
29
38
 
30
39
  def examples_complex
@@ -43,6 +43,14 @@ module KLog
43
43
  green(character * size)
44
44
  end
45
45
 
46
+ def self.dynamic_heading(heading, size: 70, type: :heading)
47
+ return heading(heading, size) if type == :heading
48
+ return subheading(heading, size) if type == :subheading
49
+ return [section_heading(heading, size)] if %i[section_heading section].include?(type)
50
+
51
+ []
52
+ end
53
+
46
54
  def self.heading(heading, size = 70)
47
55
  line = line(size)
48
56
 
@@ -69,7 +77,8 @@ module KLog
69
77
  def self.section_heading(heading, size = 70)
70
78
  brace_open = green('[ ')
71
79
  brace_close = green(' ]')
72
- line = line(size - heading.length - 4, '-')
80
+ line_length = size - heading.length - 4
81
+ line = line_length.positive? ? line(line_length, '-') : ''
73
82
 
74
83
  # It is important that you set the colour after you have calculated the size
75
84
  "#{brace_open}#{heading}#{brace_close}#{green(line)}"
@@ -89,7 +98,7 @@ module KLog
89
98
 
90
99
  unless title.nil?
91
100
  result.push(title)
92
- result.push(line(70, ','))
101
+ result.push(line(70, '-'))
93
102
  end
94
103
 
95
104
  result.push messages if messages.is_a?(String) || messages.is_a?(Integer)
@@ -106,8 +115,60 @@ module KLog
106
115
  end
107
116
  # rubocop:enable Metrics/CyclomaticComplexity
108
117
 
118
+ def self.red(value)
119
+ "\033[31m#{value}\033[0m"
120
+ end
121
+
109
122
  def self.green(value)
110
123
  "\033[32m#{value}\033[0m"
111
124
  end
125
+
126
+ def self.yellow(value)
127
+ "\033[33m#{value}\033[0m"
128
+ end
129
+
130
+ def self.blue(value)
131
+ "\033[34m#{value}\033[0m"
132
+ end
133
+
134
+ def self.purple(value)
135
+ "\033[35m#{value}\033[0m"
136
+ end
137
+
138
+ def self.cyan(value)
139
+ "\033[36m#{value}\033[0m"
140
+ end
141
+
142
+ def self.grey(value)
143
+ "\033[37m#{value}\033[0m"
144
+ end
145
+
146
+ def self.bg_red(value)
147
+ "\033[41m#{value}\033[0m"
148
+ end
149
+
150
+ def self.bg_green(value)
151
+ "\033[42m#{value}\033[0m"
152
+ end
153
+
154
+ def self.bg_yellow(value)
155
+ "\033[43m#{value}\033[0m"
156
+ end
157
+
158
+ def self.bg_blue(value)
159
+ "\033[44m#{value}\033[0m"
160
+ end
161
+
162
+ def self.bg_purple(value)
163
+ "\033[45m#{value}\033[0m"
164
+ end
165
+
166
+ def self.bg_cyan(value)
167
+ "\033[46m#{value}\033[0m"
168
+ end
169
+
170
+ def self.bg_grey(value)
171
+ "\033[47m#{value}\033[0m"
172
+ end
112
173
  end
113
174
  end
@@ -0,0 +1,399 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'k_util'
4
+
5
+ module KLog
6
+ # Log Structure is flexible logger for working through a complex object graph
7
+ class LogStructure
8
+ attr_reader :indent
9
+ attr_reader :title
10
+ attr_reader :title_type
11
+ attr_reader :heading
12
+ attr_reader :heading_type
13
+ attr_reader :line_width
14
+ attr_reader :graph
15
+ attr_reader :formatter
16
+ attr_reader :convert_data_to
17
+
18
+ attr_reader :recursion_depth
19
+ attr_reader :key_format
20
+ attr_reader :graph_path
21
+ attr_reader :graph_node
22
+
23
+ attr_reader :lines
24
+ attr_reader :output_as
25
+ attr_reader :output_file
26
+
27
+ # Log a structure
28
+ #
29
+ # Can handle Hash, Array, OpenStruct, Struct, DryStruct, Hash convertible custom classes
30
+ #
31
+ # @option opts [String] :indent Indent with string, defaults to ' '
32
+ # @option opts [String] :heading Log heading using logger.dynamic_heading
33
+ # @option opts [String] :heading_type :heading, :subheading, :section_heading
34
+ # @option opts [String] :line_width line width defaults to 80, but can be overridden here
35
+ # @option opts [String] :formatter is a complex configuration for formatting different data within the structure
36
+ def initialize(opts)
37
+ @indent = opts[:indent] || ' '
38
+ @title = opts[:title]
39
+ @title_type = opts[:title_type] || :heading
40
+
41
+ @heading = opts[:heading]
42
+ @heading_type = opts[:heading_type] || :heading
43
+ puts ':heading should be :title' if opts[:heading]
44
+ puts ':heading_type should be :title_type' if opts[:heading_type]
45
+
46
+ @formatter = opts[:formatter] || {}
47
+ @graph = parse_graph(opts[:graph] || {})
48
+ @convert_data_to = opts[:convert_data_to] || :raw # by default leave data as is
49
+
50
+ @line_width = opts[:line_width] || 80
51
+ @output_as = opts[:output_as] || [:console]
52
+ @output_as = [@output_as] unless @output_as.is_a?(Array)
53
+ @output_file = opts[:output_file]
54
+
55
+ @recursion_depth = 0
56
+ @key_format = nil
57
+ @graph_path = []
58
+ @lines = []
59
+
60
+ update_indent_label
61
+ end
62
+
63
+ def l
64
+ @l ||= KLog::LogUtil.new(KLog.logger)
65
+ end
66
+
67
+ def log(data)
68
+ log_heading(title, title_type) if title
69
+
70
+ data = convert_data(data)
71
+
72
+ log_data(data)
73
+
74
+ add_line(KLog::LogHelper.line(line_width))
75
+
76
+ render_output
77
+ end
78
+
79
+ def content
80
+ @content ||= lines.join("\n")
81
+ end
82
+
83
+ def clean_content
84
+ # remove color escape codes
85
+ @clean_content ||= content.gsub(/\x1B\[\d*m/, '')
86
+ end
87
+
88
+ def clean_lines
89
+ # remove color escape codes
90
+ lines.flat_map { |line| line.gsub(/\x1B\[\d*m/, '').split("\n") }
91
+ end
92
+
93
+ def add_lines(lines)
94
+ @lines += lines
95
+ end
96
+
97
+ def add_line(line)
98
+ @lines << line
99
+ end
100
+
101
+ private
102
+
103
+ # format_config = @formatter[:_root] if format_config.nil? && @recursion_depth.zero?
104
+
105
+ def log_data(data)
106
+ data.each_pair do |k, v|
107
+ key = k.is_a?(String) ? k.to_sym : k
108
+
109
+ graph_path.push(key)
110
+ @graph_node = GraphNode.for(graph, graph_path)
111
+ # l.kv 'key', "#{key.to_s.ljust(15)}#{graph_node.skip?.to_s.ljust(6)}#{@recursion_depth}"
112
+
113
+ if graph_node.skip?
114
+ # l.kv 'key', 'skipping...'
115
+ @graph_path.pop
116
+ next
117
+ end
118
+
119
+ value = graph_node.transform? ? graph_node.transform(v) : v
120
+
121
+ case value
122
+ when OpenStruct
123
+ # l.kv 'go', 'open struct ->'
124
+ log_structure(key, value)
125
+ # l.kv 'go', 'open struct <-'
126
+ when Array
127
+ # l.kv 'go', 'array ->'
128
+ log_array(key, value)
129
+ # l.kv 'go', 'array <-'
130
+ else
131
+ # l.kv 'go', 'value ->'
132
+ log_heading(graph_node.heading, graph_node.heading_type) if graph_node.heading
133
+ add_line KLog::LogHelper.kv "#{@indent_label}#{key}", value
134
+ # l.kv 'go', 'value <-'
135
+ end
136
+
137
+ # l.line
138
+ # @graph_node = graph.for_path(graph_path)
139
+ # l.line
140
+ @graph_path.pop
141
+ end
142
+ nil
143
+ end
144
+
145
+ def log_structure(key, value)
146
+ log_heading(graph_node.heading, graph_node.heading_type) if graph_node.heading
147
+ add_line(KLog::LogHelper.green("#{@indent_label}#{key}"))
148
+ log_child_data(value)
149
+ end
150
+
151
+ def log_child_data(value)
152
+ depth_down
153
+ log_data(value)
154
+ depth_up
155
+ end
156
+
157
+ def log_array(key, array)
158
+ # return if items.length.zero? && graph_node.skip_empty?
159
+
160
+ items = array.clone
161
+ items.select! { |item| graph_node.filter(item) } if graph_node.filter?
162
+ items = items.take(graph_node.take) if graph_node.limited?
163
+ items.sort!(&graph_node.sort) if graph_node.sort?
164
+
165
+ return if items.length.zero? && graph_node.skip_empty?
166
+
167
+ log_heading(graph_node.heading, graph_node.heading_type) if graph_node.heading
168
+
169
+ if primitive?(items)
170
+ add_line KLog::LogHelper.kv "#{@indent_label}#{key}", items.map(&:to_s).join(', ')
171
+ else
172
+ table_print items, tp_columns(items)
173
+ end
174
+ rescue StandardError
175
+ #
176
+ end
177
+
178
+ def table_print(items, columns)
179
+ io = TablePrintIo.new(self)
180
+
181
+ tp.set :io, io
182
+ tp items, columns
183
+ tp.clear :io
184
+ end
185
+
186
+ def primitive?(items)
187
+ item = items.first
188
+ KUtil.data.basic_type?(item)
189
+ end
190
+
191
+ def log_heading(heading, heading_type)
192
+ add_lines(KLog::LogHelper.dynamic_heading(heading, size: line_width, type: heading_type))
193
+ end
194
+
195
+ def tp_columns(items)
196
+ # Use configured array columns
197
+ return graph_node.columns if graph_node.columns
198
+
199
+ # Slow but complete list of keys
200
+ # items.flat_map { |v| v.to_h.keys }.uniq
201
+
202
+ items.first.to_h.keys
203
+ end
204
+
205
+ def update_indent_label
206
+ # puts "indent_label: #{indent} - #{@recursion_depth} - #{(indent * @recursion_depth)}"
207
+ @indent_label = (indent * @recursion_depth)
208
+ end
209
+
210
+ def indent_in
211
+ @indent = "#{@indent} "
212
+ end
213
+
214
+ def indent_out
215
+ @indent = indent.chomp(' ')
216
+ end
217
+
218
+ def depth_down
219
+ @recursion_depth = recursion_depth + 1
220
+ update_indent_label
221
+ end
222
+
223
+ def depth_up
224
+ @recursion_depth = recursion_depth - 1
225
+ update_indent_label
226
+ end
227
+
228
+ def render_output
229
+ puts content if output_as.include?(:console)
230
+ File.write(output_file, clean_content) if output_as.include?(:file) && output_file
231
+ end
232
+
233
+ # convert_data_to: :open_struct
234
+ def convert_data(data)
235
+ return KUtil.data.to_open_struct(data) if convert_data_to == :open_struct
236
+
237
+ data
238
+ end
239
+
240
+ def parse_graph(data)
241
+ if data.is_a?(Hash)
242
+ transform_hash = data.each_with_object({}) do |(key, value), new_hash|
243
+ new_hash[key] = if key == :columns && value.is_a?(Array)
244
+ # Don't transform the table_print GEM columns definition as it must stay as a hash
245
+ value
246
+ else
247
+ parse_graph(value)
248
+ end
249
+ end
250
+
251
+ return OpenStruct.new(transform_hash.to_h)
252
+ end
253
+
254
+ return data.map { |o| parse_graph(o) } if data.is_a?(Array)
255
+ return parse_graph(data.to_h) if data.respond_to?(:to_h) # hash_convertible?(data)
256
+
257
+ # Some primitave type: String, True/False or an ObjectStruct
258
+ data
259
+ end
260
+
261
+ # def hash_convertible?(value)
262
+ # # Nil is a special case, it responds to :to_h but generally
263
+ # # you only want to convert nil to {} in specific scenarios
264
+ # return false if value.nil?
265
+
266
+ # value.is_a?(Array) ||
267
+ # value.is_a?(Hash) ||
268
+ # value.is_a?(Struct) ||
269
+ # value.is_a?(OpenStruct) ||
270
+ # value.respond_to?(:to_h)
271
+ # end
272
+
273
+ # Format configuration for a specific key
274
+ #
275
+ # @example Example configuration for key: tables
276
+ #
277
+ # configuration = {
278
+ # tables: {
279
+ # heading: 'Database Tables',
280
+ # take: :all,
281
+ # columns: [
282
+ # :name,
283
+ # :force,
284
+ # :primary_key,
285
+ # :id,
286
+ # columns: { display_method: lambda { |row| row.columns.map { |c| c.name }.join(', ') }, width: 100 }
287
+ # ]
288
+ # },
289
+ # people: {
290
+ # ... people configuration goes here
291
+ # }
292
+ # }
293
+ #
294
+
295
+ # Override table_print IO stream so that it writes into the structure
296
+ class TablePrintIo
297
+ def initialize(log_structure)
298
+ @log_structure = log_structure
299
+ end
300
+
301
+ def puts(line)
302
+ @log_structure.add_line(line)
303
+ end
304
+ end
305
+
306
+ class GraphNode
307
+ attr_accessor :config
308
+
309
+ class << self
310
+ def null
311
+ @null ||= OpenStruct.new
312
+ end
313
+
314
+ def for(graph, graph_path)
315
+ # node_config = graph_path.inject(graph, :send) # (uses deep nesting, but fails when nil is returned) https://stackoverflow.com/questions/15862455/ruby-nested-send
316
+ # node.nil? ? null : node.send(name) || null
317
+ node_config = graph_path.reduce(graph) do |node, name|
318
+ result = node.send(name)
319
+
320
+ break null if result.nil?
321
+
322
+ result
323
+ end
324
+
325
+ new(node_config)
326
+ end
327
+ end
328
+
329
+ def initialize(config)
330
+ @config = config || OpenStruct.new
331
+ end
332
+
333
+ # table_print compatible configuration for displaying columns for an array
334
+ def columns
335
+ config.columns
336
+ end
337
+
338
+ # Optional heading for the node
339
+ def heading
340
+ config.heading
341
+ end
342
+
343
+ # Type of heading [:heading, :subheading, :section]
344
+ def heading_type
345
+ config.heading_type || :section
346
+ end
347
+
348
+ # Node data is to be transformed
349
+ def transform?
350
+ config&.transform.respond_to?(:call)
351
+ end
352
+
353
+ # Transform node value
354
+ def transform(value)
355
+ config.transform.call(value)
356
+ end
357
+
358
+ # Array rows are filtered
359
+ def filter?
360
+ config&.filter.respond_to?(:call)
361
+ end
362
+
363
+ # Array rows are filtered via this predicate
364
+ def filter(value)
365
+ config.filter.call(value)
366
+ end
367
+
368
+ # How any array rows to take
369
+ def take
370
+ config.take
371
+ end
372
+
373
+ # Array rows are limited, see take
374
+ def limited?
375
+ config.take&.is_a?(Integer)
376
+ end
377
+
378
+ # Array rows are sorted using .sort
379
+ def sort?
380
+ config&.sort.respond_to?(:call)
381
+ end
382
+
383
+ # Use array.sort?
384
+ def sort
385
+ config.sort
386
+ end
387
+
388
+ # Skip this node
389
+ def skip?
390
+ config.skip == true
391
+ end
392
+
393
+ # Skip empty array node (my be useful for other nodes, but not yet)
394
+ def skip_empty?
395
+ config.skip_empty == true
396
+ end
397
+ end
398
+ end
399
+ end