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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +34 -0
- data/LICENSE +21 -0
- data/README.md +669 -0
- data/lib/adornment/core/annotation.rb +326 -0
- data/lib/adornment/core/base.rb +116 -0
- data/lib/adornment/core/diagnostics/lint.rb +150 -0
- data/lib/adornment/core/errors.rb +62 -0
- data/lib/adornment/core/hooks.rb +77 -0
- data/lib/adornment/core/immutable.rb +56 -0
- data/lib/adornment/core/private/annotation_methods.rb +61 -0
- data/lib/adornment/core/private/attachment.rb +122 -0
- data/lib/adornment/core/private/pending.rb +142 -0
- data/lib/adornment/core/private/query.rb +136 -0
- data/lib/adornment/core/private/site.rb +45 -0
- data/lib/adornment/core/private/state.rb +117 -0
- data/lib/adornment/core/private/utils.rb +40 -0
- data/lib/adornment/core/reflection.rb +146 -0
- data/lib/adornment/core/version.rb +8 -0
- data/lib/adornment/core.rb +200 -0
- metadata +70 -0
|
@@ -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,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: []
|