assistant 0.1.0 → 1.0.0.rc1
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 +4 -4
- data/.github/PULL_REQUEST_TEMPLATE.md +39 -0
- data/.github/workflows/ci.yml +99 -0
- data/.github/workflows/docs.yml +64 -0
- data/.github/workflows/release.yml +1 -1
- data/.gitignore +5 -1
- data/.opencode/.gitignore +4 -0
- data/.opencode/opencode.json +13 -0
- data/.opencode/skills/create-pr/SKILL.md +138 -0
- data/.opencode/skills/ruby-services/SKILL.md +81 -0
- data/.rubocop.yml +14 -4
- data/.yardopts +17 -0
- data/CHANGELOG.md +378 -0
- data/CONTRIBUTING.md +131 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +196 -29
- data/README.md +125 -16
- data/Rakefile +45 -0
- data/SECURITY.md +50 -0
- data/Steepfile +49 -0
- data/_config.yml +87 -0
- data/assistant.gemspec +24 -7
- data/docs/api-reference.md +264 -0
- data/docs/changelog.md +26 -0
- data/docs/deprecations.md +86 -0
- data/docs/examples/cli-handler.md +17 -0
- data/docs/examples/composing-services.md +17 -0
- data/docs/examples/execute-callbacks.md +17 -0
- data/docs/examples/index.md +29 -0
- data/docs/examples/instrumentation-notifier.md +17 -0
- data/docs/examples/rails-service.md +17 -0
- data/docs/examples/rbs-generator.md +17 -0
- data/docs/examples/sidekiq-worker.md +17 -0
- data/docs/getting-started.md +136 -0
- data/docs/guides/composing-services.md +222 -0
- data/docs/guides/index.md +25 -0
- data/docs/guides/inputs.md +333 -0
- data/docs/guides/logging-and-results.md +202 -0
- data/docs/guides/rbs-and-types.md +16 -0
- data/docs/guides/validation.md +180 -0
- data/docs/index.md +69 -0
- data/docs/roadmap.md +33 -0
- data/exe/assistant-rbs +7 -0
- data/lib/assistant/execute_callbacks.rb +103 -0
- data/lib/assistant/execute_callbacks.rbs +30 -0
- data/lib/assistant/input_builder/accessors.rb +36 -0
- data/lib/assistant/input_builder/accessors.rbs +10 -0
- data/lib/assistant/input_builder/default_option.rb +41 -0
- data/lib/assistant/input_builder/default_option.rbs +11 -0
- data/lib/assistant/input_builder/dsl.rb +37 -0
- data/lib/assistant/input_builder/dsl.rbs +12 -0
- data/lib/assistant/input_builder/optional_option.rb +45 -0
- data/lib/assistant/input_builder/optional_option.rbs +10 -0
- data/lib/assistant/input_builder/registry.rb +27 -0
- data/lib/assistant/input_builder/registry.rbs +13 -0
- data/lib/assistant/input_builder/require_validator.rb +104 -0
- data/lib/assistant/input_builder/require_validator.rbs +24 -0
- data/lib/assistant/input_builder/type_validator.rb +47 -0
- data/lib/assistant/input_builder/type_validator.rbs +18 -0
- data/lib/assistant/input_builder.rb +25 -81
- data/lib/assistant/input_builder.rbs +15 -0
- data/lib/assistant/log_item.rb +74 -16
- data/lib/assistant/log_item.rbs +40 -0
- data/lib/assistant/log_list.rb +43 -17
- data/lib/assistant/log_list.rbs +48 -0
- data/lib/assistant/rbs_generator/cli.rb +109 -0
- data/lib/assistant/rbs_generator/cli.rbs +24 -0
- data/lib/assistant/rbs_generator/renderer.rb +67 -0
- data/lib/assistant/rbs_generator/renderer.rbs +11 -0
- data/lib/assistant/rbs_generator/writer.rb +65 -0
- data/lib/assistant/rbs_generator/writer.rbs +24 -0
- data/lib/assistant/rbs_generator.rb +38 -0
- data/lib/assistant/rbs_generator.rbs +5 -0
- data/lib/assistant/refinements/string_blankness.rb +9 -13
- data/lib/assistant/refinements/string_blankness.rbs +6 -0
- data/lib/assistant/service.rb +300 -11
- data/lib/assistant/service.rbs +82 -1
- data/lib/assistant/version.rb +5 -1
- data/lib/assistant/version.rbs +5 -0
- data/lib/assistant.rb +54 -4
- data/lib/assistant.rbs +25 -0
- data/mise.toml +2 -0
- data/sig/examples/greeter.rbs +14 -0
- metadata +142 -38
- data/.fasterer.yml +0 -19
- data/.rubocop_todo.yml +0 -7
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'optparse'
|
|
4
|
+
|
|
5
|
+
module Assistant::RbsGenerator
|
|
6
|
+
# Command-line entry point. Parses argv, loads the input files,
|
|
7
|
+
# discovers Service subclasses, renders, writes.
|
|
8
|
+
class Cli
|
|
9
|
+
# Usage banner printed by `--help`.
|
|
10
|
+
# @return [String]
|
|
11
|
+
USAGE = <<~USAGE
|
|
12
|
+
Usage: assistant-rbs [PATH...] [--output DIR] [--quiet]
|
|
13
|
+
|
|
14
|
+
Loads every Ruby file under the given PATHs (default: lib/) and
|
|
15
|
+
writes one .rbs file per Assistant::Service subclass found, under
|
|
16
|
+
DIR (default: sig/).
|
|
17
|
+
|
|
18
|
+
Existing .rbs files without the generator marker comment on their
|
|
19
|
+
first line are left untouched.
|
|
20
|
+
USAGE
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
# Convenience wrapper that builds a `Cli` and runs it.
|
|
24
|
+
#
|
|
25
|
+
# @param argv [Array<String>] command-line arguments
|
|
26
|
+
# @param stdout [IO] output stream for non-error messages
|
|
27
|
+
# @param stderr [IO] output stream for warnings and errors
|
|
28
|
+
# @return [Integer] process exit status (0 on success)
|
|
29
|
+
def run(argv, stdout: $stdout, stderr: $stderr)
|
|
30
|
+
new(argv, stdout:, stderr:).run
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def initialize(argv, stdout: $stdout, stderr: $stderr)
|
|
35
|
+
@argv = argv
|
|
36
|
+
@stdout = stdout
|
|
37
|
+
@stderr = stderr
|
|
38
|
+
@output_dir = Assistant::RbsGenerator::DEFAULT_OUTPUT_DIR
|
|
39
|
+
@quiet = false
|
|
40
|
+
@paths = nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Returns an exit status (0 on success, non-zero on failure).
|
|
44
|
+
def run
|
|
45
|
+
parse_options!
|
|
46
|
+
before = service_subclasses
|
|
47
|
+
load_paths(@paths || Assistant::RbsGenerator::DEFAULT_INPUT_PATHS)
|
|
48
|
+
emit(service_subclasses - before)
|
|
49
|
+
0
|
|
50
|
+
rescue SystemExit => e
|
|
51
|
+
e.status
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def emit(services)
|
|
57
|
+
writer = Assistant::RbsGenerator::Writer.new(
|
|
58
|
+
output_dir: @output_dir, quiet: @quiet, stdout: @stdout, stderr: @stderr
|
|
59
|
+
)
|
|
60
|
+
services.sort_by(&:name).each do |service_class|
|
|
61
|
+
writer.write(service_class, Assistant::RbsGenerator::Renderer.render(service_class))
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def parse_options!
|
|
66
|
+
OptionParser.new { |opts| configure_parser(opts) }.then { |p| @paths = p.parse(@argv) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def configure_parser(opts)
|
|
70
|
+
opts.banner = USAGE
|
|
71
|
+
default = Assistant::RbsGenerator::DEFAULT_OUTPUT_DIR
|
|
72
|
+
opts.on('-o', '--output DIR', "Output directory (default: #{default})") do |dir|
|
|
73
|
+
@output_dir = dir
|
|
74
|
+
end
|
|
75
|
+
opts.on('-q', '--quiet', 'Suppress non-error output') { @quiet = true }
|
|
76
|
+
opts.on('-h', '--help', 'Show this message') do
|
|
77
|
+
@stdout.puts opts
|
|
78
|
+
raise SystemExit, 0
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def load_paths(paths)
|
|
83
|
+
paths.each do |path|
|
|
84
|
+
if File.directory?(path)
|
|
85
|
+
Dir.glob(File.join(path, '**/*.rb')).each { |file| safe_require(file) }
|
|
86
|
+
elsif File.file?(path)
|
|
87
|
+
safe_require(path)
|
|
88
|
+
else
|
|
89
|
+
@stderr.puts "[warn] no such file or directory: #{path}"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def safe_require(path)
|
|
95
|
+
require File.expand_path(path)
|
|
96
|
+
rescue LoadError, StandardError => e
|
|
97
|
+
@stderr.puts "[warn] failed to load #{path}: #{e.class}: #{e.message}"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Snapshot of every loaded Service subclass. Used to diff before /
|
|
101
|
+
# after `load_paths` so we only emit signatures for classes whose
|
|
102
|
+
# source files we were actually asked to scan -- this keeps a
|
|
103
|
+
# long-running process (or a test suite) from re-emitting sigs for
|
|
104
|
+
# every Service subclass it has ever seen.
|
|
105
|
+
def service_subclasses
|
|
106
|
+
ObjectSpace.each_object(Class).select { |klass| klass < Assistant::Service && klass.name }
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
class Assistant::RbsGenerator::Cli
|
|
2
|
+
USAGE: String
|
|
3
|
+
|
|
4
|
+
@argv: Array[String]
|
|
5
|
+
@stdout: IO
|
|
6
|
+
@stderr: IO
|
|
7
|
+
@output_dir: String
|
|
8
|
+
@quiet: bool
|
|
9
|
+
@paths: Array[String]?
|
|
10
|
+
|
|
11
|
+
def self.run: (Array[String] argv, ?stdout: IO, ?stderr: IO) -> Integer
|
|
12
|
+
|
|
13
|
+
def initialize: (Array[String] argv, ?stdout: IO, ?stderr: IO) -> void
|
|
14
|
+
def run: () -> Integer
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def emit: (Array[singleton(Assistant::Service)] services) -> void
|
|
19
|
+
def parse_options!: () -> void
|
|
20
|
+
def configure_parser: (OptionParser opts) -> void
|
|
21
|
+
def load_paths: (Array[String] paths) -> void
|
|
22
|
+
def safe_require: (String path) -> void
|
|
23
|
+
def service_subclasses: () -> Array[singleton(Assistant::Service)]
|
|
24
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Converts a Service subclass to a `.rbs` source string. Pure
|
|
4
|
+
# function; does no I/O.
|
|
5
|
+
module Assistant::RbsGenerator::Renderer
|
|
6
|
+
class << self
|
|
7
|
+
# Render a `.rbs` source string for the given `Service` subclass.
|
|
8
|
+
# Module-prefix segments of the class name are wrapped in nested
|
|
9
|
+
# `module ... end` blocks; the trailing segment becomes the
|
|
10
|
+
# `class X < Assistant::Service` declaration. The body lists one
|
|
11
|
+
# `def name: () -> Type` and `def name?: () -> bool` per declared
|
|
12
|
+
# input.
|
|
13
|
+
#
|
|
14
|
+
# @param service_class [Class<Assistant::Service>]
|
|
15
|
+
# @return [String] the rendered `.rbs` source, ending with a newline
|
|
16
|
+
# @raise [RuntimeError] when `service_class` is anonymous or declares
|
|
17
|
+
# an input with a non-Class / anonymous `type:`
|
|
18
|
+
def render(service_class)
|
|
19
|
+
name = service_class.name or raise 'anonymous Service class cannot be rendered'
|
|
20
|
+
segments = name.split('::')
|
|
21
|
+
# `String#split` on a non-empty string always returns at least
|
|
22
|
+
# one element, but Steep can't prove that -- guard for narrowing.
|
|
23
|
+
class_name = segments.pop or raise "unexpected empty name for #{service_class.inspect}"
|
|
24
|
+
body_lines = render_class_body(class_name, service_class.input_definitions)
|
|
25
|
+
nested_lines = nest_in_modules(segments, body_lines)
|
|
26
|
+
"#{[Assistant::RbsGenerator::MARKER, '', *nested_lines].join("\n")}\n"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def render_class_body(class_name, definitions)
|
|
32
|
+
header = "class #{class_name} < Assistant::Service"
|
|
33
|
+
method_lines = definitions.flat_map { |name, options| input_method_lines(name, options) }
|
|
34
|
+
return [header, 'end'] if method_lines.empty?
|
|
35
|
+
|
|
36
|
+
[header, '', *method_lines.map { |line| " #{line}" }, '', 'end']
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def nest_in_modules(segments, body_lines)
|
|
40
|
+
segments.reverse.reduce(body_lines) do |body, segment|
|
|
41
|
+
indented = body.map { |line| line.empty? ? '' : " #{line}" }
|
|
42
|
+
["module #{segment}", *indented, 'end']
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def input_method_lines(name, options)
|
|
47
|
+
[
|
|
48
|
+
"def #{name}: () -> #{render_type(name, options)}",
|
|
49
|
+
"def #{name}?: () -> bool"
|
|
50
|
+
]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def render_type(name, options)
|
|
54
|
+
raise "input #{name.inspect} has no `type:` declared" unless options.key?(:type)
|
|
55
|
+
|
|
56
|
+
rendered = Array(options[:type]).map { |type| render_single_type(name, type) }
|
|
57
|
+
union = rendered.length == 1 ? rendered.first : "(#{rendered.join(' | ')})"
|
|
58
|
+
options[:allow_nil] == true ? "#{union}?" : union
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def render_single_type(name, type)
|
|
62
|
+
raise "input #{name.inspect} has non-Class type #{type.inspect}" unless type.is_a?(Module)
|
|
63
|
+
|
|
64
|
+
type.name || raise("input #{name.inspect} has anonymous type")
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module Assistant::RbsGenerator::Renderer
|
|
2
|
+
def self.render: (singleton(Assistant::Service) service_class) -> String
|
|
3
|
+
|
|
4
|
+
private
|
|
5
|
+
|
|
6
|
+
def self.render_class_body: (String class_name, Hash[Symbol, untyped] definitions) -> Array[String]
|
|
7
|
+
def self.nest_in_modules: (Array[String] segments, Array[String] body_lines) -> Array[String]
|
|
8
|
+
def self.input_method_lines: (Symbol name, Hash[Symbol, untyped] options) -> Array[String]
|
|
9
|
+
def self.render_type: (Symbol name, Hash[Symbol, untyped] options) -> String
|
|
10
|
+
def self.render_single_type: (Symbol name, untyped type) -> String
|
|
11
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
|
|
5
|
+
module Assistant::RbsGenerator
|
|
6
|
+
# File-writing layer with marker-aware idempotency.
|
|
7
|
+
class Writer
|
|
8
|
+
def initialize(output_dir:, quiet: false, stdout: $stdout, stderr: $stderr)
|
|
9
|
+
@output_dir = output_dir
|
|
10
|
+
@quiet = quiet
|
|
11
|
+
@stdout = stdout
|
|
12
|
+
@stderr = stderr
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Returns one of :written, :unchanged, :skipped.
|
|
16
|
+
def write(service_class, contents)
|
|
17
|
+
target = path_for(service_class)
|
|
18
|
+
return skip!(target) if exists_without_marker?(target)
|
|
19
|
+
return unchanged!(target) if File.exist?(target) && File.read(target) == contents
|
|
20
|
+
|
|
21
|
+
FileUtils.mkdir_p(File.dirname(target))
|
|
22
|
+
File.write(target, contents)
|
|
23
|
+
announce("[written] #{target}")
|
|
24
|
+
:written
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
attr_reader :output_dir, :quiet, :stdout, :stderr
|
|
30
|
+
|
|
31
|
+
def exists_without_marker?(target)
|
|
32
|
+
File.exist?(target) && !generated_file?(target)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def skip!(target)
|
|
36
|
+
stderr.puts "[skipped] #{target} (no generator marker; will not overwrite)"
|
|
37
|
+
:skipped
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def unchanged!(target)
|
|
41
|
+
announce("[unchanged] #{target}")
|
|
42
|
+
:unchanged
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def path_for(service_class)
|
|
46
|
+
name = service_class.name or raise "anonymous Service class #{service_class.inspect}"
|
|
47
|
+
relative = name.split('::').map { |seg| underscore(seg) }.join('/')
|
|
48
|
+
File.join(output_dir, "#{relative}.rbs")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# ASCII-only underscoring -- the gem ships no runtime deps, so this
|
|
52
|
+
# mirrors the common ActiveSupport rule without pulling it in.
|
|
53
|
+
def underscore(camel)
|
|
54
|
+
camel.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def generated_file?(path)
|
|
58
|
+
File.foreach(path).first&.chomp == Assistant::RbsGenerator::MARKER
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def announce(message)
|
|
62
|
+
stdout.puts(message) unless quiet
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
class Assistant::RbsGenerator::Writer
|
|
2
|
+
@output_dir: String
|
|
3
|
+
@quiet: bool
|
|
4
|
+
@stdout: IO
|
|
5
|
+
@stderr: IO
|
|
6
|
+
|
|
7
|
+
def initialize: (output_dir: String, ?quiet: bool, ?stdout: IO, ?stderr: IO) -> void
|
|
8
|
+
def write: (singleton(Assistant::Service) service_class, String contents) -> Symbol
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
attr_reader output_dir: String
|
|
13
|
+
attr_reader quiet: bool
|
|
14
|
+
attr_reader stdout: IO
|
|
15
|
+
attr_reader stderr: IO
|
|
16
|
+
|
|
17
|
+
def path_for: (singleton(Assistant::Service) service_class) -> String
|
|
18
|
+
def exists_without_marker?: (String target) -> bool
|
|
19
|
+
def skip!: (String target) -> Symbol
|
|
20
|
+
def unchanged!: (String target) -> Symbol
|
|
21
|
+
def underscore: (String camel) -> String
|
|
22
|
+
def generated_file?: (String path) -> bool
|
|
23
|
+
def announce: (String message) -> void
|
|
24
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Per-class RBS generator for `Assistant::Service` subclasses. The
|
|
4
|
+
# generic RBS shipped in `lib/assistant/service.rbs` cannot describe the
|
|
5
|
+
# per-input getter / predicate methods produced by the
|
|
6
|
+
# `Assistant::InputBuilder` DSL because their names and return types are
|
|
7
|
+
# only known at class-definition time. Users with Steep run this
|
|
8
|
+
# generator over their service files to obtain accurate signatures for
|
|
9
|
+
# their own subclasses.
|
|
10
|
+
#
|
|
11
|
+
# The generator is intentionally **not** loaded by `require 'assistant'`
|
|
12
|
+
# -- it is a developer-time tool. The shipped `exe/assistant-rbs`
|
|
13
|
+
# binary requires it explicitly.
|
|
14
|
+
#
|
|
15
|
+
# Marked Experimental in `docs/v1/01-api-surface.md` so the output
|
|
16
|
+
# format may evolve within 1.x.
|
|
17
|
+
#
|
|
18
|
+
# See `docs/v1/02-features.md` (M11) for the contract.
|
|
19
|
+
module Assistant::RbsGenerator
|
|
20
|
+
# Header marker written as the first line of every generated `.rbs`
|
|
21
|
+
# file. {Writer} refuses to overwrite a file whose first line is
|
|
22
|
+
# not this exact string, so hand-edited signatures are preserved.
|
|
23
|
+
# @return [String]
|
|
24
|
+
MARKER = '# Generated by assistant-rbs; do not edit.'
|
|
25
|
+
|
|
26
|
+
# Default value for the `--output DIR` option of `exe/assistant-rbs`.
|
|
27
|
+
# @return [String]
|
|
28
|
+
DEFAULT_OUTPUT_DIR = 'sig'
|
|
29
|
+
|
|
30
|
+
# Default value for the positional `PATH` arguments of
|
|
31
|
+
# `exe/assistant-rbs` when none are supplied.
|
|
32
|
+
# @return [Array<String>]
|
|
33
|
+
DEFAULT_INPUT_PATHS = ['lib'].freeze
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
require 'assistant/rbs_generator/cli'
|
|
37
|
+
require 'assistant/rbs_generator/renderer'
|
|
38
|
+
require 'assistant/rbs_generator/writer'
|
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def whitespace?
|
|
13
|
-
strip.empty?
|
|
14
|
-
end
|
|
15
|
-
end
|
|
3
|
+
# Refines String with `#whitespace?`, true when a string is empty or
|
|
4
|
+
# contains only whitespace characters. Used by `InputBuilder` validators
|
|
5
|
+
# to treat whitespace-only strings as missing input without depending on
|
|
6
|
+
# ActiveSupport's `String#blank?`.
|
|
7
|
+
module Assistant::Refinements::StringBlankness
|
|
8
|
+
refine String do
|
|
9
|
+
# True when the string is empty or contains only whitespace.
|
|
10
|
+
def whitespace?
|
|
11
|
+
strip.empty?
|
|
16
12
|
end
|
|
17
13
|
end
|
|
18
14
|
end
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Type signatures for `lib/assistant/refinements/string_blankness.rb`.
|
|
2
|
+
# RBS cannot fully model refinements; the bundled refinement module is
|
|
3
|
+
# declared as an empty module so downstream signatures can name it.
|
|
4
|
+
|
|
5
|
+
module Assistant::Refinements::StringBlankness
|
|
6
|
+
end
|