k_log 0.0.17 → 0.0.27

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 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