adornment-core 0.1.0

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.
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Adornment
4
+ module Core
5
+ # Per-owner state, held in a hidden instance variable of the owner (so it lives and dies with
6
+ # the owner; nothing global refers to it strongly).
7
+ #
8
+ # Entries (attached annotations) are one Hash per side, written under the lock and read
9
+ # without it. Readers of a single entry get a frozen Array: CRuby runs `Hash#[]` and
10
+ # `Hash#[]=` with Symbol keys atomically under the GVL, so a reader sees the old or the new
11
+ # value. A whole-side read freezes the Hash in place and returns it as the snapshot; the next
12
+ # write copies it into a new mutable Hash first. So writing N names in a row costs O(N) (not
13
+ # O(N^2) as with a copy per write), and at rest only one Hash per side exists.
14
+ #
15
+ # The lock guards the entry writes; no user code runs under it. Pending declarations are not
16
+ # kept here (see {Pending}).
17
+ # @api private
18
+ class State
19
+ # The owner's instance variable that holds its state.
20
+ IVAR = :@__adornment_state__
21
+ # Guards the creation of states (one per owner).
22
+ CREATE_LOCK = Mutex.new
23
+ # The shared empty entry.
24
+ NONE = [].freeze
25
+
26
+ # @param owner [Module]
27
+ # @return [State, nil] the owner's state; nil when it has none yet
28
+ def self.of(owner) = owner.instance_variable_get(IVAR)
29
+
30
+ # @param owner [Module]
31
+ # @return [State] the owner's state, created on first use. Creating it registers the owner:
32
+ # activation registers too, but a subclass of an activated class inherits the hooks without
33
+ # being activated, and is registered when it first declares.
34
+ def self.for(owner)
35
+ owner.instance_variable_get(IVAR) || CREATE_LOCK.synchronize do
36
+ owner.instance_variable_get(IVAR) || begin
37
+ state = new
38
+ owner.instance_variable_set(IVAR, state)
39
+ Registry.register(owner)
40
+ state
41
+ end
42
+ end
43
+ end
44
+
45
+ # @return [Mutex] guards entry writes and the creation of {#annotation_methods}
46
+ attr_reader :lock
47
+
48
+ # @return [AnnotationMethods, nil] the owner's declaration methods module, once created
49
+ attr_accessor :annotation_methods
50
+
51
+ def initialize
52
+ @lock = Mutex.new
53
+ @instance = {}
54
+ @singleton = {}
55
+ @annotation_methods = nil
56
+ end
57
+
58
+ # @param side [:instance, :singleton]
59
+ # @param name [Symbol]
60
+ # @return [Array<Annotation>, nil] frozen; nil when the method never took declarations
61
+ def entry(side, name)
62
+ (side == :singleton ? @singleton : @instance)[name]
63
+ end
64
+
65
+ # @param side [:instance, :singleton]
66
+ # @return [Hash{Symbol => Array<Annotation>}] frozen snapshot of a side
67
+ def entries(side)
68
+ live = side == :singleton ? @singleton : @instance
69
+ return live if live.frozen?
70
+
71
+ @lock.synchronize { (side == :singleton ? @singleton : @instance).freeze }
72
+ end
73
+
74
+ # Replaces the entry for `name`.
75
+ # @param side [:instance, :singleton]
76
+ # @param name [Symbol]
77
+ # @param value [Array<Annotation>] frozen
78
+ # @return [nil]
79
+ def set_entry(side, name, value)
80
+ @lock.synchronize { writable(side)[name] = value }
81
+ nil
82
+ end
83
+
84
+ # The side's mutable Hash (a frozen snapshot is copied first). Called under the lock.
85
+ # @param side [:instance, :singleton]
86
+ # @return [Hash{Symbol => Array<Annotation>}]
87
+ def writable(side)
88
+ live = side == :singleton ? @singleton : @instance
89
+ return live unless live.frozen?
90
+
91
+ live = live.dup
92
+ side == :singleton ? @singleton = live : @instance = live
93
+ end
94
+ end
95
+
96
+ # Weak registry of activated owners, for global discovery and verification.
97
+ # `ObjectSpace::WeakMap` is used because `ObjectSpace::WeakKeyMap` cannot enumerate its keys.
98
+ # @api private
99
+ module Registry
100
+ @map = ObjectSpace::WeakMap.new
101
+ @lock = Mutex.new
102
+
103
+ class << self
104
+ # @param owner [Module]
105
+ # @return [void]
106
+ def register(owner) = @lock.synchronize { @map[owner] = true }
107
+
108
+ # @param owner [Module]
109
+ # @return [void]
110
+ def unregister(owner) = @lock.synchronize { @map.delete(owner) }
111
+
112
+ # @return [Array<Module>] the reachable registered owners (a copy)
113
+ def owners = @lock.synchronize { @map.keys }
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Adornment
4
+ module Core
5
+ # Checks on annotation types, shared by the facade, the owner API and reflection.
6
+ # @api private
7
+ module Utils
8
+ # @api private
9
+ # @param type [Class] an annotation type
10
+ # @return [Symbol] the declaration method name of a type without an alias: the last segment of
11
+ # its constant name
12
+ # @raise [ArgumentError] for a non-annotation type, or an anonymous one
13
+ def self.declaration_name(type)
14
+ check_type!(type)
15
+ name = type.name or raise ArgumentError, "anonymous annotation type #{type.inspect} needs an alias (Name: type)"
16
+ name.split("::").last.to_sym
17
+ end
18
+
19
+ # Raises unless `type` is a method annotation type: a class that included
20
+ # `Adornment::Annotation[...]` (the policy module is the only way in).
21
+ # @api private
22
+ # @param type [Object]
23
+ # @return [void]
24
+ # @raise [ArgumentError]
25
+ def self.check_type!(type)
26
+ return if type.is_a?(Class) && type < Annotation
27
+
28
+ raise ArgumentError, "#{type.inspect} is not an annotation type; include Adornment::Annotation[...]"
29
+ end
30
+
31
+ # @api private
32
+ # @param args [Array] positional arguments
33
+ # @param options [Hash] keyword arguments
34
+ # @return [String] call arguments as written: `Integer, default: 1`
35
+ def self.render_arguments(args, options)
36
+ [*args.map(&:inspect), *options.map { |key, value| "#{key}: #{value.inspect}" }].join(", ")
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Adornment
4
+ module Core
5
+ # One declaration found along a method's ancestor chain (see {Reflection#declarations_for}).
6
+ #
7
+ # @!attribute [r] annotation
8
+ # @return [Adornment::Annotation]
9
+ # @!attribute [r] owner
10
+ # @return [Module] the class or module that declared it
11
+ # @!attribute [r] method_name
12
+ # @return [Symbol]
13
+ # @!attribute [r] singleton
14
+ # @return [Boolean] whether it annotates a singleton method of `owner`
15
+ # @!attribute [r] resolved
16
+ # @return [Boolean] whether it is attached to the method Ruby's lookup reaches
17
+ Declaration = Data.define(:annotation, :owner, :method_name, :singleton, :resolved)
18
+
19
+ # Owner API for reading annotations. Activated owners have it through {Target}; classes and
20
+ # modules that include, prepend or extend an activated module receive it automatically.
21
+ #
22
+ # Queries read annotations from the *resolved method*: the method Ruby would call for
23
+ # the given name. An override does not inherit annotations from the method it replaces.
24
+ # {#find_annotation} and {#declarations_for} also search overridden methods along the
25
+ # ancestor chain. Query results are frozen.
26
+ module Reflection
27
+ # Annotations of the resolved method.
28
+ #
29
+ # @param name [Symbol, String]
30
+ # @param type [Class, nil] only annotations that are `is_a?(type)`
31
+ # @param singleton [Boolean] query the singleton method
32
+ # @return [Array<Adornment::Annotation>] frozen, in declaration order
33
+ def annotations_for(name, type = nil, singleton: false)
34
+ Core::Query.annotations_for(self, name.to_sym, type, singleton)
35
+ end
36
+
37
+ # Every declaration for `name` along the ancestor chain, nearest owner first.
38
+ #
39
+ # @param name [Symbol, String]
40
+ # @param singleton [Boolean] walk the singleton method
41
+ # @return [Array<Adornment::Core::Declaration>] frozen; the resolved method's come first, with
42
+ # `resolved: true`
43
+ def declarations_for(name, singleton: false)
44
+ name = name.to_sym
45
+ result = []
46
+ resolved = true
47
+ Core::Query.each_entry(self, name, singleton) do |holder, side, annotations|
48
+ annotations.each do |annotation|
49
+ result << Declaration.new(annotation: annotation, owner: holder, method_name: name,
50
+ singleton: side == :singleton, resolved: resolved)
51
+ end
52
+ resolved = false
53
+ end
54
+ result.freeze
55
+ end
56
+
57
+ # The annotation of `type` on the resolved method, or nil.
58
+ #
59
+ # @param name [Symbol, String]
60
+ # @param type [Class] a non-repeatable annotation type; matches with `is_a?(type)`
61
+ # @param singleton [Boolean] query the singleton method
62
+ # @return [Adornment::Annotation, nil]
63
+ # @raise [ArgumentError] for repeatable types (use {#annotations_for}) or non-annotation types
64
+ def annotation(name, type, singleton: false)
65
+ Core::Utils.check_type!(type)
66
+ raise ArgumentError, "#{type} is repeatable; use annotations_for(name, #{type})" if type.repeatable?
67
+
68
+ Core::Query.resolved_entry(self, name.to_sym, singleton).find { |a| a.is_a?(type) }
69
+ end
70
+
71
+ # The annotation of `type` on the resolved method or, failing that, on the nearest method it
72
+ # overrides along the ancestor chain; nil when none has one.
73
+ #
74
+ # @param name [Symbol, String]
75
+ # @param type [Class] a non-repeatable annotation type; matches with `is_a?(type)`
76
+ # @param singleton [Boolean] query the singleton method
77
+ # @return [Adornment::Annotation, nil]
78
+ # @raise [ArgumentError] for repeatable types (use {#declarations_for}) or non-annotation
79
+ # types
80
+ def find_annotation(name, type, singleton: false)
81
+ Core::Utils.check_type!(type)
82
+ raise ArgumentError, "#{type} is repeatable; use declarations_for(name)" if type.repeatable?
83
+
84
+ Core::Query.each_entry(self, name.to_sym, singleton) do |_holder, _side, annotations|
85
+ annotations.each { |a| return a if a.is_a?(type) }
86
+ end
87
+ nil
88
+ end
89
+
90
+ # Every method whose resolved method is annotated with `type` (see {#annotations_for}),
91
+ # including methods inherited from ancestors.
92
+ #
93
+ # @param type [Class] an annotation type; matches with `is_a?(type)`
94
+ # @param singleton [Boolean] look at singleton methods
95
+ # @return [Hash{Symbol => Adornment::Annotation}] frozen, for non-repeatable types
96
+ # @return [Hash{Symbol => Array<Adornment::Annotation>}] frozen, for repeatable types
97
+ # @raise [ArgumentError] when `type` is not an annotation type
98
+ def annotated_with(type, singleton: false)
99
+ Core::Utils.check_type!(type)
100
+ result = {}
101
+ Core::Query.candidate_names(self, singleton).each do |name|
102
+ found = Core::Query.annotations_for(self, name, type, singleton)
103
+ next if found.empty?
104
+
105
+ result[name] = type.repeatable? ? found : found.first
106
+ end
107
+ result.freeze
108
+ end
109
+
110
+ # Only the annotations this class or module itself attached to `name`, while it still
111
+ # defines the method.
112
+ #
113
+ # @param name [Symbol, String]
114
+ # @param type [Class, nil] only annotations that are `is_a?(type)`
115
+ # @param singleton [Boolean] query the singleton method
116
+ # @return [Array<Adornment::Annotation>] frozen
117
+ def own_annotations_for(name, type = nil, singleton: false)
118
+ name = name.to_sym
119
+ entry = Core::State.of(self)&.entry(singleton ? :singleton : :instance, name)
120
+ return Core::State::NONE unless entry && Core::Query.own?(singleton ? singleton_class : self, name)
121
+
122
+ type ? entry.select { |a| a.is_a?(type) }.freeze : entry
123
+ end
124
+
125
+ # The methods this class or module itself annotated and still defines, in the order they were
126
+ # first annotated (re-annotating a method keeps its place).
127
+ #
128
+ # @param singleton [Boolean] list singleton methods
129
+ # @return [Array<Symbol>] frozen
130
+ def own_annotated_methods(singleton: false)
131
+ side = singleton ? :singleton : :instance
132
+ entries = Core::State.of(self)&.entries(side) or return Core::State::NONE
133
+ mod = singleton ? singleton_class : self
134
+ entries.filter_map { |name, annotations| name if !annotations.empty? && Core::Query.own?(mod, name) }.freeze
135
+ end
136
+
137
+ # Annotations declared on this owner (in the current fiber) that are still waiting for a
138
+ # definition.
139
+ #
140
+ # @return [Array<Adornment::Annotation>] frozen, in declaration order
141
+ def pending_adornments
142
+ Core::Pending.peek(self).map(&:annotation).freeze
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Adornment
4
+ module Core
5
+ # The gem version.
6
+ VERSION = "0.1.0"
7
+ end
8
+ end
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ # adornment-core: the declarative metadata engine behind Adornment.
4
+ #
5
+ # require "adornment/core"
6
+ #
7
+ # class Traced
8
+ # include Adornment::Annotation[]
9
+ #
10
+ # attr_reader :span
11
+ #
12
+ # def initialize(span: nil)
13
+ # @span = span
14
+ # end
15
+ # end
16
+ #
17
+ # class Orders
18
+ # include Adornment[Traced]
19
+ #
20
+ # Traced(span: "orders.fetch")
21
+ # def fetch(id) = id
22
+ # end
23
+ #
24
+ # Orders.annotations_for(:fetch) # => [#<Traced span="orders.fetch">]
25
+ #
26
+ # This file is the public facade: every top-level `Adornment.*` method is declared here and
27
+ # delegates to an internal component under `Adornment::Core`. The supported surface is this
28
+ # facade, the owner API ({Adornment::Core::Target}, {Adornment::Core::Reflection}), the records
29
+ # and errors listed in the README. Everything else under `Adornment::Core` is internal.
30
+
31
+ require_relative "core/version"
32
+ require_relative "core/errors"
33
+
34
+ unless RUBY_ENGINE == "ruby"
35
+ raise Adornment::UnsupportedPlatformError,
36
+ "adornment-core requires CRuby (MRI); #{RUBY_ENGINE} #{RUBY_ENGINE_VERSION} is not supported"
37
+ end
38
+
39
+ require_relative "core/immutable"
40
+ require_relative "core/private/state"
41
+ require_relative "core/private/site"
42
+ require_relative "core/private/utils"
43
+ require_relative "core/private/pending"
44
+ require_relative "core/private/attachment"
45
+ require_relative "core/private/query"
46
+ require_relative "core/private/annotation_methods"
47
+ require_relative "core/reflection"
48
+ require_relative "core/annotation"
49
+ require_relative "core/hooks"
50
+ require_relative "core/base"
51
+
52
+ # Annotations for Ruby methods: types, declarations above `def`, reflection and verification.
53
+ # `Adornment[...]`, {Adornment::Annotation} and the errors live here; the owner API and records
54
+ # live under {Adornment::Core}.
55
+ module Adornment
56
+ class << self
57
+ # @!group Activation
58
+
59
+ # The recommended activation: `include Adornment[Traced, Cached]`, `include Adornment[Trace:
60
+ # Traced]` (an alias names the declaration method), or `include Adornment[]` (no declaration
61
+ # methods; declare with `adorn`).
62
+ #
63
+ # @param types [Array<Class>] annotation types; each declaration method is named after its
64
+ # type (`Traced(...)`)
65
+ # @param aliases [Hash{Symbol => Class}] declaration method name => annotation type
66
+ # @return [Adornment::Core::Base] a module whose inclusion activates the includer
67
+ # @raise [ArgumentError] for a class that is not an annotation type
68
+ def [](*types, **aliases)
69
+ Core::Base.new(types, aliases)
70
+ end
71
+
72
+ # @param owner [Object]
73
+ # @return [Boolean] whether `owner` is an activated class or module (it or an ancestor used
74
+ # `include Adornment[...]` or `extend Adornment::Core::Target`)
75
+ def activated?(owner)
76
+ Core::Attachment.activated?(owner)
77
+ end
78
+
79
+ # @!group Discovery
80
+
81
+ # Every registered class or module with its own methods annotated with `type`: the defining
82
+ # owners, not subclasses or includers that inherit those methods (ask those with
83
+ # `Owner.annotated_with`).
84
+ #
85
+ # @param type [Class] an annotation type; annotations match with `is_a?(type)`
86
+ # @param singleton [Boolean] look at singleton methods
87
+ # @return [Hash{Module => Hash{Symbol => Annotation}}] frozen; for repeatable types each value
88
+ # is a frozen Array of annotations
89
+ # @raise [ArgumentError] when `type` is not an annotation type
90
+ def annotated_with(type, singleton: false)
91
+ Core::Utils.check_type!(type)
92
+ side = singleton ? :singleton : :instance
93
+ result = {}
94
+ Core::Registry.owners.each do |owner|
95
+ methods = {}
96
+ mod = singleton ? owner.singleton_class : owner
97
+ Core::State.of(owner)&.entries(side)&.each do |name, annotations|
98
+ found = annotations.select { |a| a.is_a?(type) }
99
+ next if found.empty? || !Core::Query.own?(mod, name) # removed since
100
+
101
+ methods[name] = type.repeatable? ? found.freeze : found.first
102
+ end
103
+ result[owner] = methods.freeze if methods.any?
104
+ end
105
+ result.freeze
106
+ end
107
+
108
+ # @return [Array<Module>] the registered (activated), still reachable owners. The registry
109
+ # holds owners weakly.
110
+ def registered_owners
111
+ Core::Registry.owners
112
+ end
113
+
114
+ # @!group Verification and diagnostics
115
+
116
+ # Raises one aggregated error for every declaration still pending in the current fiber (and
117
+ # clears them), and runs the lint over the registered, reachable owners.
118
+ #
119
+ # @param lint [:warn, :error, false] how to run the bare-constant lint: print its diagnostics,
120
+ # raise them, or skip it
121
+ # @return [nil]
122
+ # @raise [Adornment::LeftoverAnnotations] for declarations that never attached
123
+ # @raise [Adornment::LintError] with `lint: :error`, when the lint finds something
124
+ # @raise [ArgumentError] for any other `lint:` value
125
+ def verify_all!(lint: :warn)
126
+ unless [:warn, :error, false].include?(lint)
127
+ raise ArgumentError, "lint: must be :warn, :error or false, got #{lint.inspect}"
128
+ end
129
+
130
+ owners = Core::Registry.owners
131
+ leftovers = Core::Pending.take_all
132
+ diagnostics = lint ? Core::Lint.run(owners) : []
133
+ diagnostics.each { |d| warn("adornment: #{d}") } if lint == :warn
134
+ raise LeftoverAnnotations, leftovers unless leftovers.empty?
135
+ raise LintError, diagnostics if lint == :error && !diagnostics.empty?
136
+
137
+ nil
138
+ end
139
+
140
+ # Runs the heuristic bare-constant lint.
141
+ #
142
+ # @param targets [Array<Module, String>] registered owners or file paths
143
+ # @return [Array<Adornment::Core::Lint::Diagnostic>]
144
+ # @raise [LoadError] when Prism cannot be loaded
145
+ def lint(*targets)
146
+ Core::Lint.run(targets.flatten)
147
+ end
148
+
149
+ # Removes an owner from global discovery and verification (idempotent) and drops its pending
150
+ # declarations. Its hooks stay installed and its metadata stays readable through
151
+ # its own reflection API; activating it again registers it again.
152
+ #
153
+ # @param owner [Module]
154
+ # @return [nil]
155
+ def unregister(owner)
156
+ Core::Registry.unregister(owner)
157
+ Core::Pending.take(owner)
158
+ nil
159
+ end
160
+
161
+ # @!group Utilities for extension gems
162
+
163
+ # The freezing rule of annotation values: Array, Hash, Set and String values are copied and
164
+ # deep-frozen (the argument is never mutated); other objects are returned as they are.
165
+ #
166
+ # @param value [Object]
167
+ # @return [Object] a deep-frozen copy, or `value` itself when it is already deeply immutable or
168
+ # not a container or String
169
+ def immutable_copy(value)
170
+ Core::Immutable.copy(value)
171
+ end
172
+
173
+ # @!endgroup
174
+
175
+ private
176
+
177
+ def append_features(_base)
178
+ raise ArgumentError, "include Adornment[...] (with the annotation types), not bare Adornment: " \
179
+ "a bare include would leak Adornment's constants into your constant lookup"
180
+ end
181
+
182
+ def prepend_features(_base)
183
+ raise ArgumentError, "use include Adornment[...], not prepend Adornment"
184
+ end
185
+
186
+ def extend_object(_base)
187
+ raise ArgumentError, "use include Adornment[...] or extend Adornment::Core::Target, not extend Adornment"
188
+ end
189
+ end
190
+
191
+ # The annotation engine. The owner API ({Base}, {Target}, {Reflection}), the records
192
+ # ({Declaration}, {Pending::PendingDeclaration}, {Site}, {Lint::Diagnostic}), {Lint} and
193
+ # {VERSION} are public; everything marked `@api private` is internal and may change in any
194
+ # release.
195
+ module Core
196
+ # Loaded on first use: the lint (which parses source with Prism) is not needed to declare or
197
+ # reflect on annotations.
198
+ autoload :Lint, File.expand_path("core/diagnostics/lint", __dir__)
199
+ end
200
+ end
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: adornment-core
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Yaroslav Zahoruiko
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: |
13
+ Adornment Core provides declarative method annotations for Ruby.
14
+ Define annotation types, attach immutable metadata to the next method
15
+ definition, and inspect annotations along Ruby's method lookup chain.
16
+ Detects annotation declarations left without a method.
17
+
18
+ Pure Ruby with no runtime dependencies. Does not wrap or replace methods.
19
+ Supports CRuby 3.3 and later.
20
+ email:
21
+ - zahoruiko.yaroslav@gmail.com
22
+ executables: []
23
+ extensions: []
24
+ extra_rdoc_files: []
25
+ files:
26
+ - CHANGELOG.md
27
+ - LICENSE
28
+ - README.md
29
+ - lib/adornment/core.rb
30
+ - lib/adornment/core/annotation.rb
31
+ - lib/adornment/core/base.rb
32
+ - lib/adornment/core/diagnostics/lint.rb
33
+ - lib/adornment/core/errors.rb
34
+ - lib/adornment/core/hooks.rb
35
+ - lib/adornment/core/immutable.rb
36
+ - lib/adornment/core/private/annotation_methods.rb
37
+ - lib/adornment/core/private/attachment.rb
38
+ - lib/adornment/core/private/pending.rb
39
+ - lib/adornment/core/private/query.rb
40
+ - lib/adornment/core/private/site.rb
41
+ - lib/adornment/core/private/state.rb
42
+ - lib/adornment/core/private/utils.rb
43
+ - lib/adornment/core/reflection.rb
44
+ - lib/adornment/core/version.rb
45
+ homepage: https://github.com/YaroslavZahoruiko/adornment-core
46
+ licenses:
47
+ - MIT
48
+ metadata:
49
+ homepage_uri: https://github.com/YaroslavZahoruiko/adornment-core
50
+ changelog_uri: https://github.com/YaroslavZahoruiko/adornment-core/blob/main/CHANGELOG.md
51
+ bug_tracker_uri: https://github.com/YaroslavZahoruiko/adornment-core/issues
52
+ rubygems_mfa_required: 'true'
53
+ rdoc_options: []
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '3.3'
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubygems_version: 4.0.20
68
+ specification_version: 4
69
+ summary: Declarative method annotations and metadata for Ruby.
70
+ test_files: []