yuuki 0.1.12 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7409615113f6585101139dd2df95c8f737d0f291016351b152bc3e01390ab8d0
4
- data.tar.gz: 3941d1d916033d8cbf5944f55cad051f74213d8d0d809476d155800b60204b5f
3
+ metadata.gz: 73ee236ec079617db802006ec29558cb42cbeb17143487fb5d03f9b0781e2211
4
+ data.tar.gz: 507eaffb469b260061af77703546bfb20bb4c9e5c8d62c26b946a861a2c69f36
5
5
  SHA512:
6
- metadata.gz: 2a7dc76ec6ac0d04fbc8a734a5e79994a3f58fbb1f8d8c888c586e66442ce1eb5398ce9e1a413c9edb0145a3bfeb479843bbc2c721dd317305fdeb242bac8e3c
7
- data.tar.gz: b22db32336dcceced700ffb15b65221c8020db7243df8b9cf79adc7a9d6a51426cfade7826a38045e74eb5da4d2ce5b2df0d76c2a90a5924591fe65c76fe9373
6
+ metadata.gz: 70b198d6f2b79d3df1a27bd750d5ded8d7a1a827b6078f90f34b03ff701716d60a69dd0baa5e2774c4da0b2af7ccbc4b46fb9139056d301a6cfa59b5adcbe714
7
+ data.tar.gz: 7a8cdefa1d6aa71c65f45b6dda02e6b6987435d82efc821a31a3d40d2921443ad69088bd7e83a339aed22462894ac33afa84416f20af93ecb1468c9895a497d2
@@ -0,0 +1,50 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ rspec:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ ruby: ['3.0', 'ruby']
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: ruby/setup-ruby@v1
17
+ with:
18
+ ruby-version: ${{ matrix.ruby }}
19
+ bundler-cache: true
20
+ - run: bundle exec rspec
21
+
22
+ rubocop:
23
+ runs-on: ubuntu-latest
24
+ steps:
25
+ - uses: actions/checkout@v4
26
+ - uses: ruby/setup-ruby@v1
27
+ with:
28
+ ruby-version: ruby
29
+ bundler-cache: true
30
+ - uses: reviewdog/action-rubocop@v2
31
+ with:
32
+ github_token: ${{ secrets.github_token }}
33
+ reporter: ${{ github.event_name == 'pull_request' && 'github-pr-review' || 'github-check' }}
34
+ skip_install: true
35
+ use_bundler: true
36
+ fail_level: any
37
+ rubocop_flags: lib
38
+
39
+ rbs:
40
+ runs-on: ubuntu-latest
41
+ strategy:
42
+ matrix:
43
+ ruby: ['3.0', 'ruby']
44
+ steps:
45
+ - uses: actions/checkout@v4
46
+ - uses: ruby/setup-ruby@v1
47
+ with:
48
+ ruby-version: ${{ matrix.ruby }}
49
+ bundler-cache: true
50
+ - run: bundle exec rbs -I sig validate
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,21 @@
1
+ ---
2
+ AllCops:
3
+ TargetRubyVersion: 3.0
4
+ Metrics/AbcSize:
5
+ Enabled: false
6
+ Metrics/BlockLength:
7
+ Enabled: false
8
+ Metrics/ClassLength:
9
+ Enabled: false
10
+ Metrics/CyclomaticComplexity:
11
+ Enabled: false
12
+ Metrics/MethodLength:
13
+ Enabled: false
14
+ Metrics/PerceivedComplexity:
15
+ Enabled: false
16
+ Style/Documentation:
17
+ Enabled: false
18
+ Style/NumericPredicate:
19
+ EnforcedStyle: comparison
20
+ Style/SpecialGlobalVars:
21
+ EnforcedStyle: use_perl_names
data/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  A caller / runner framework for Ruby.
4
4
 
5
+ Yuuki lets you tag methods of a class and invoke them by tag. Arguments are delivered to each method by parameter name, and methods can be run in threads, ordered by priority, or executed periodically.
6
+
5
7
  ## Installation
6
8
 
7
9
  Add this line to your application's Gemfile:
@@ -20,33 +22,132 @@ Or install it yourself as:
20
22
 
21
23
  ## Usage
22
24
 
25
+ ### Basics
26
+
27
+ Define a runner class that inherits `Yuuki::Runner`. A method named `on_<tag>` is automatically registered with the tag `<tag>`. Any other method can be registered with the `on` decorator, which applies to the next method definition.
28
+
23
29
  ```ruby
24
30
  require 'yuuki'
25
31
 
26
- class HogeClass
27
- extend Yuuki::Runner
32
+ class Greeter < Yuuki::Runner
33
+ # registered with the tag :greet (taken from the method name)
34
+ def on_greet
35
+ puts 'hello!'
36
+ end
37
+
38
+ # registered with the tags :greet and :farewell
39
+ on :greet
40
+ on :farewell
41
+ def say_goodbye
42
+ puts 'goodbye!'
43
+ end
44
+ end
45
+
46
+ yuuki = Yuuki::Caller.new(Greeter)
47
+ yuuki.run(:greet)
48
+ # hello!
49
+ # goodbye!
50
+
51
+ yuuki.run(:farewell)
52
+ # goodbye!
53
+ ```
54
+
55
+ `Yuuki::Caller.new` (and `#add`) accepts runner classes and runner instances. A class is instantiated automatically. Inside a runner method, `yuuki` returns the caller the instance belongs to.
56
+
57
+ ### Passing arguments
58
+
59
+ Keyword arguments given to `run` are delivered to each method by parameter name — no matter whether the parameter is positional or keyword. Extra arguments are simply ignored, and a missing required parameter raises `Yuuki::Error`. A block given to `run` is forwarded as the block of each method.
60
+
61
+ ```ruby
62
+ class Worker < Yuuki::Runner
63
+ def on_process(name, size: 1)
64
+ puts "processing #{name} (size: #{size})"
65
+ end
66
+ end
67
+
68
+ yuuki = Yuuki::Caller.new(Worker)
69
+ yuuki.run(:process, name: 'job', size: 3, unused: 42)
70
+ # processing job (size: 3)
71
+ ```
72
+
73
+ ### Decorators
74
+
75
+ Decorators are class methods that annotate the **next** method definition.
76
+
77
+ | Decorator | Effect |
78
+ | --- | --- |
79
+ | `on :tag` | Adds a tag. Can be specified multiple times. |
80
+ | `priority n` | Runs methods with higher priority first (default 0). Also used as `Thread#priority` for threaded methods. |
81
+ | `thread` | Runs the method in a new thread. |
82
+ | `periodic interval` | Runs the method every `interval` seconds (`Yuuki::PeriodicCaller` only). |
83
+ | `first_run` | Runs the method once on startup (`Yuuki::PeriodicCaller` only). |
28
84
 
29
- add :a
30
- def a
31
- puts 'a'
85
+ ### Threading
86
+
87
+ Methods marked with `thread` run in their own thread; `run` returns without waiting for them.
88
+
89
+ ```ruby
90
+ class Background < Yuuki::Runner
91
+ on :heavy
92
+ thread
93
+ def heavy_task
94
+ sleep 1
95
+ puts 'done'
96
+ end
97
+ end
98
+
99
+ yuuki = Yuuki::Caller.new(Background)
100
+ yuuki.run(:heavy) # returns immediately
101
+ yuuki.running? # => true
102
+ yuuki.join # waits for all running threads
103
+ ```
104
+
105
+ ### Periodic execution
106
+
107
+ `Yuuki::PeriodicCaller` runs `periodic` methods repeatedly and `first_run` methods once on startup. It is not required by default:
108
+
109
+ ```ruby
110
+ require 'yuuki/periodic_caller'
111
+
112
+ class Watcher < Yuuki::Runner
113
+ first_run
114
+ def setup
115
+ puts 'starting'
116
+ end
117
+
118
+ periodic 60
119
+ def check
120
+ puts 'checking...'
32
121
  end
33
122
 
34
- add :b
35
- tag :b, :tag_b
36
- def b
37
- puts 'b'
123
+ periodic 86_400
124
+ def daily
125
+ puts 'a new day has come'
38
126
  end
39
127
  end
40
128
 
41
- yuuki = Yuuki::Caller.new(HogeClass)
42
- yuuki.run
43
- # a
44
- # b
129
+ yuuki = Yuuki::PeriodicCaller.new(Watcher)
130
+ yuuki.on_error { |error| warn error.message }
131
+ yuuki.run # blocks forever
132
+ ```
133
+
134
+ Intervals are aligned to wall-clock boundaries in local time: `periodic 60` fires at the top of every minute, and `periodic 86_400` fires at local midnight (pass a GMT offset as the first argument of `run` to change the time zone). Intervals shorter than 1 second are not supported.
135
+
136
+ An exception raised by a (non-threaded) runner method is passed to the `on_error` callback and the loop keeps running. Without `on_error`, the exception stops `run`.
45
137
 
46
- yuuki.run_tag(:tag_b)
47
- # b
138
+ ### Loading runner files
139
+
140
+ `Yuuki::Caller.require_dir` requires all `*.rb` files in a directory:
141
+
142
+ ```ruby
143
+ Yuuki::Caller.require_dir('runners')
144
+ Yuuki::Caller.require_dir('runners', recursive: true)
48
145
  ```
49
146
 
147
+ ### Graceful shutdown with Yoshinon
148
+
149
+ By default, each method invocation is wrapped in a [Yoshinon](https://github.com/Ishotihadus/yoshinon) lock so that a trapped signal waits for running methods to finish. Pass `use_yoshinon: false` to `Yuuki::Caller.new` to disable this.
150
+
50
151
  ## Contributing
51
152
 
52
153
  Bug reports and pull requests are welcome on GitHub at [https://github.com/ishotihadus/yuuki](https://github.com/ishotihadus/yuuki).
data/lib/yuuki/caller.rb CHANGED
@@ -1,18 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'yuuki/runner'
3
+ require 'set'
4
4
  require 'yoshinon'
5
+ require 'yuuki/runner'
5
6
 
6
7
  module Yuuki
7
8
  class Caller
8
- # requires all the rb files in the given directory
9
- # @param [String] require_dir directory
10
- # @param [Boolean] recursive
11
9
  def self.require_dir(require_dir, recursive: false)
12
10
  Dir.glob(recursive ? "#{require_dir}/**/*.rb" : "#{require_dir}/*.rb").each { |file| require file }
13
11
  end
14
12
 
15
- # @param [Object] instances
16
13
  def initialize(*instances, use_yoshinon: true)
17
14
  @instances = Set.new
18
15
  @threads = []
@@ -20,110 +17,53 @@ module Yuuki
20
17
  @use_yoshinon = use_yoshinon
21
18
  end
22
19
 
23
- # adds instances to yuuki
24
- # @param [Object] instances
25
20
  def add(*instances)
26
21
  instances.each do |instance|
27
- # create instance if class is given
28
22
  if instance.is_a?(Class)
29
23
  klass = instance
30
- # check the klass is extended
31
- unless klass.singleton_class.include?(Yuuki::Runner)
32
- raise Yuuki::Error, 'Runner instance must be extend Yuuki::Runner'
33
- end
24
+ raise Yuuki::Error, 'Runner instance must be inherit Yuuki::Runner' unless klass < Yuuki::Runner
34
25
 
26
+ # allocate + explicit initialize so that @yuuki is already set when
27
+ # the runner's #initialize runs
35
28
  instance = instance.allocate
36
29
  instance.instance_variable_set(:@yuuki, self)
37
30
  instance.send(:initialize)
38
31
  else
39
- # check the klass is extended
40
- unless instance.class.singleton_class.include?(Yuuki::Runner)
41
- raise Yuuki::Error, 'Runner instance must be extend Yuuki::Runner'
42
- end
32
+ raise Yuuki::Error, 'Runner instance must be inherit Yuuki::Runner' unless instance.is_a?(Yuuki::Runner)
43
33
 
44
- # add @yuuki to the instance
45
34
  instance.instance_variable_set(:@yuuki, self)
46
35
  end
47
-
48
- # regist
49
36
  @instances << instance
50
37
  end
51
38
  end
52
39
 
53
- # returns runners
54
- # @return [Array<[Method, Hash<Symbol, Object>]>]
55
- def runners
40
+ # NOTE: intentionally shadows Object#methods; returns [Method, meta] pairs
41
+ # sorted by priority (highest first)
42
+ def methods
56
43
  list = @instances.flat_map do |instance|
57
- methods = instance.class.instance_variable_get(:@yuuki_methods)
58
- methods.select { |_sig, meta| meta[:enabled] }.map { |sig, meta| [instance.method(sig), meta] }
44
+ methods = instance.class.yuuki_methods
45
+ methods.map { |sig, meta| [instance.method(sig), meta] }
59
46
  end
60
- list.sort_by { |_method, meta| -(meta[:priority] || 0) }
47
+ list.sort_by! { |_method, meta| -(meta[:priority] || 0) }
48
+ list
61
49
  end
62
50
 
63
- # returns all tags defined as Set
64
- def tags
65
- tags = @instances.flat_map do |instance|
66
- methods = instance.class.instance_variable_get(:@yuuki_methods)
67
- methods.select { |_sig, meta| meta[:enabled] }.flat_map { |_sig, meta| meta[:tags] }
51
+ def run(*tags, **args, &block)
52
+ selector = proc do |_method, meta|
53
+ meta[:tags] && !(meta[:tags] & tags).empty?
68
54
  end
69
- ret = Set.new
70
- tags.each { |e| ret += e if e }
71
- ret
72
- end
73
-
74
- # runs all methods
75
- # @param [Object] args arguments
76
- def run(**args, &)
77
- run_internal(runners, args, &)
78
- end
79
-
80
- # runs all selected methods
81
- # @param [Proc] proc_select
82
- # @param [Object] args arguments
83
- def run_select(proc_select, **args, &)
84
- run_internal(runners.select(&proc_select), args, &)
55
+ run_select(selector, **args, &block)
85
56
  end
86
57
 
87
- # set whether to ignore ``tag not associated'' error
88
- def ignore_tag_error(enabled: true)
89
- @ignore_tag_error = enabled
58
+ def run_select(selector, **args, &block)
59
+ run_internal(methods.select(&selector), args, &block)
90
60
  end
91
61
 
92
- # runs all methods with the specified tags
93
- # @param [Array<Symbol>] tags
94
- # @param [Object] args arguments
95
- def run_tag(*tags, **args, &)
96
- t = self.tags
97
- tags.each do |e|
98
- next if t.include?(e)
99
- raise Yuuki::Error, "tag `#{e}` is not associated" unless @ignore_tag_error
100
-
101
- warn "Yuuki Warning: tag `#{e}` is not associated"
102
- end
103
- run_select(proc { |_method, meta| meta[:tags]&.intersect?(tags) }, **args, &)
104
- end
105
-
106
- # runs the specific method
107
- # @param [Class, nil] klass
108
- # @param [Symbol, nil] method_sig method name
109
- # @param [Object] args arguments
110
- def run_method(klass, method_sig, **args, &)
111
- select_proc = proc do |method, _meta|
112
- flag_klass = klass ? method.receiver.instance_of?(klass) : true
113
- flag_method = method_sig ? method.name == method_sig : true
114
- flag_klass && flag_method
115
- end
116
- run_select(select_proc, **args, &)
117
- end
118
-
119
- # joins all runnning threads
120
62
  def join
121
63
  @threads.each(&:join)
122
64
  @threads.select!(&:alive?)
123
65
  end
124
66
 
125
- # returns whether any thread is alive
126
- # @return [Boolean]
127
67
  def alive?
128
68
  @threads.select!(&:alive?)
129
69
  !@threads.empty?
@@ -147,10 +87,13 @@ module Yuuki
147
87
  end
148
88
  end
149
89
 
90
+ # Maps the args hash onto the method's parameters by name. Positional
91
+ # optionals can only be filled left-to-right: once one is unspecified,
92
+ # passing any later :opt/:rest argument is an error (nonspecified_last_opt).
150
93
  def run_method_internal(method, args, &block)
151
94
  yoshinon = Yoshinon.lock if @use_yoshinon
152
95
  params = method.parameters
153
- return method[] if params.empty?
96
+ return method[&block] if params.empty?
154
97
 
155
98
  params_array = []
156
99
  params_hash = {}
@@ -160,7 +103,8 @@ module Yuuki
160
103
  case type
161
104
  when :req
162
105
  unless args.key?(name)
163
- raise Yuuki::Error, "A required argument '#{name}' was not found on running #{method.owner}::#{method.name}"
106
+ raise Yuuki::Error,
107
+ "A required argument '#{name}' was not found on running #{method.owner}::#{method.name}"
164
108
  end
165
109
 
166
110
  params_array << args[name]
@@ -205,10 +149,11 @@ module Yuuki
205
149
  params_hash[name] = args[name]
206
150
  end
207
151
  when :block
208
- params_block = args[name]
152
+ params_block = args[name] if args.key?(name)
209
153
  end
210
154
  end
211
- params_block = block unless params.any? { |type, _| type == :block }
155
+ # fall back to the block passed to run unless one was explicitly given via args
156
+ params_block = block unless params.any? { |type, name| type == :block && args.key?(name) }
212
157
  params_hash.empty? ? method[*params_array, &params_block] : method[*params_array, **params_hash, &params_block]
213
158
  ensure
214
159
  yoshinon&.unlock
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yoshinon'
4
+ require 'yuuki/caller'
5
+ require 'yuuki/runner'
6
+
7
+ module Yuuki
8
+ class PeriodicCaller < Caller
9
+ attr_reader :first_run, :current_time
10
+
11
+ def initialize(*instances, use_yoshinon: true)
12
+ super
13
+ @first_run = true
14
+ end
15
+
16
+ def on_error(&block)
17
+ @on_error = block
18
+ end
19
+
20
+ def start(gmtoff = Time.now.gmtoff, **args, &block)
21
+ last_time = nil
22
+ loop do
23
+ @current_time = Time.now.to_f
24
+ begin
25
+ selector = proc do |_method, meta|
26
+ next true if @first_run && meta[:first_run]
27
+ next false unless meta[:periodic]
28
+ next false unless last_time
29
+
30
+ # gmtoff shifts epoch seconds into local time so that intervals
31
+ # align with local clock boundaries (e.g. 86400 fires at local midnight).
32
+ # Fires when a multiple of the interval lies in (last_time, current_time].
33
+ c = @current_time + gmtoff
34
+ l = last_time + gmtoff
35
+ (l.div(meta[:periodic]) + 1) * meta[:periodic] <= c
36
+ end
37
+ run_select(selector, **args, &block)
38
+ rescue StandardError
39
+ @on_error ? @on_error[$!] : raise
40
+ end
41
+ @first_run = false
42
+
43
+ last_time = @current_time
44
+ # wake at the next wall-clock second; the boundary check above makes
45
+ # ticks drift-free, but intervals below 1 second cannot be honored
46
+ sleep_duration = (@current_time + 1).floor - Time.now.to_f
47
+ sleep(sleep_duration) if sleep_duration > 0
48
+ end
49
+ end
50
+ end
51
+ end
data/lib/yuuki/runner.rb CHANGED
@@ -1,63 +1,77 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Yuuki
4
- module Runner
5
- def self.extended(base)
6
- base.define_method(:yuuki) do
7
- @yuuki
8
- end
4
+ class Runner
5
+ attr_reader :yuuki
6
+
7
+ # Decorator-style DSL (`on` / `priority` / `thread` / `periodic` / `first_run`)
8
+ # pushes entries here; the next instance-method definition consumes them
9
+ # (see method_added). Both this and yuuki_methods live in per-class ivars.
10
+ def self.accumulated_decorators
11
+ @accumulated_decorators ||= []
9
12
  end
10
13
 
11
- # adds methods to yuuki
12
- # @param [Array<Symbol>] methods method names
13
- def add(*methods)
14
+ def self.yuuki_methods
14
15
  @yuuki_methods ||= {}
15
- methods.each do |method|
16
- @yuuki_methods[method] ||= {}
17
- @yuuki_methods[method][:enabled] = true
18
- end
19
16
  end
20
17
 
21
- # deletes methods from yuuki
22
- # @param [Array<Symbol>] methods method names
23
- def delete(*methods)
24
- @yuuki_methods ||= {}
25
- methods.each do |method|
26
- @yuuki_methods[method] ||= {}
27
- @yuuki_methods[method][:enabled] = false
28
- end
18
+ # yuuki_methods is a per-class ivar, so copy it on inheritance to make
19
+ # runner methods defined in a superclass visible from the subclass.
20
+ # NOTE: methods added to the superclass after the subclass is defined are
21
+ # not reflected.
22
+ def self.inherited(subclass)
23
+ subclass.instance_variable_set(:@yuuki_methods, yuuki_methods.dup)
24
+ super
29
25
  end
30
26
 
31
- # adds tags to the method
32
- # @param [Symbol] method method name
33
- # @param [Array<Symbol>] tags tag names
34
- def tag(method, *tags)
35
- @yuuki_methods ||= {}
36
- @yuuki_methods[method] ||= {}
37
- @yuuki_methods[method][:tags] ||= Set.new
38
- @yuuki_methods[method][:tags].merge(tags)
27
+ def self.on(tag)
28
+ accumulated_decorators << [:tag, tag]
39
29
  end
40
30
 
41
- # enables threading for the methods
42
- # @param [Array<Symbol>] methods method names
43
- # @param [Boolean] enabled
44
- def thread(*methods, enabled: true)
45
- @yuuki_methods ||= {}
46
- methods.each do |method|
47
- @yuuki_methods[method] ||= {}
48
- @yuuki_methods[method][:thread] = enabled
49
- end
31
+ def self.priority(priority = 0)
32
+ accumulated_decorators << [:priority, priority]
50
33
  end
51
34
 
52
- # sets priority to the method
53
- # @param [Array<Symbol>] methods method names
54
- # @param [Numeric] value
55
- def priority(*methods, value)
56
- @yuuki_methods ||= {}
57
- methods.each do |method|
58
- @yuuki_methods[method] ||= {}
59
- @yuuki_methods[method][:priority] = value
35
+ def self.thread(enabled: true)
36
+ accumulated_decorators << [:thread, enabled]
37
+ end
38
+
39
+ def self.periodic(interval)
40
+ accumulated_decorators << [:periodic, interval]
41
+ end
42
+
43
+ def self.first_run(enabled: true)
44
+ accumulated_decorators << [:first_run, enabled]
45
+ end
46
+
47
+ # Registers the method as a runner method when decorators are pending or the
48
+ # name is `on_<tag>` (which implies the tag <tag>). Note this hook fires for
49
+ # ANY instance-method definition, including attr_* and private helpers, so
50
+ # pending decorators are consumed by whatever method comes next.
51
+ def self.method_added(method_name)
52
+ return if accumulated_decorators.empty? && !method_name.start_with?('on_')
53
+
54
+ options = {}
55
+ options[:tags] = [method_name[3..].to_sym] if method_name.start_with?('on_')
56
+
57
+ accumulated_decorators.each do |tag, elem|
58
+ if tag == :tag
59
+ options[:tags] ||= []
60
+ options[:tags] << elem
61
+ else
62
+ options[tag] = elem
63
+ end
60
64
  end
65
+ accumulated_decorators.clear
66
+
67
+ yuuki_methods[method_name] = options
68
+
69
+ super
70
+ end
71
+
72
+ def self.singleton_method_added(_method_name)
73
+ accumulated_decorators.clear
74
+ super
61
75
  end
62
76
  end
63
77
  end
data/lib/yuuki/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Yuuki
4
- VERSION = '0.1.12'
4
+ VERSION = '1.0.2'
5
5
  end
data/lib/yuuki.rb CHANGED
@@ -2,5 +2,6 @@
2
2
 
3
3
  require 'yuuki/caller'
4
4
  require 'yuuki/error'
5
+ require 'yuuki/periodic_caller'
5
6
  require 'yuuki/runner'
6
7
  require 'yuuki/version'
data/sig/yuuki/caller.rbs CHANGED
@@ -1,25 +1,43 @@
1
- # TypeProf 0.21.2
2
-
3
- # Classes
4
1
  module Yuuki
5
2
  class Caller
6
- @instances: Set[untyped]
3
+ # a runner method paired with its registered metadata
4
+ type runner_entry = [Method, meta]
5
+
6
+ type selector = ^(Method, meta) -> boolish
7
+
8
+ @instances: Set[Runner]
9
+
7
10
  @threads: Array[Thread]
8
11
 
12
+ @use_yoshinon: bool
13
+
9
14
  def self.require_dir: (String require_dir, ?recursive: bool) -> void
10
- def initialize: (*untyped instances) -> void
11
- def add: (*untyped instances) -> void
12
- def runners: -> Array[untyped]
13
- def run: (**untyped) -> void
14
- def run_select: (^(Method, Hash[Symbol, untyped]) -> bool proc_select, **untyped) -> void
15
- def run_tag: (*Symbol tags, **untyped) -> void
16
- def run_method: (Class klass, Symbol method_sig, **untyped) -> void
17
- def join: -> void
18
- def alive?: -> bool
15
+
16
+ def initialize: (*(Runner | Class) instances, ?use_yoshinon: bool) -> void
17
+
18
+ def add: (*(Runner | Class) instances) -> void
19
+
20
+ # NOTE: intentionally shadows Object#methods; returns [Method, meta] pairs
21
+ # sorted by priority (highest first)
22
+ def methods: () -> Array[runner_entry]
23
+
24
+ def run: (*Symbol tags, **untyped args) ?{ (?) -> untyped } -> void
25
+
26
+ def run_select: (selector selector, **untyped args) ?{ (?) -> untyped } -> void
27
+
28
+ def join: () -> void
29
+
30
+ def alive?: () -> bool
31
+
19
32
  alias running? alive?
20
33
 
21
34
  private
22
- def run_internal: (Array[untyped] runners, Hash[Symbol, untyped] args) -> void
23
- def run_method_internal: (Method method, Hash[Symbol, untyped] args) -> void
35
+
36
+ def run_internal: (Array[runner_entry] runners, Hash[Symbol, untyped] args) ?{ (?) -> untyped } -> void
37
+
38
+ # Maps the args hash onto the method's parameters by name. Positional
39
+ # optionals can only be filled left-to-right: once one is unspecified,
40
+ # passing any later :opt/:rest argument is an error (nonspecified_last_opt).
41
+ def run_method_internal: (Method method, Hash[Symbol, untyped] args) ?{ (?) -> untyped } -> untyped
24
42
  end
25
43
  end
data/sig/yuuki/error.rbs CHANGED
@@ -1,6 +1,3 @@
1
- # TypeProf 0.21.2
2
-
3
- # Classes
4
1
  module Yuuki
5
2
  class Error < StandardError
6
3
  end
@@ -0,0 +1,18 @@
1
+ module Yuuki
2
+ class PeriodicCaller < Caller
3
+ @on_error: (^(StandardError) -> untyped)?
4
+
5
+ # true until the first iteration of #run has finished
6
+ attr_reader first_run: bool
7
+
8
+ # epoch seconds of the current iteration; nil before #run is called
9
+ attr_reader current_time: Float?
10
+
11
+ def initialize: (*(Runner | Class) instances, ?use_yoshinon: bool) -> void
12
+
13
+ def on_error: () { (StandardError) -> untyped } -> void
14
+
15
+ # loops forever; only returns by raising (when no on_error callback is set)
16
+ def start: (?Integer gmtoff, **untyped args) ?{ (?) -> untyped } -> bot
17
+ end
18
+ end
data/sig/yuuki/runner.rbs CHANGED
@@ -1,14 +1,44 @@
1
- # TypeProf 0.21.2
2
-
3
- # Classes
4
1
  module Yuuki
5
- module Runner
6
- @yuuki_methods: Hash[Symbol, Hash[Symbol, untyped]]
7
-
8
- def add: (*Symbol methods) -> void
9
- def delete: (*Symbol methods) -> void
10
- def tag: (Symbol method, *Symbol tags) -> void
11
- def thread: (*Symbol methods, ?enabled: bool) -> void
12
- def priority: (*Symbol methods, Numeric value) -> void
2
+ class Runner
3
+ # a decorator entry: [:tag, Symbol] | [:priority, Integer] | [:thread, bool]
4
+ # | [:periodic, Numeric] | [:first_run, bool]
5
+ type decorator = [Symbol, untyped]
6
+
7
+ self.@accumulated_decorators: Array[decorator]
8
+
9
+ self.@yuuki_methods: Hash[Symbol, meta]
10
+
11
+ attr_reader yuuki: Caller?
12
+
13
+ # Decorator-style DSL (`on` / `priority` / `thread` / `periodic` / `first_run`)
14
+ # pushes entries here; the next instance-method definition consumes them
15
+ # (see method_added). Both this and yuuki_methods live in per-class ivars.
16
+ def self.accumulated_decorators: () -> Array[decorator]
17
+
18
+ def self.yuuki_methods: () -> Hash[Symbol, meta]
19
+
20
+ # yuuki_methods is a per-class ivar, so copy it on inheritance to make
21
+ # runner methods defined in a superclass visible from the subclass.
22
+ # NOTE: methods added to the superclass after the subclass is defined are
23
+ # not reflected.
24
+ def self.inherited: (singleton(Runner) subclass) -> void
25
+
26
+ def self.on: (Symbol tag) -> void
27
+
28
+ def self.priority: (?Integer priority) -> void
29
+
30
+ def self.thread: (?enabled: bool) -> void
31
+
32
+ def self.periodic: (Numeric interval) -> void
33
+
34
+ def self.first_run: (?enabled: bool) -> void
35
+
36
+ # Registers the method as a runner method when decorators are pending or the
37
+ # name is `on_<tag>` (which implies the tag <tag>). Note this hook fires for
38
+ # ANY instance-method definition, including attr_* and private helpers, so
39
+ # pending decorators are consumed by whatever method comes next.
40
+ def self.method_added: (Symbol method_name) -> void
41
+
42
+ def self.singleton_method_added: (Symbol _method_name) -> void
13
43
  end
14
44
  end
@@ -1,6 +1,3 @@
1
- # TypeProf 0.21.2
2
-
3
- # Classes
4
1
  module Yuuki
5
2
  VERSION: String
6
3
  end
data/sig/yuuki.rbs ADDED
@@ -0,0 +1,10 @@
1
+ module Yuuki
2
+ # metadata registered for a runner method (see Runner.method_added)
3
+ type meta = {
4
+ ?tags: Array[Symbol],
5
+ ?priority: Integer,
6
+ ?thread: bool,
7
+ ?periodic: Numeric,
8
+ ?first_run: bool
9
+ }
10
+ end
data/yuuki.gemspec CHANGED
@@ -18,15 +18,18 @@ Gem::Specification.new do |spec|
18
18
  # Specify which files should be added to the gem when it is released.
19
19
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
20
20
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
21
- `git ls-files -z`.split("\x0").reject {|f| f.match(%r{^(test|spec|features)/})}
21
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
22
22
  end
23
23
  spec.bindir = 'exe'
24
- spec.executables = spec.files.grep(%r{^exe/}) {|f| File.basename(f)}
24
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
25
25
  spec.require_paths = ['lib']
26
+ spec.required_ruby_version = '>= 3.0'
26
27
 
27
28
  spec.add_dependency 'yoshinon'
28
29
  spec.add_development_dependency 'bundler'
29
- spec.add_development_dependency 'minitest'
30
- spec.add_development_dependency 'pry'
30
+ spec.add_development_dependency 'irb'
31
31
  spec.add_development_dependency 'rake'
32
+ spec.add_development_dependency 'rbs'
33
+ spec.add_development_dependency 'rspec'
34
+ spec.add_development_dependency 'rubocop'
32
35
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yuuki
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.12
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ishotihadus
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-08-03 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: yoshinon
@@ -38,7 +38,7 @@ dependencies:
38
38
  - !ruby/object:Gem::Version
39
39
  version: '0'
40
40
  - !ruby/object:Gem::Dependency
41
- name: minitest
41
+ name: irb
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - ">="
@@ -52,7 +52,7 @@ dependencies:
52
52
  - !ruby/object:Gem::Version
53
53
  version: '0'
54
54
  - !ruby/object:Gem::Dependency
55
- name: pry
55
+ name: rake
56
56
  requirement: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - ">="
@@ -66,7 +66,35 @@ dependencies:
66
66
  - !ruby/object:Gem::Version
67
67
  version: '0'
68
68
  - !ruby/object:Gem::Dependency
69
- name: rake
69
+ name: rbs
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: rspec
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: rubocop
70
98
  requirement: !ruby/object:Gem::Requirement
71
99
  requirements:
72
100
  - - ">="
@@ -86,8 +114,10 @@ executables: []
86
114
  extensions: []
87
115
  extra_rdoc_files: []
88
116
  files:
89
- - ".DS_Store"
117
+ - ".github/workflows/ci.yml"
90
118
  - ".gitignore"
119
+ - ".rspec"
120
+ - ".rubocop.yml"
91
121
  - Gemfile
92
122
  - LICENSE.txt
93
123
  - README.md
@@ -97,12 +127,13 @@ files:
97
127
  - lib/yuuki.rb
98
128
  - lib/yuuki/caller.rb
99
129
  - lib/yuuki/error.rb
100
- - lib/yuuki/periodic.rb
130
+ - lib/yuuki/periodic_caller.rb
101
131
  - lib/yuuki/runner.rb
102
132
  - lib/yuuki/version.rb
133
+ - sig/yuuki.rbs
103
134
  - sig/yuuki/caller.rbs
104
135
  - sig/yuuki/error.rbs
105
- - sig/yuuki/periodic.rbs
136
+ - sig/yuuki/periodic_caller.rbs
106
137
  - sig/yuuki/runner.rbs
107
138
  - sig/yuuki/version.rbs
108
139
  - yuuki.gemspec
@@ -117,14 +148,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
117
148
  requirements:
118
149
  - - ">="
119
150
  - !ruby/object:Gem::Version
120
- version: '0'
151
+ version: '3.0'
121
152
  required_rubygems_version: !ruby/object:Gem::Requirement
122
153
  requirements:
123
154
  - - ">="
124
155
  - !ruby/object:Gem::Version
125
156
  version: '0'
126
157
  requirements: []
127
- rubygems_version: 3.6.2
158
+ rubygems_version: 4.0.10
128
159
  specification_version: 4
129
160
  summary: A caller / runner framework for Ruby
130
161
  test_files: []
data/.DS_Store DELETED
Binary file
@@ -1,80 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'English'
4
- require 'yuuki/caller'
5
- require 'yuuki/runner'
6
- require 'yoshinon'
7
-
8
- module Yuuki
9
- module Runner
10
- # sets interval to the method
11
- # @param [Array<Symbol>] methods method names
12
- # @param [Integer] interval
13
- def periodic(*methods, interval)
14
- @yuuki_methods ||= {}
15
- methods.each do |method|
16
- @yuuki_methods[method] ||= {}
17
- @yuuki_methods[method][:periodic] = interval
18
- end
19
- end
20
-
21
- # sets whether the method run at the first time
22
- # @param [Array<Symbol>] methods method names
23
- # @param [Boolean] enabled
24
- def first_run(*methods, enabled: true)
25
- @yuuki_methods ||= {}
26
- methods.each do |method|
27
- @yuuki_methods[method] ||= {}
28
- @yuuki_methods[method][:first_run] = enabled
29
- end
30
- end
31
- end
32
- end
33
-
34
- module Yuuki
35
- # @attr_reader [Boolean] first_run
36
- # @attr_reader [Float] current_time
37
- class PeriodicCaller < Caller
38
- attr_reader :first_run, :current_time
39
-
40
- def initialize(*instances, use_yoshinon: true)
41
- super
42
- @first_run = true
43
- end
44
-
45
- # sets error callback
46
- # @yield [error]
47
- # @yieldparam [Exception] error
48
- def on_error(&block)
49
- @on_error = block
50
- end
51
-
52
- # runs the periodic caller
53
- # @param [Numeric] gmtoff GMT Offset
54
- # @param [Object] args arguments
55
- def run(gmtoff = Time.now.gmtoff, **args, &)
56
- last_time = nil
57
- loop do
58
- @current_time = Time.now.to_f
59
- begin
60
- select_proc = proc do |_method, meta|
61
- next true if @first_run && meta[:first_run]
62
- next false unless meta[:periodic]
63
- next false unless last_time
64
-
65
- c = @current_time + gmtoff
66
- l = last_time + gmtoff
67
- (l.div(meta[:periodic]) + 1) * meta[:periodic] <= c
68
- end
69
- run_select(select_proc, **args, &)
70
- rescue StandardError
71
- @on_error ? @on_error[$ERROR_INFO] : raise
72
- end
73
- @first_run = false
74
-
75
- last_time = @current_time
76
- ((@current_time + 1).floor - Time.now.to_f).tap { |e| sleep e if e > 0 }
77
- end
78
- end
79
- end
80
- end
@@ -1,19 +0,0 @@
1
- # TypeProf 0.21.2
2
-
3
- # Classes
4
- module Yuuki
5
- module Runner
6
- def periodic: (Symbol method, Integer interval) -> void
7
- def first_run: (Symbol method, ?enabled: bool) -> void
8
- end
9
-
10
- class PeriodicCaller < Caller
11
- @on_error: Proc
12
-
13
- attr_reader first_run: bool
14
- attr_reader current_time: Float
15
- def initialize: (*untyped instances) -> void
16
- def on_error: {(Exception) -> void} -> void
17
- def run: (?Integer gmtoff, **untyped) -> void
18
- end
19
- end