spoom 1.6.3 → 1.7.1

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: 4e47e1d01da6e3ee465eccc91bb4e3cd29549bb06d585ec0c3b6c6c129120a56
4
- data.tar.gz: fbff7dfda675c426510d4147ab5ebaa281eb3480eb5bf8c09648b143f46a9c40
3
+ metadata.gz: 87a613b9da2addba3344ec53859a248cfbdc6805f7e24a87797b92f0487a1697
4
+ data.tar.gz: 61b5686efef0f8bc03ff948ef5053330b067318d1535ffbdcdc84b0b29ccc80a
5
5
  SHA512:
6
- metadata.gz: 2e555b99d0fca2a6e4a2da4cc5b36313d8823d4736c205187f52bab79670e839251cd22f47ba525c171ca3cff6dc59559b95969f3c7fc6aa103ff7d38f1144fa
7
- data.tar.gz: 8a2c8b5e18b55c1f5813e33f2ebe03c86610de90c96b6d805bded2f79278da2897862a97bf70de5843135086373435e22bffc71f10b9aaf71b67d118797731e7
6
+ metadata.gz: e70eef5d6ccd57d8ae2eb766493cf16ba26eee6ed5e210c0340e87c11e7c7f80881cdd3e1513bff1fe7a46417972709f1f65988ebd254a624cffc004d7f8459a
7
+ data.tar.gz: ecf60bcaebaf6cd10498762858a47690ec0550b8259659fcef5af635090cf82120826fb8e15163610ed0497bc62920175815325715b893460910af4766b74fe0
@@ -19,7 +19,7 @@ module Spoom
19
19
  "in `#{files.size}` file#{files.size == 1 ? "" : "s"}...\n\n")
20
20
 
21
21
  transformed_files = transform_files(files) do |file, contents|
22
- Spoom::Sorbet::Assertions.rbi_to_rbs(contents, file: file)
22
+ Spoom::Sorbet::Translate.sorbet_assertions_to_rbs_comments(contents, file: file)
23
23
  end
24
24
 
25
25
  say("Translated type assertions in `#{transformed_files}` file#{transformed_files == 1 ? "" : "s"}.")
@@ -0,0 +1,68 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ module Cli
6
+ module Srb
7
+ class Metrics < Thor
8
+ include Helper
9
+
10
+ default_task :show
11
+
12
+ desc "show", "Show metrics about Sorbet usage"
13
+ option :dump, type: :boolean, default: false
14
+ def show(*paths)
15
+ files = collect_files(paths)
16
+ metrics = Spoom::Sorbet::Metrics.collect_code_metrics(files)
17
+
18
+ if options[:dump]
19
+ metrics.sort_by { |key, _value| key }.each do |key, value|
20
+ puts "#{key} #{value}"
21
+ end
22
+
23
+ return
24
+ end
25
+
26
+ say("Files: `#{files.size}`")
27
+
28
+ ["classes", "modules", "singleton_classes"].each do |key|
29
+ value = metrics[key]
30
+ next if value == 0
31
+
32
+ say("\n#{key.capitalize}: `#{value}`")
33
+ ["with_srb_type_params", "with_rbs_type_params"].each do |subkey|
34
+ say(" * #{subkey.gsub("_", " ")}: `#{metrics["#{key}_#{subkey}"]}`")
35
+ end
36
+ end
37
+
38
+ ["methods", "accessors"].each do |key|
39
+ value = metrics[key]
40
+ next if value == 0
41
+
42
+ say("\n#{key.capitalize}: `#{value}`")
43
+ ["without_sig", "with_srb_sig", "with_rbs_sig"].each do |subkey|
44
+ say(" * #{subkey.gsub("_", " ")}: `#{metrics["#{key}_#{subkey}"]}`")
45
+ end
46
+ end
47
+
48
+ say("\nT. calls: `#{metrics["T_calls"]}`")
49
+ metrics
50
+ .select { |key, _value| key.start_with?("T.") }
51
+ .sort_by { |_key, value| -value }
52
+ .each do |key, value|
53
+ say(" * #{key}: `#{value}`")
54
+ end
55
+
56
+ say("\nRBS Assertions: `#{metrics["rbs_assertions"]}`")
57
+ metrics
58
+ .reject { |key, _value| key == "rbs_assertions" }
59
+ .select { |key, _value| key.start_with?("rbs_") }
60
+ .sort_by { |_key, value| -value }
61
+ .each do |key, value|
62
+ say(" * #{key}: `#{value}`")
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -1,8 +1,6 @@
1
1
  # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
- require "spoom/sorbet/sigs"
5
-
6
4
  module Spoom
7
5
  module Cli
8
6
  module Srb
@@ -34,12 +32,12 @@ module Spoom
34
32
 
35
33
  case from
36
34
  when "rbi"
37
- transformed_files = transform_files(files) do |_file, contents|
38
- Spoom::Sorbet::Sigs.rbi_to_rbs(contents, positional_names: options[:positional_names])
35
+ transformed_files = transform_files(files) do |file, contents|
36
+ Spoom::Sorbet::Translate.sorbet_sigs_to_rbs_comments(contents, file: file, positional_names: options[:positional_names])
39
37
  end
40
38
  when "rbs"
41
- transformed_files = transform_files(files) do |_file, contents|
42
- Spoom::Sorbet::Sigs.rbs_to_rbi(contents)
39
+ transformed_files = transform_files(files) do |file, contents|
40
+ Spoom::Sorbet::Translate.rbs_comments_to_sorbet_sigs(contents, file: file)
43
41
  end
44
42
  end
45
43
 
@@ -52,8 +50,8 @@ module Spoom
52
50
 
53
51
  say("Stripping signatures from `#{files.size}` file#{files.size == 1 ? "" : "s"}...\n\n")
54
52
 
55
- transformed_files = transform_files(files) do |_file, contents|
56
- Spoom::Sorbet::Sigs.strip(contents)
53
+ transformed_files = transform_files(files) do |file, contents|
54
+ Spoom::Sorbet::Translate.strip_sorbet_sigs(contents, file: file)
57
55
  end
58
56
 
59
57
  say("Stripped signatures from `#{transformed_files}` file#{transformed_files == 1 ? "" : "s"}.")
@@ -93,8 +91,8 @@ module Spoom
93
91
  # Then, we transform the copied files to translate all the RBS signatures into RBI signatures.
94
92
  say("Translating signatures from RBS to RBI...")
95
93
  files = collect_files([copy_context.absolute_path])
96
- transform_files(files) do |_file, contents|
97
- Spoom::Sorbet::Sigs.rbs_to_rbi(contents)
94
+ transform_files(files) do |file, contents|
95
+ Spoom::Sorbet::Translate.rbs_comments_to_sorbet_sigs(contents, file: file)
98
96
  end
99
97
 
100
98
  # We need to inject `extend T::Sig` to be sure all the classes can run the `sig{}` blocks.
data/lib/spoom/cli/srb.rb CHANGED
@@ -5,6 +5,7 @@ require_relative "srb/assertions"
5
5
  require_relative "srb/bump"
6
6
  require_relative "srb/coverage"
7
7
  require_relative "srb/lsp"
8
+ require_relative "srb/metrics"
8
9
  require_relative "srb/sigs"
9
10
  require_relative "srb/tc"
10
11
 
@@ -24,6 +25,9 @@ module Spoom
24
25
  desc "lsp", "Send LSP requests to Sorbet"
25
26
  subcommand "lsp", Spoom::Cli::Srb::LSP
26
27
 
28
+ desc "metrics", "Collect metrics about Sorbet usage"
29
+ subcommand "metrics", Spoom::Cli::Srb::Metrics
30
+
27
31
  desc "sigs", "Translate signatures from/to RBI and RBS"
28
32
  subcommand "sigs", Spoom::Cli::Srb::Sigs
29
33
 
@@ -48,7 +48,7 @@ module Spoom
48
48
  return unless file?(metrics_file)
49
49
 
50
50
  metrics_path = absolute_path_to(metrics_file)
51
- metrics = Spoom::Sorbet::MetricsParser.parse_file(metrics_path)
51
+ metrics = Spoom::Sorbet::Metrics::MetricsFileParser.parse_file(metrics_path)
52
52
  remove!(metrics_file)
53
53
  metrics
54
54
  end
@@ -0,0 +1,22 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ #: [K = String, V = Integer, Elem = [String, Integer]]
6
+ class Counters < Hash
7
+ #: -> void
8
+ def initialize
9
+ super(0)
10
+ end
11
+
12
+ #: (String) -> void
13
+ def increment(key)
14
+ self[key] += 1
15
+ end
16
+
17
+ #: (String) -> Integer
18
+ def [](key)
19
+ super(key) #: as Integer
20
+ end
21
+ end
22
+ end
@@ -49,10 +49,10 @@ module Spoom
49
49
 
50
50
  #: (String rb, file: String, ?plugins: Array[Plugins::Base]) -> void
51
51
  def index_ruby(rb, file:, plugins: [])
52
- node, comments = Spoom.parse_ruby_with_comments(rb, file: file)
52
+ node = Spoom.parse_ruby(rb, file: file, comments: true)
53
53
 
54
54
  # Index definitions
55
- model_builder = Model::Builder.new(@model, file, comments: comments)
55
+ model_builder = Model::Builder.new(@model, file)
56
56
  model_builder.visit(node)
57
57
 
58
58
  # Index references
@@ -41,6 +41,11 @@ module Spoom
41
41
  "before_validation",
42
42
  ].freeze #: Array[String]
43
43
 
44
+ CALLBACK_CONDITIONS = [
45
+ "if",
46
+ "unless",
47
+ ].freeze #: Array[String]
48
+
44
49
  CRUD_METHODS = [
45
50
  "assign_attributes",
46
51
  "create",
@@ -63,9 +68,23 @@ module Spoom
63
68
  #: (Send send) -> void
64
69
  def on_send(send)
65
70
  if send.recv.nil? && CALLBACKS.include?(send.name)
71
+ # Process direct symbol arguments
66
72
  send.each_arg(Prism::SymbolNode) do |arg|
67
73
  @index.reference_method(arg.unescaped, send.location)
68
74
  end
75
+
76
+ # Process hash arguments for conditions like if: :method_name
77
+ send.each_arg_assoc do |key, value|
78
+ key = key.slice.delete_suffix(":")
79
+
80
+ case key
81
+ when *CALLBACK_CONDITIONS
82
+ if value&.is_a?(Prism::SymbolNode)
83
+ @index.reference_method(value.unescaped, send.location)
84
+ end
85
+ end
86
+ end
87
+
69
88
  return
70
89
  end
71
90
 
@@ -5,15 +5,12 @@ module Spoom
5
5
  class Model
6
6
  # Populate a Model by visiting the nodes from a Ruby file
7
7
  class Builder < NamespaceVisitor
8
- #: (Model model, String file, ?comments: Array[Prism::Comment]) -> void
9
- def initialize(model, file, comments:)
8
+ #: (Model model, String file) -> void
9
+ def initialize(model, file)
10
10
  super()
11
11
 
12
12
  @model = model
13
13
  @file = file
14
- @comments_by_line = comments.to_h do |c|
15
- [c.location.start_line, c]
16
- end #: Hash[Integer, Prism::Comment]
17
14
  @namespace_nesting = [] #: Array[Namespace]
18
15
  @visibility_stack = [Visibility::Public] #: Array[Visibility]
19
16
  @last_sigs = [] #: Array[Sig]
@@ -263,22 +260,20 @@ module Spoom
263
260
 
264
261
  #: (Prism::Node node) -> Array[Comment]
265
262
  def node_comments(node)
263
+ last_line = node.location.start_line
266
264
  comments = []
267
265
 
268
- start_line = node.location.start_line
269
- start_line -= 1 unless @comments_by_line.key?(start_line)
266
+ node.location.leading_comments.reverse_each do |comment|
267
+ if comment.location.start_line < last_line - 1
268
+ break
269
+ end
270
270
 
271
- start_line.downto(1) do |line|
272
- comment = @comments_by_line[line]
273
- break unless comment
271
+ last_line = comment.location.start_line
274
272
 
275
- spoom_comment = Comment.new(
273
+ comments.unshift(Comment.new(
276
274
  comment.slice.gsub(/^#\s?/, "").rstrip,
277
275
  Location.from_prism(@file, comment.location),
278
- )
279
-
280
- comments.unshift(spoom_comment)
281
- @comments_by_line.delete(line)
276
+ ))
282
277
  end
283
278
 
284
279
  comments
@@ -84,7 +84,7 @@ module Spoom
84
84
  #: Array[Comment]
85
85
  attr_reader :comments
86
86
 
87
- #: (Symbol symbol, owner: Namespace?, location: Location, ?comments: Array[Comment]) -> void
87
+ #: (Symbol symbol, owner: Namespace?, location: Location, comments: Array[Comment]) -> void
88
88
  def initialize(symbol, owner:, location:, comments:)
89
89
  @symbol = symbol
90
90
  @owner = owner
data/lib/spoom/parse.rb CHANGED
@@ -7,8 +7,8 @@ module Spoom
7
7
  class ParseError < Error; end
8
8
 
9
9
  class << self
10
- #: (String ruby, file: String) -> Prism::Node
11
- def parse_ruby(ruby, file:)
10
+ #: (String ruby, file: String, ?comments: bool) -> Prism::Node
11
+ def parse_ruby(ruby, file:, comments: false)
12
12
  result = Prism.parse(ruby)
13
13
  unless result.success?
14
14
  message = +"Error while parsing #{file}:\n"
@@ -20,23 +20,9 @@ module Spoom
20
20
  raise ParseError, message
21
21
  end
22
22
 
23
- result.value
24
- end
25
-
26
- #: (String ruby, file: String) -> [Prism::Node, Array[Prism::Comment]]
27
- def parse_ruby_with_comments(ruby, file:)
28
- result = Prism.parse(ruby)
29
- unless result.success?
30
- message = +"Error while parsing #{file}:\n"
31
-
32
- result.errors.each do |e|
33
- message << "- #{e.message} (at #{e.location.start_line}:#{e.location.start_column})\n"
34
- end
23
+ result.attach_comments! if comments
35
24
 
36
- raise ParseError, message
37
- end
38
-
39
- [result.value, result.comments]
25
+ result.value
40
26
  end
41
27
  end
42
28
  end
data/lib/spoom/rbs.rb ADDED
@@ -0,0 +1,77 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ module RBS
6
+ class Comments
7
+ #: Array[Annotations]
8
+ attr_reader :annotations
9
+
10
+ #: Array[Signature]
11
+ attr_reader :signatures
12
+
13
+ #: -> void
14
+ def initialize
15
+ @annotations = [] #: Array[Annotations]
16
+ @signatures = [] #: Array[Signature]
17
+ end
18
+
19
+ #: -> bool
20
+ def empty?
21
+ @annotations.empty? && @signatures.empty?
22
+ end
23
+ end
24
+
25
+ class Comment
26
+ #: String
27
+ attr_reader :string
28
+
29
+ #: Prism::Location
30
+ attr_reader :location
31
+
32
+ #: (String, Prism::Location) -> void
33
+ def initialize(string, location)
34
+ @string = string
35
+ @location = location
36
+ end
37
+ end
38
+
39
+ class Annotations < Comment; end
40
+ class Signature < Comment; end
41
+
42
+ module ExtractRBSComments
43
+ #: (Prism::Node) -> Comments
44
+ def node_rbs_comments(node)
45
+ res = Comments.new
46
+
47
+ comments = node.location.leading_comments.reverse
48
+ return res if comments.empty?
49
+
50
+ continuation_comments = [] #: Array[Prism::Comment]
51
+
52
+ comments.each do |comment|
53
+ string = comment.slice
54
+
55
+ if string.start_with?("# @")
56
+ string = string.delete_prefix("#").strip
57
+ res.annotations << Annotations.new(string, comment.location)
58
+ elsif string.start_with?("#: ")
59
+ string = string.delete_prefix("#:").strip
60
+ location = comment.location
61
+
62
+ continuation_comments.reverse_each do |continuation_comment|
63
+ string = "#{string}#{continuation_comment.slice.delete_prefix("#|")}"
64
+ location = location.join(continuation_comment.location)
65
+ end
66
+ continuation_comments.clear
67
+ res.signatures << Signature.new(string, location)
68
+ elsif string.start_with?("#|")
69
+ continuation_comments << comment
70
+ end
71
+ end
72
+
73
+ res
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,236 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ module Sorbet
6
+ module Metrics
7
+ class << self
8
+ #: (Array[String]) -> Spoom::Counters
9
+ def collect_code_metrics(files)
10
+ counters = Counters.new
11
+
12
+ files.each do |file|
13
+ counters.increment("files")
14
+
15
+ content = File.read(file)
16
+ node = Spoom.parse_ruby(content, file: file, comments: true)
17
+ visitor = CodeMetricsVisitor.new(counters)
18
+ visitor.visit(node)
19
+ end
20
+
21
+ counters
22
+ end
23
+ end
24
+
25
+ # Collects metrics about how Sorbet is used in the codebase.
26
+ #
27
+ # This approach is different from the metrics file we get directly from Sorbet.
28
+ #
29
+ # This visitor actually visits the codebase and collects metrics about the amount of signatures, `T.` calls,
30
+ # and other metrics. It also knows about RBS comments.
31
+ #
32
+ # On the other hand, the metrics file is a snapshot of the metrics at type checking time and knows about
33
+ # is calls are typed, how many assertions are done, etc.
34
+ class CodeMetricsVisitor < Spoom::Visitor
35
+ include RBS::ExtractRBSComments
36
+
37
+ #: Counters
38
+ attr_reader :counters
39
+
40
+ #: (Spoom::Counters) -> void
41
+ def initialize(counters)
42
+ super()
43
+
44
+ @counters = counters
45
+
46
+ @last_sigs = [] #: Array[Prism::CallNode]
47
+ @type_params = [] #: Array[Prism::CallNode]
48
+ end
49
+
50
+ # @override
51
+ #: (Prism::Node?) -> void
52
+ def visit(node)
53
+ return if node.nil?
54
+
55
+ node.location.trailing_comments.each do |comment|
56
+ text = comment.slice.strip
57
+ next unless text.start_with?("#:")
58
+
59
+ @counters.increment("rbs_assertions")
60
+
61
+ case text
62
+ when /^#: as !nil/
63
+ @counters.increment("rbs_must")
64
+ when /^#: as untyped/
65
+ @counters.increment("rbs_unsafe")
66
+ when /^#: as/
67
+ @counters.increment("rbs_cast")
68
+ when /^#:/
69
+ @counters.increment("rbs_let")
70
+ end
71
+ end
72
+
73
+ super
74
+ end
75
+
76
+ # @override
77
+ #: (Prism::ClassNode) -> void
78
+ def visit_class_node(node)
79
+ visit_scope(node) do
80
+ super
81
+ end
82
+ end
83
+
84
+ # @override
85
+ #: (Prism::ModuleNode) -> void
86
+ def visit_module_node(node)
87
+ visit_scope(node) do
88
+ super
89
+ end
90
+ end
91
+
92
+ # @override
93
+ #: (Prism::SingletonClassNode) -> void
94
+ def visit_singleton_class_node(node)
95
+ visit_scope(node) do
96
+ super
97
+ end
98
+ end
99
+
100
+ # @override
101
+ #: (Prism::DefNode) -> void
102
+ def visit_def_node(node)
103
+ unless node.name.to_s.start_with?("test_")
104
+ @counters.increment("methods")
105
+
106
+ rbs_sigs = node_rbs_comments(node).signatures
107
+ srb_sigs = collect_last_srb_sigs
108
+
109
+ if rbs_sigs.any?
110
+ @counters.increment("methods_with_rbs_sig")
111
+ end
112
+
113
+ if srb_sigs.any?
114
+ @counters.increment("methods_with_srb_sig")
115
+ end
116
+
117
+ if rbs_sigs.empty? && srb_sigs.empty?
118
+ @counters.increment("methods_without_sig")
119
+ end
120
+ end
121
+
122
+ super
123
+ end
124
+
125
+ # @override
126
+ #: (Prism::CallNode) -> void
127
+ def visit_call_node(node)
128
+ @counters.increment("calls")
129
+
130
+ case node.name
131
+ when :attr_accessor, :attr_reader, :attr_writer
132
+ visit_attr_accessor(node)
133
+ return
134
+ when :sig
135
+ visit_sig(node)
136
+ return
137
+ when :type_member, :type_template
138
+ visit_type_member(node)
139
+ return
140
+ end
141
+
142
+ case node.receiver&.slice
143
+ when /^(::)?T$/
144
+ @counters.increment("T_calls")
145
+ @counters.increment("T.#{node.name}")
146
+ end
147
+
148
+ super
149
+ end
150
+
151
+ private
152
+
153
+ #: (Prism::ClassNode | Prism::ModuleNode | Prism::SingletonClassNode) { -> void } -> void
154
+ def visit_scope(node, &block)
155
+ key = node_key(node)
156
+ @counters.increment(key)
157
+ @counters.increment("#{key}_with_rbs_type_params") if node_rbs_comments(node).signatures.any?
158
+
159
+ old_type_params = @type_params
160
+ @type_params = []
161
+
162
+ yield
163
+
164
+ @counters.increment("#{key}_with_srb_type_params") if @type_params.any?
165
+
166
+ @type_params = old_type_params
167
+ end
168
+
169
+ #: (Prism::CallNode) -> void
170
+ def visit_attr_accessor(node)
171
+ @counters.increment("accessors")
172
+
173
+ rbs_sigs = node_rbs_comments(node).signatures
174
+ srb_sigs = collect_last_srb_sigs
175
+
176
+ if rbs_sigs.any?
177
+ @counters.increment("accessors_with_rbs_sig")
178
+ end
179
+
180
+ if srb_sigs.any?
181
+ @counters.increment("accessors_with_srb_sig")
182
+ end
183
+
184
+ if rbs_sigs.empty? && srb_sigs.empty?
185
+ @counters.increment("accessors_without_sig")
186
+ end
187
+ end
188
+
189
+ #: (Prism::CallNode) -> void
190
+ def visit_sig(node)
191
+ @last_sigs << node
192
+ @counters.increment("srb_sigs")
193
+
194
+ if node.slice =~ /abstract/
195
+ @counters.increment("srb_sigs_abstract")
196
+ end
197
+ end
198
+
199
+ #: (Prism::CallNode) -> void
200
+ def visit_type_member(node)
201
+ key = case node.name
202
+ when :type_member
203
+ "type_members"
204
+ when :type_template
205
+ "type_templates"
206
+ else
207
+ return
208
+ end
209
+
210
+ @counters.increment(key)
211
+
212
+ @type_params << node
213
+ end
214
+
215
+ #: -> Array[Prism::CallNode]
216
+ def collect_last_srb_sigs
217
+ sigs = @last_sigs.dup
218
+ @last_sigs.clear
219
+ sigs
220
+ end
221
+
222
+ #: (Prism::ClassNode | Prism::ModuleNode | Prism::SingletonClassNode) -> String
223
+ def node_key(node)
224
+ case node
225
+ when Prism::ClassNode
226
+ "classes"
227
+ when Prism::ModuleNode
228
+ "modules"
229
+ when Prism::SingletonClassNode
230
+ "singleton_classes"
231
+ end
232
+ end
233
+ end
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,34 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "spoom/sorbet/sigils"
5
+
6
+ module Spoom
7
+ module Sorbet
8
+ module Metrics
9
+ module MetricsFileParser
10
+ DEFAULT_PREFIX = "ruby_typer.unknown."
11
+
12
+ class << self
13
+ #: (String path, ?String prefix) -> Hash[String, Integer]
14
+ def parse_file(path, prefix = DEFAULT_PREFIX)
15
+ parse_string(File.read(path), prefix)
16
+ end
17
+
18
+ #: (String string, ?String prefix) -> Hash[String, Integer]
19
+ def parse_string(string, prefix = DEFAULT_PREFIX)
20
+ parse_hash(JSON.parse(string), prefix)
21
+ end
22
+
23
+ #: (Hash[String, untyped] obj, ?String prefix) -> Counters
24
+ def parse_hash(obj, prefix = DEFAULT_PREFIX)
25
+ obj["metrics"].each_with_object(Counters.new) do |metric, metrics|
26
+ name = metric["name"].sub(prefix, "")
27
+ metrics[name] = metric["value"] || 0
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end