assistant 0.0.2 → 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/.editorconfig +13 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +39 -0
- data/.github/dependabot.yml +4 -0
- data/.github/workflows/ci.yml +140 -0
- data/.github/workflows/docs.yml +64 -0
- data/.github/workflows/release.yml +46 -0
- data/.gitignore +5 -1
- data/.markdownlint.json +6 -0
- 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 +40 -148
- data/.ruby-version +1 -1
- data/.yardopts +17 -0
- data/CHANGELOG.md +434 -0
- data/CONTRIBUTING.md +131 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +264 -94
- data/README.md +125 -16
- data/Rakefile +53 -3
- data/SECURITY.md +50 -0
- data/Steepfile +49 -0
- data/_config.yml +87 -0
- data/assistant.gemspec +33 -20
- 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 +28 -0
- data/lib/assistant/input_builder.rbs +15 -0
- data/lib/assistant/log_item.rb +75 -17
- data/lib/assistant/log_item.rbs +40 -0
- data/lib/assistant/log_list.rb +44 -12
- 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 +14 -0
- data/lib/assistant/refinements/string_blankness.rbs +6 -0
- data/lib/assistant/service.rb +328 -8
- data/lib/assistant/service.rbs +86 -0
- data/lib/assistant/version.rb +5 -1
- data/lib/assistant/version.rbs +5 -0
- data/lib/assistant.rb +53 -4
- data/lib/assistant.rbs +25 -0
- data/mise.toml +6 -0
- data/sig/examples/greeter.rbs +14 -0
- metadata +128 -112
- data/.circleci/config.yml +0 -45
- data/.fasterer.yml +0 -19
- data/.rspec +0 -3
- data/.rubocop_todo.yml +0 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Type signatures for `lib/assistant/input_builder/registry.rb`.
|
|
2
|
+
|
|
3
|
+
module Assistant::InputBuilder::Registry
|
|
4
|
+
@input_definitions: Hash[Symbol, Hash[Symbol, untyped]]
|
|
5
|
+
|
|
6
|
+
def input_definitions: () -> Hash[Symbol, Hash[Symbol, untyped]]
|
|
7
|
+
|
|
8
|
+
def register_input_definition: (
|
|
9
|
+
name: Symbol,
|
|
10
|
+
type: untyped,
|
|
11
|
+
options: Hash[Symbol, untyped]
|
|
12
|
+
) -> Hash[Symbol, untyped]
|
|
13
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Generators for the per-input requirement validators. Canonical names
|
|
4
|
+
# (M9):
|
|
5
|
+
#
|
|
6
|
+
# #valid_required_<name>? # for `required: true`
|
|
7
|
+
# #valid_required_conditional_<name>? # for `required: true, if: ...`
|
|
8
|
+
#
|
|
9
|
+
# Pre-M9 names (`valid_require_*?`, `valid_require_conditional_*?`) are
|
|
10
|
+
# kept as deprecated aliases that delegate to the canonical method and
|
|
11
|
+
# emit a one-shot `Kernel.warn` per call site. They are scheduled for
|
|
12
|
+
# removal in `assistant 2.0`.
|
|
13
|
+
module Assistant::InputBuilder::RequireValidator
|
|
14
|
+
# Guard so each deprecated alias warns at most once per textual call
|
|
15
|
+
# site (file + lineno), regardless of how many times it is invoked or
|
|
16
|
+
# how many input names are involved. Internal; tests reset it via
|
|
17
|
+
# `.send(:__reset_deprecation_warnings__)`.
|
|
18
|
+
DEPRECATION_WARNED = Set.new
|
|
19
|
+
private_constant :DEPRECATION_WARNED
|
|
20
|
+
|
|
21
|
+
# Emit the M9 one-shot deprecation warning for a `valid_require_*?`
|
|
22
|
+
# call. Deduped per `[canonical, caller path, caller lineno]` so the
|
|
23
|
+
# same call site never warns twice in one process.
|
|
24
|
+
#
|
|
25
|
+
# @param deprecated_name [Symbol] e.g. `:valid_require_name?`
|
|
26
|
+
# @param canonical_name [Symbol] e.g. `:valid_required_name?`
|
|
27
|
+
# @param caller_location [Thread::Backtrace::Location]
|
|
28
|
+
# @return [void]
|
|
29
|
+
def self.warn_deprecated(deprecated_name, canonical_name, caller_location)
|
|
30
|
+
key = [canonical_name, caller_location.path, caller_location.lineno]
|
|
31
|
+
return if DEPRECATION_WARNED.include?(key)
|
|
32
|
+
|
|
33
|
+
DEPRECATION_WARNED << key
|
|
34
|
+
Kernel.warn("assistant: `##{deprecated_name}` is deprecated; use `##{canonical_name}` (removed in assistant 2.0)")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Test-only hook: clears the per-call-site dedupe set so a single
|
|
38
|
+
# test process can exercise multiple "first warn" scenarios.
|
|
39
|
+
#
|
|
40
|
+
# @return [void]
|
|
41
|
+
def self.__reset_deprecation_warnings__
|
|
42
|
+
DEPRECATION_WARNED.clear
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Define `#valid_required_<name>?` on the host class plus the
|
|
46
|
+
# deprecated `#valid_require_<name>?` alias.
|
|
47
|
+
#
|
|
48
|
+
# @param name [Symbol] input name
|
|
49
|
+
# @return [void]
|
|
50
|
+
def input_require_validator_meth(name:, **)
|
|
51
|
+
canonical = :"valid_required_#{name}?"
|
|
52
|
+
define_required_validator(canonical:, name:, **)
|
|
53
|
+
define_deprecated_alias(:"valid_require_#{name}?", canonical)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Define `#valid_required_conditional_<name>?` on the host class
|
|
57
|
+
# plus the deprecated `#valid_require_conditional_<name>?` alias.
|
|
58
|
+
#
|
|
59
|
+
# @param name [Symbol] input name
|
|
60
|
+
# @return [void]
|
|
61
|
+
def input_require_conditional_meth(name:, **)
|
|
62
|
+
canonical = :"valid_required_conditional_#{name}?"
|
|
63
|
+
define_required_conditional_validator(canonical:, name:, **)
|
|
64
|
+
define_deprecated_alias(:"valid_require_conditional_#{name}?", canonical)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def define_required_validator(canonical:, name:, **options)
|
|
70
|
+
allow_nil = options.fetch(:allow_nil, false) == true
|
|
71
|
+
|
|
72
|
+
define_method(canonical) do |log = true|
|
|
73
|
+
# M2: explicit nil counts as "supplied" when allow_nil: true is set.
|
|
74
|
+
return true if allow_nil && @inputs.key?(name)
|
|
75
|
+
return true if options[:required] == true && send("#{name}?") == true
|
|
76
|
+
|
|
77
|
+
log && send(
|
|
78
|
+
:log_item_error_initialize, attr_name: name, message: "Service is missing argument with name #{name}"
|
|
79
|
+
)
|
|
80
|
+
false
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def define_required_conditional_validator(canonical:, name:, **options)
|
|
85
|
+
define_method(canonical) do
|
|
86
|
+
return false if send(:"valid_required_#{name}?", false) == false
|
|
87
|
+
return true if options[:if].call(send(name))
|
|
88
|
+
|
|
89
|
+
send(
|
|
90
|
+
:log_item_error_initialize,
|
|
91
|
+
attr_name: name,
|
|
92
|
+
message: "Service argument conditional requirement not met properly for #{name}"
|
|
93
|
+
)
|
|
94
|
+
false
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def define_deprecated_alias(deprecated, canonical)
|
|
99
|
+
define_method(deprecated) do |*args, **kwargs|
|
|
100
|
+
::Assistant::InputBuilder::RequireValidator.warn_deprecated(deprecated, canonical, caller_locations(1, 1).first)
|
|
101
|
+
send(canonical, *args, **kwargs)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Type signatures for `lib/assistant/input_builder/require_validator.rb`
|
|
2
|
+
# (M9). Generates the canonical `valid_required_*?` and
|
|
3
|
+
# `valid_required_conditional_*?` instance methods plus deprecated
|
|
4
|
+
# `valid_require_*?` aliases.
|
|
5
|
+
|
|
6
|
+
module Assistant::InputBuilder::RequireValidator
|
|
7
|
+
DEPRECATION_WARNED: Set[Array[untyped]]
|
|
8
|
+
|
|
9
|
+
def self.warn_deprecated: (Symbol deprecated_name, Symbol canonical_name, Thread::Backtrace::Location caller_location) -> void
|
|
10
|
+
|
|
11
|
+
def self.__reset_deprecation_warnings__: () -> void
|
|
12
|
+
|
|
13
|
+
def input_require_validator_meth: (name: Symbol, **untyped) -> Symbol
|
|
14
|
+
|
|
15
|
+
def input_require_conditional_meth: (name: Symbol, **untyped) -> Symbol
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def define_required_validator: (canonical: Symbol, name: Symbol, **untyped) -> Symbol
|
|
20
|
+
|
|
21
|
+
def define_required_conditional_validator: (canonical: Symbol, name: Symbol, **untyped) -> Symbol
|
|
22
|
+
|
|
23
|
+
def define_deprecated_alias: (Symbol deprecated, Symbol canonical) -> Symbol
|
|
24
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Generators for the per-input `valid_type_<name>?` validator (M3:
|
|
4
|
+
# multi-type accepted; M2: `allow_nil:` short-circuits the check).
|
|
5
|
+
module Assistant::InputBuilder::TypeValidator
|
|
6
|
+
# Define `#valid_type_<name>?` on the host class.
|
|
7
|
+
#
|
|
8
|
+
# @param name [Symbol] input name
|
|
9
|
+
# @param type [Class, Array<Class>] declared type(s); `Array(type)` is unioned
|
|
10
|
+
# @param options [Hash] remaining `#input` keyword options (reads `:allow_nil`)
|
|
11
|
+
# @return [void]
|
|
12
|
+
def input_type_validator_meth(name:, type:, **options)
|
|
13
|
+
allow_nil = options.fetch(:allow_nil, false) == true
|
|
14
|
+
types = Array(type)
|
|
15
|
+
message_builder = type_mismatch_message_builder(name:, types:)
|
|
16
|
+
body = type_validator_body(name:, types:, allow_nil:, message_builder:)
|
|
17
|
+
|
|
18
|
+
define_method("valid_type_#{name}?", &body)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Builds the Proc body for the per-input valid_type_<name>? method.
|
|
22
|
+
# Extracted to keep input_type_validator_meth under metric limits.
|
|
23
|
+
def type_validator_body(name:, types:, allow_nil:, message_builder:)
|
|
24
|
+
lambda do
|
|
25
|
+
value = @inputs[name]
|
|
26
|
+
# M2: when allow_nil: true is set, any supplied key short-circuits
|
|
27
|
+
# the type check (mirrors the require validator's behaviour).
|
|
28
|
+
next true if allow_nil && @inputs.key?(name)
|
|
29
|
+
next true if types.any? { |klass| value.is_a?(klass) }
|
|
30
|
+
|
|
31
|
+
send("#{name}?") &&
|
|
32
|
+
send(:log_item_error_initialize, attr_name: name, message: message_builder.call(send(name).class))
|
|
33
|
+
false
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Returns a Proc that, given the actual class of a failing input,
|
|
38
|
+
# produces the error message. Single-type keeps the original 0.1.0
|
|
39
|
+
# wording for back-compat; multi-type uses "is not one of […]". (M3)
|
|
40
|
+
def type_mismatch_message_builder(name:, types:)
|
|
41
|
+
if types.size == 1
|
|
42
|
+
->(actual) { "Service argument with name #{name} is not a #{types.first} but #{actual}" }
|
|
43
|
+
else
|
|
44
|
+
->(actual) { "Service argument with name #{name} is not one of [#{types.join(', ')}] but #{actual}" }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Type signatures for `lib/assistant/input_builder/type_validator.rb`
|
|
2
|
+
# (M3 multi-type; M2 `allow_nil:`).
|
|
3
|
+
|
|
4
|
+
module Assistant::InputBuilder::TypeValidator
|
|
5
|
+
def input_type_validator_meth: (name: Symbol, type: untyped, **untyped) -> Symbol
|
|
6
|
+
|
|
7
|
+
def type_validator_body: (
|
|
8
|
+
name: Symbol,
|
|
9
|
+
types: Array[untyped],
|
|
10
|
+
allow_nil: bool,
|
|
11
|
+
message_builder: ^(Class) -> String
|
|
12
|
+
) -> ^() -> bool
|
|
13
|
+
|
|
14
|
+
def type_mismatch_message_builder: (
|
|
15
|
+
name: Symbol,
|
|
16
|
+
types: Array[untyped]
|
|
17
|
+
) -> ^(Class) -> String
|
|
18
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Umbrella for the input DSL. Each submodule owns one cohesive
|
|
4
|
+
# responsibility; this file wires them into a single `Assistant::InputBuilder`
|
|
5
|
+
# module that `Service` extends. (M13, v1 plan)
|
|
6
|
+
require 'assistant/input_builder/accessors'
|
|
7
|
+
require 'assistant/input_builder/default_option'
|
|
8
|
+
require 'assistant/input_builder/dsl'
|
|
9
|
+
require 'assistant/input_builder/optional_option'
|
|
10
|
+
require 'assistant/input_builder/registry'
|
|
11
|
+
require 'assistant/input_builder/require_validator'
|
|
12
|
+
require 'assistant/input_builder/type_validator'
|
|
13
|
+
|
|
14
|
+
# Declarative input DSL for `Assistant::Service` subclasses. `#input`
|
|
15
|
+
# registers a definition and generates the per-input reader, `?`-checker,
|
|
16
|
+
# type validator, and (when `required:` is set) requirement validator(s).
|
|
17
|
+
# Behaviour is unchanged from pre-M13; the umbrella only re-exports the
|
|
18
|
+
# submodule methods. See the per-submodule files for the specific concern
|
|
19
|
+
# each owns.
|
|
20
|
+
module Assistant::InputBuilder
|
|
21
|
+
include Registry
|
|
22
|
+
include DefaultOption
|
|
23
|
+
include OptionalOption
|
|
24
|
+
include Accessors
|
|
25
|
+
include RequireValidator
|
|
26
|
+
include TypeValidator
|
|
27
|
+
include Dsl
|
|
28
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Type signatures for `lib/assistant/input_builder.rb` — the umbrella
|
|
2
|
+
# module that includes every per-concern submodule. The submodule
|
|
3
|
+
# signatures live alongside the corresponding `.rb` files under
|
|
4
|
+
# `lib/assistant/input_builder/`. See docs/v1/01-api-surface.md and
|
|
5
|
+
# docs/v1/02-features.md (M13).
|
|
6
|
+
|
|
7
|
+
module Assistant::InputBuilder
|
|
8
|
+
include Registry
|
|
9
|
+
include DefaultOption
|
|
10
|
+
include OptionalOption
|
|
11
|
+
include Accessors
|
|
12
|
+
include RequireValidator
|
|
13
|
+
include TypeValidator
|
|
14
|
+
include Dsl
|
|
15
|
+
end
|
data/lib/assistant/log_item.rb
CHANGED
|
@@ -1,26 +1,69 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Assistant
|
|
4
|
-
#
|
|
4
|
+
# A single structured log entry produced by `Service` (directly via
|
|
5
|
+
# `LogList#add_log` or through one of the `log_item_*` shorthands).
|
|
6
|
+
# Construction is **strict** since 1.0 (M10): invalid attributes raise
|
|
7
|
+
# `ArgumentError` rather than producing an instance whose `#valid?`
|
|
8
|
+
# returns `false`. See `docs/v1/06-migration-0x-to-1.md` for the
|
|
9
|
+
# rationale.
|
|
10
|
+
#
|
|
11
|
+
# @example Build an error log entry
|
|
12
|
+
# Assistant::LogItem.new(
|
|
13
|
+
# level: :error,
|
|
14
|
+
# source: :create_user,
|
|
15
|
+
# detail: :email,
|
|
16
|
+
# message: 'must not be blank'
|
|
17
|
+
# )
|
|
5
18
|
class LogItem
|
|
19
|
+
# The exhaustive set of accepted `level:` values, in display order.
|
|
20
|
+
# @return [Array<Symbol>]
|
|
6
21
|
VALID_LEVELS = %i[info warning error].freeze
|
|
7
22
|
|
|
23
|
+
# Validation rules applied in `#initialize`. Each entry pairs a
|
|
24
|
+
# human message with the predicate that must hold. Internal.
|
|
25
|
+
# @api private
|
|
26
|
+
ERRORS = [
|
|
27
|
+
["level must be one of [#{VALID_LEVELS.join(', ')}]", :valid_level?],
|
|
28
|
+
['source must be present and different from detail', :valid_source?],
|
|
29
|
+
['detail must be present and different from source', :valid_detail?],
|
|
30
|
+
['message must be present', :valid_message?]
|
|
31
|
+
].freeze
|
|
32
|
+
|
|
33
|
+
# @return [Symbol] severity (`:info`, `:warning`, or `:error`)
|
|
34
|
+
# @!attribute [r] level
|
|
35
|
+
# @return [Symbol] high-level subsystem the entry came from (e.g. `:initialize`, `:hook`)
|
|
36
|
+
# @!attribute [r] source
|
|
37
|
+
# @return [Symbol] finer-grained detail under `source` (often an attribute name)
|
|
38
|
+
# @!attribute [r] detail
|
|
39
|
+
# @return [String] human-readable message
|
|
40
|
+
# @!attribute [r] message
|
|
41
|
+
# @return [Array<String>, nil] optional backtrace captured at construction
|
|
42
|
+
# @!attribute [r] trace
|
|
8
43
|
attr_reader :level, :source, :detail, :message, :trace
|
|
9
44
|
|
|
45
|
+
# @param level [Symbol, #to_sym] one of {VALID_LEVELS}
|
|
46
|
+
# @param source [Symbol, #to_sym] subsystem identifier; must differ from `detail`
|
|
47
|
+
# @param detail [Symbol, #to_sym] sub-identifier; must differ from `source`
|
|
48
|
+
# @param message [#to_s] human-readable message; must not be blank
|
|
49
|
+
# @param trace [Array<String>, nil] optional backtrace
|
|
50
|
+
# @raise [ArgumentError] when any of the constructor checks in {ERRORS} fail
|
|
10
51
|
def initialize(level:, source:, detail:, message:, trace: nil)
|
|
11
|
-
@level = level
|
|
12
|
-
@source = source
|
|
13
|
-
@detail = detail
|
|
52
|
+
@level = normalize_symbol(level)
|
|
53
|
+
@source = normalize_symbol(source)
|
|
54
|
+
@detail = normalize_symbol(detail)
|
|
14
55
|
@message = message.to_s
|
|
15
56
|
@trace = trace
|
|
57
|
+
validate!
|
|
16
58
|
end
|
|
17
59
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
60
|
+
# @return [Boolean] always `true` for instances constructed via {#initialize} (which raises on invalid input).
|
|
61
|
+
# Retained for introspection and downstream tooling.
|
|
62
|
+
def valid? = [valid_level?, valid_source?, valid_detail?, valid_message?].all?
|
|
21
63
|
|
|
64
|
+
# @return [Hash{Symbol => Object}] hash view with keys `:level`, `:source`, `:detail`, `:message`, `:trace`
|
|
22
65
|
def item
|
|
23
|
-
{ level
|
|
66
|
+
{ level:, source:, detail:, message:, trace: }
|
|
24
67
|
end
|
|
25
68
|
|
|
26
69
|
VALID_LEVELS.each do |valid_level|
|
|
@@ -30,20 +73,35 @@ module Assistant
|
|
|
30
73
|
end
|
|
31
74
|
end
|
|
32
75
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
76
|
+
# @return [Boolean] true when `level` is one of {VALID_LEVELS}
|
|
77
|
+
def valid_level? = VALID_LEVELS.include?(level)
|
|
78
|
+
|
|
79
|
+
# @return [Boolean] true when `source` is non-empty and not equal to `detail`
|
|
80
|
+
def valid_source? = present_log_attribute?(source) && detail != source
|
|
81
|
+
|
|
82
|
+
# @return [Boolean] true when `detail` is non-empty and not equal to `source`
|
|
83
|
+
def valid_detail? = present_log_attribute?(detail) && source != detail
|
|
36
84
|
|
|
37
|
-
|
|
38
|
-
|
|
85
|
+
# @return [Boolean] true when `message` contains at least one non-whitespace character
|
|
86
|
+
def valid_message? = !message.strip.empty?
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def validate!
|
|
91
|
+
errors = validation_errors
|
|
92
|
+
return if errors.empty?
|
|
93
|
+
|
|
94
|
+
raise ArgumentError, "invalid LogItem: #{errors.join('; ')}"
|
|
39
95
|
end
|
|
40
96
|
|
|
41
|
-
def
|
|
42
|
-
|
|
97
|
+
def validation_errors = ERRORS.reject { |_, validation_method| send(validation_method) }.map(&:first)
|
|
98
|
+
|
|
99
|
+
def normalize_symbol(value)
|
|
100
|
+
value.respond_to?(:to_sym) ? value.to_sym : value
|
|
43
101
|
end
|
|
44
102
|
|
|
45
|
-
def
|
|
46
|
-
|
|
103
|
+
def present_log_attribute?(value)
|
|
104
|
+
value.respond_to?(:size) && value.size.positive?
|
|
47
105
|
end
|
|
48
106
|
end
|
|
49
107
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Type signatures for `lib/assistant/log_item.rb`. See
|
|
2
|
+
# docs/v1/01-api-surface.md for the frozen surface.
|
|
3
|
+
|
|
4
|
+
class Assistant::LogItem
|
|
5
|
+
VALID_LEVELS: Array[Symbol]
|
|
6
|
+
ERRORS: Array[[String, Symbol]]
|
|
7
|
+
|
|
8
|
+
attr_reader level: Symbol
|
|
9
|
+
attr_reader source: Symbol
|
|
10
|
+
attr_reader detail: Symbol
|
|
11
|
+
attr_reader message: String
|
|
12
|
+
attr_reader trace: untyped
|
|
13
|
+
|
|
14
|
+
def initialize: (
|
|
15
|
+
detail: Symbol | String,
|
|
16
|
+
message: String | _ToS,
|
|
17
|
+
level: Symbol | String,
|
|
18
|
+
source: Symbol | String,
|
|
19
|
+
?trace: untyped
|
|
20
|
+
) -> void
|
|
21
|
+
|
|
22
|
+
def valid?: () -> bool
|
|
23
|
+
def valid_level?: () -> bool
|
|
24
|
+
def valid_source?: () -> bool
|
|
25
|
+
def valid_detail?: () -> bool
|
|
26
|
+
def valid_message?: () -> bool
|
|
27
|
+
|
|
28
|
+
def info?: () -> bool
|
|
29
|
+
def warning?: () -> bool
|
|
30
|
+
def error?: () -> bool
|
|
31
|
+
|
|
32
|
+
def item: () -> Hash[Symbol, untyped]
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def validate!: () -> void
|
|
37
|
+
def validation_errors: () -> Array[String]
|
|
38
|
+
def normalize_symbol: (untyped value) -> untyped
|
|
39
|
+
def present_log_attribute?: (untyped value) -> bool
|
|
40
|
+
end
|
data/lib/assistant/log_list.rb
CHANGED
|
@@ -1,20 +1,52 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
# Mixin that gives `Assistant::Service` its log timeline. Owns the
|
|
4
|
+
# `@logs` array and exposes the public helpers (`#add_log`, `#merge_logs`,
|
|
5
|
+
# the `log_item_*` shorthands, `#infos` / `#warnings` / `#errors`) used
|
|
6
|
+
# by service code to record observations and by callers to read the
|
|
7
|
+
# result.
|
|
8
|
+
module Assistant::LogList
|
|
9
|
+
# Append a single {Assistant::LogItem} to the timeline.
|
|
10
|
+
#
|
|
11
|
+
# @param level [Symbol] `:info`, `:warning`, or `:error`
|
|
12
|
+
# @param source [Symbol] subsystem identifier
|
|
13
|
+
# @param detail [Symbol] sub-identifier (often an attribute name)
|
|
14
|
+
# @param message [String] human-readable message
|
|
15
|
+
# @param trace [Array<String>, nil] optional backtrace
|
|
16
|
+
# @return [Array<Assistant::LogItem>] the updated timeline
|
|
17
|
+
# @raise [ArgumentError] if {Assistant::LogItem#initialize} rejects the inputs
|
|
18
|
+
def add_log(level:, source:, detail:, message:, trace: nil)
|
|
19
|
+
@logs << Assistant::LogItem.new(level:, source:, detail:, message:, trace:)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Concatenate an existing log timeline onto this service's `@logs`,
|
|
23
|
+
# preserving insertion order. Used by `Service#call_service` (M-S2).
|
|
24
|
+
#
|
|
25
|
+
# @param logs [Array<Assistant::LogItem>] entries to append
|
|
26
|
+
# @return [Array<Assistant::LogItem>] the updated timeline
|
|
27
|
+
def merge_logs(logs:)
|
|
28
|
+
@logs.concat(logs)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Convenience used by InputBuilder-generated validators to record an
|
|
32
|
+
# initialization-time error for a specific input attribute.
|
|
33
|
+
#
|
|
34
|
+
# @param attr_name [Symbol] the offending input name (recorded as `detail`)
|
|
35
|
+
# @param message [String] human-readable explanation
|
|
36
|
+
# @return [Array<Assistant::LogItem>] the updated timeline
|
|
37
|
+
def log_item_error_initialize(attr_name:, message:)
|
|
38
|
+
@logs << Assistant::LogItem.new(detail: attr_name, level: :error, message:, source: :initialize)
|
|
39
|
+
end
|
|
9
40
|
|
|
10
|
-
|
|
11
|
-
|
|
41
|
+
::Assistant::LogItem::VALID_LEVELS.each do |level|
|
|
42
|
+
define_method("#{level}s") do
|
|
43
|
+
@logs.select { |log| log.send("#{level}?") }
|
|
12
44
|
end
|
|
13
45
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
46
|
+
# Shorthand: log_item_info / log_item_warning / log_item_error.
|
|
47
|
+
# See docs/v1/02-features.md M5.
|
|
48
|
+
define_method("log_item_#{level}") do |source:, detail:, message:, trace: nil|
|
|
49
|
+
add_log(level:, source:, detail:, message:, trace:)
|
|
18
50
|
end
|
|
19
51
|
end
|
|
20
52
|
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Type signatures for `lib/assistant/log_list.rb`. See
|
|
2
|
+
# docs/v1/01-api-surface.md for the frozen surface.
|
|
3
|
+
#
|
|
4
|
+
# `Assistant::LogList` is mixed into `Assistant::Service`; the `@logs`
|
|
5
|
+
# instance variable it operates on is declared on `Service` itself.
|
|
6
|
+
|
|
7
|
+
module Assistant::LogList
|
|
8
|
+
def add_log: (
|
|
9
|
+
detail: Symbol | String,
|
|
10
|
+
level: Symbol | String,
|
|
11
|
+
message: String | _ToS,
|
|
12
|
+
source: Symbol | String,
|
|
13
|
+
?trace: untyped
|
|
14
|
+
) -> Array[Assistant::LogItem]
|
|
15
|
+
|
|
16
|
+
def merge_logs: (logs: Array[Assistant::LogItem]) -> Array[Assistant::LogItem]
|
|
17
|
+
|
|
18
|
+
def log_item_error_initialize: (
|
|
19
|
+
attr_name: Symbol | String,
|
|
20
|
+
message: String | _ToS
|
|
21
|
+
) -> Array[Assistant::LogItem]
|
|
22
|
+
|
|
23
|
+
# M5 shorthands: `log_item_info`, `log_item_warning`, `log_item_error`.
|
|
24
|
+
def log_item_info: (
|
|
25
|
+
detail: Symbol | String,
|
|
26
|
+
message: String | _ToS,
|
|
27
|
+
source: Symbol | String,
|
|
28
|
+
?trace: untyped
|
|
29
|
+
) -> Array[Assistant::LogItem]
|
|
30
|
+
|
|
31
|
+
def log_item_warning: (
|
|
32
|
+
detail: Symbol | String,
|
|
33
|
+
message: String | _ToS,
|
|
34
|
+
source: Symbol | String,
|
|
35
|
+
?trace: untyped
|
|
36
|
+
) -> Array[Assistant::LogItem]
|
|
37
|
+
|
|
38
|
+
def log_item_error: (
|
|
39
|
+
detail: Symbol | String,
|
|
40
|
+
message: String | _ToS,
|
|
41
|
+
source: Symbol | String,
|
|
42
|
+
?trace: untyped
|
|
43
|
+
) -> Array[Assistant::LogItem]
|
|
44
|
+
|
|
45
|
+
def infos: () -> Array[Assistant::LogItem]
|
|
46
|
+
def warnings: () -> Array[Assistant::LogItem]
|
|
47
|
+
def errors: () -> Array[Assistant::LogItem]
|
|
48
|
+
end
|
|
@@ -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
|