automatic 14.12.2 → 26.08
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 +5 -5
- data/README.md +635 -83
- data/VERSION +1 -1
- data/automatic.gemspec +109 -248
- data/bin/automatic +20 -139
- data/config/feed2console.yml +10 -4
- data/config/feed2markdown.yml +41 -0
- data/doc/AI_TUTORIAL.md +518 -0
- data/doc/BASIC_DESIGN.md +516 -0
- data/doc/COPYING.LESSER +165 -0
- data/doc/DEPLOYMENT.md +824 -0
- data/doc/LICENSE.md +14 -0
- data/doc/PLUGINS.md +1875 -0
- data/doc/PLUGIN_DEVELOPMENT.md +86 -0
- data/doc/POLICY.md +857 -0
- data/doc/QUICKSTART.md +256 -0
- data/doc/RELEASING.md +381 -0
- data/doc/REQUIREMENTS.md +526 -0
- data/doc/VERSIONS +208 -0
- data/lib/automatic/cli.rb +248 -0
- data/lib/automatic/environment.rb +31 -5
- data/lib/automatic/feed_maker.rb +10 -9
- data/lib/automatic/feed_parser.rb +51 -35
- data/lib/automatic/http.rb +107 -0
- data/lib/automatic/log.rb +49 -18
- data/lib/automatic/opml.rb +3 -1
- data/lib/automatic/pipeline.rb +63 -32
- data/lib/automatic/recipe.rb +56 -17
- data/lib/automatic/version.rb +14 -1
- data/lib/automatic.rb +78 -20
- data/plugins/custom_feed/svn_log.rb +73 -32
- data/plugins/custom_feed/web.rb +348 -0
- data/plugins/filter/absolute_uri.rb +43 -27
- data/plugins/filter/accept.rb +38 -45
- data/plugins/filter/claude.rb +217 -0
- data/plugins/filter/clear.rb +12 -8
- data/plugins/filter/description_link.rb +49 -51
- data/plugins/filter/full_feed.rb +158 -52
- data/plugins/filter/gemini.rb +216 -0
- data/plugins/filter/github_feed.rb +38 -26
- data/plugins/filter/ignore.rb +33 -44
- data/plugins/filter/image.rb +36 -25
- data/plugins/filter/image_source.rb +58 -52
- data/plugins/filter/join.rb +107 -0
- data/plugins/filter/one.rb +19 -26
- data/plugins/filter/open_ai.rb +198 -0
- data/plugins/filter/rand.rb +16 -17
- data/plugins/filter/sakura_ai.rb +205 -0
- data/plugins/filter/sanitize.rb +29 -34
- data/plugins/filter/sort.rb +20 -27
- data/plugins/filter/tumblr_resize.rb +31 -23
- data/plugins/notify/ikachan.rb +86 -48
- data/plugins/provide/fluentd.rb +43 -24
- data/plugins/publish/amazon_s3.rb +73 -40
- data/plugins/publish/console.rb +19 -16
- data/plugins/publish/console_link.rb +20 -17
- data/plugins/publish/eject.rb +48 -26
- data/plugins/publish/fluentd.rb +50 -30
- data/plugins/publish/hatena_bookmark.rb +88 -71
- data/plugins/publish/instapaper.rb +69 -59
- data/plugins/publish/markdown.rb +278 -0
- data/plugins/publish/memcached.rb +35 -29
- data/plugins/store/database.rb +50 -48
- data/plugins/store/digest.rb +212 -0
- data/plugins/store/file.rb +99 -68
- data/plugins/store/full_text.rb +32 -25
- data/plugins/store/permalink.rb +18 -22
- data/plugins/subscription/feed.rb +34 -21
- data/plugins/subscription/link.rb +31 -32
- data/plugins/subscription/text.rb +32 -46
- data/plugins/subscription/tumblr.rb +55 -44
- data/plugins/subscription/xml.rb +40 -36
- metadata +108 -321
- data/Gemfile +0 -38
- data/Rakefile +0 -59
- data/doc/ChangeLog +0 -303
- data/doc/PLUGINS +0 -750
- data/doc/PLUGINS.ja +0 -753
- data/doc/README +0 -511
- data/doc/README.ja +0 -519
- data/plugins/filter/google_news.rb +0 -50
- data/plugins/publish/google_calendar.rb +0 -84
- data/plugins/publish/hipchat.rb +0 -46
- data/plugins/publish/pocket.rb +0 -45
- data/plugins/publish/twitter.rb +0 -58
- data/plugins/subscription/chan_toru.rb +0 -57
- data/plugins/subscription/g_guide.rb +0 -57
- data/plugins/subscription/pocket.rb +0 -51
- data/plugins/subscription/twitter.rb +0 -69
- data/plugins/subscription/twitter_search.rb +0 -50
- data/plugins/subscription/weather.rb +0 -33
- data/script/build +0 -84
- data/spec/fixtures/sampleFeeds.tsv +0 -1
- data/spec/fixtures/sampleFeeds2.tsv +0 -2
- data/spec/fixtures/sampleRecipe.yml +0 -24
- data/spec/lib/automatic/log_spec.rb +0 -32
- data/spec/lib/automatic/pipeline_spec.rb +0 -68
- data/spec/lib/automatic/recipe_spec.rb +0 -40
- data/spec/lib/automatic_spec.rb +0 -99
- data/spec/plugins/custom_feed/svn_log_spec.rb +0 -31
- data/spec/plugins/filter/absolute_uri_spec.rb +0 -61
- data/spec/plugins/filter/accept_spec.rb +0 -331
- data/spec/plugins/filter/clear_spec.rb +0 -49
- data/spec/plugins/filter/description_link_spec.rb +0 -138
- data/spec/plugins/filter/full_feed_spec.rb +0 -129
- data/spec/plugins/filter/github_feed_spec.rb +0 -55
- data/spec/plugins/filter/google_news_spec.rb +0 -69
- data/spec/plugins/filter/ignore_spec.rb +0 -328
- data/spec/plugins/filter/image_source_spec.rb +0 -89
- data/spec/plugins/filter/image_spec.rb +0 -65
- data/spec/plugins/filter/one_spec.rb +0 -71
- data/spec/plugins/filter/rand_spec.rb +0 -52
- data/spec/plugins/filter/sanitize_spec.rb +0 -153
- data/spec/plugins/filter/sort_spec.rb +0 -189
- data/spec/plugins/filter/tumblr_resize_spec.rb +0 -109
- data/spec/plugins/notify/ikachan_spec.rb +0 -58
- data/spec/plugins/provide/fluentd_spec.rb +0 -49
- data/spec/plugins/publish/amazon_s3_spec.rb +0 -40
- data/spec/plugins/publish/console_spec.rb +0 -30
- data/spec/plugins/publish/eject_spec.rb +0 -40
- data/spec/plugins/publish/fluentd_spec.rb +0 -40
- data/spec/plugins/publish/google_calendar_spec.rb +0 -83
- data/spec/plugins/publish/hatena_bookmark_spec.rb +0 -134
- data/spec/plugins/publish/hipchat_spec.rb +0 -69
- data/spec/plugins/publish/instapaper_spec.rb +0 -82
- data/spec/plugins/publish/memcached_spec.rb +0 -63
- data/spec/plugins/publish/pocket_spec.rb +0 -51
- data/spec/plugins/publish/twitter_spec.rb +0 -73
- data/spec/plugins/store/file_spec.rb +0 -58
- data/spec/plugins/store/full_text_spec.rb +0 -152
- data/spec/plugins/store/permalink_spec.rb +0 -206
- data/spec/plugins/subscription/chan_toru_spec.rb +0 -56
- data/spec/plugins/subscription/feed_spec.rb +0 -71
- data/spec/plugins/subscription/g_guide_spec.rb +0 -82
- data/spec/plugins/subscription/link_spec.rb +0 -72
- data/spec/plugins/subscription/pocket_spec.rb +0 -57
- data/spec/plugins/subscription/text_spec.rb +0 -84
- data/spec/plugins/subscription/tumblr_spec.rb +0 -74
- data/spec/plugins/subscription/twitter_search_spec.rb +0 -57
- data/spec/plugins/subscription/twitter_spec.rb +0 -73
- data/spec/plugins/subscription/weather_spec.rb +0 -44
- data/spec/plugins/subscription/xml_spec.rb +0 -84
- data/spec/spec_helper.rb +0 -106
- data/spec/user_dir/plugins/store/mock.rb +0 -16
- data/test/fixtures/sampleOPML.xml +0 -11
- data/test/integration/test_absoluteurl.yml +0 -25
- data/test/integration/test_activerecord.yml +0 -24
- data/test/integration/test_add_pocket.yml +0 -26
- data/test/integration/test_chan_toru.yml +0 -21
- data/test/integration/test_descriptionlink.yml +0 -21
- data/test/integration/test_fluentd.yml +0 -22
- data/test/integration/test_fulltext.yml +0 -30
- data/test/integration/test_google_news.yml +0 -21
- data/test/integration/test_googlealert.yml +0 -21
- data/test/integration/test_hatenabookmark.yml +0 -30
- data/test/integration/test_ignore.yml +0 -25
- data/test/integration/test_ignore2.yml +0 -22
- data/test/integration/test_image2local.yml +0 -33
- data/test/integration/test_instapaper.yml +0 -26
- data/test/integration/test_link2local.yml +0 -34
- data/test/integration/test_one.yml +0 -23
- data/test/integration/test_pocket.yml +0 -22
- data/test/integration/test_rand.yml +0 -21
- data/test/integration/test_sanitize.yml +0 -23
- data/test/integration/test_sort.yml +0 -36
- data/test/integration/test_svnlog.yml +0 -15
- data/test/integration/test_text2feed.yml +0 -36
- data/test/integration/test_tumblr2local.yml +0 -43
- data/test/integration/test_twitter_search.yml +0 -22
- data/test/integration/test_weather.yml +0 -19
- data/test/integration/test_xml2fluentd.yml +0 -21
- data/vendor/.gitkeep +0 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Name:: Automatic::Http
|
|
3
|
+
# Author: id774 (More info: http://id774.net)
|
|
4
|
+
# Source Code:: https://github.com/id774/automaticruby
|
|
5
|
+
# License:: The GPL version 3, or LGPL version 3 (Dual License).
|
|
6
|
+
# Contact:: idnanashi@gmail.com
|
|
7
|
+
# Created:: Aug 15, 2026
|
|
8
|
+
# Updated:: Aug 21, 2026
|
|
9
|
+
# Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
|
|
10
|
+
#
|
|
11
|
+
# One way in for everything the plugins fetch over HTTP.
|
|
12
|
+
#
|
|
13
|
+
# This exists because seven plugins were each calling `URI.open` on a string,
|
|
14
|
+
# and the four decisions that call implies -- which schemes are allowed, how
|
|
15
|
+
# long to wait, how many redirects to follow, what to send as a User-Agent --
|
|
16
|
+
# were being made seven times, mostly by omission. They are made once here.
|
|
17
|
+
# It is a helper, not a client framework: one method fetches, one builds a URI,
|
|
18
|
+
# and a plugin that wants something else still calls Ruby directly.
|
|
19
|
+
# See doc/POLICY.md section 9.2 and doc/PLUGINS.md section 3.8.
|
|
20
|
+
|
|
21
|
+
require 'open-uri'
|
|
22
|
+
require 'uri'
|
|
23
|
+
require 'automatic/version'
|
|
24
|
+
|
|
25
|
+
module Automatic
|
|
26
|
+
module Http
|
|
27
|
+
# A link in a pipeline item comes from a feed, which is to say from
|
|
28
|
+
# outside. `URI.open` on such a string will happily read `file:///etc/passwd`
|
|
29
|
+
# or run an FTP session; a plugin fetching an article body wants neither.
|
|
30
|
+
SCHEMES = %w[http https].freeze
|
|
31
|
+
|
|
32
|
+
OPEN_TIMEOUT = 10
|
|
33
|
+
READ_TIMEOUT = 30
|
|
34
|
+
|
|
35
|
+
# open-uri follows redirects itself and refuses an HTTPS-to-HTTP downgrade.
|
|
36
|
+
# This bounds the chain so that a redirect loop ends as an error rather
|
|
37
|
+
# than as an unattended run that never returns.
|
|
38
|
+
REDIRECT_LIMIT = 5
|
|
39
|
+
|
|
40
|
+
USER_AGENT = "Automatic Ruby/#{Automatic::VERSION} " \
|
|
41
|
+
'(+https://github.com/id774/automaticruby)'
|
|
42
|
+
|
|
43
|
+
# The RFC 2396 parser is named directly: `URI.escape` was removed in Ruby
|
|
44
|
+
# 3.0, and `URI::Parser` became the RFC 3986 parser in 3.4, which reports
|
|
45
|
+
# #escape as obsolete. This spelling means the same thing on every
|
|
46
|
+
# supported Ruby.
|
|
47
|
+
ESCAPER = URI::RFC2396_Parser.new
|
|
48
|
+
|
|
49
|
+
class << self
|
|
50
|
+
# Fetch a URL and return its body as a string. Raises rather than
|
|
51
|
+
# returning nil: a plugin's retry handling is built around an exception,
|
|
52
|
+
# and a body that could not be fetched is not an empty body.
|
|
53
|
+
def read(url)
|
|
54
|
+
open(url, &:read)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Fetch a URL and yield the IO, for a caller that would rather stream
|
|
58
|
+
# than hold the whole body.
|
|
59
|
+
def open(url, &block)
|
|
60
|
+
uri(url).open(
|
|
61
|
+
'User-Agent' => USER_AGENT,
|
|
62
|
+
open_timeout: OPEN_TIMEOUT,
|
|
63
|
+
read_timeout: READ_TIMEOUT,
|
|
64
|
+
redirect: true,
|
|
65
|
+
max_redirects: REDIRECT_LIMIT,
|
|
66
|
+
&block
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Parse a URL into a URI this framework will fetch, or raise. A string
|
|
71
|
+
# carrying characters a URI may not (a space, a Japanese query term) is
|
|
72
|
+
# escaped and parsed again, which is what the plugins used to do for
|
|
73
|
+
# themselves before every call.
|
|
74
|
+
def uri(url)
|
|
75
|
+
string = url.to_s.strip
|
|
76
|
+
raise ArgumentError, 'no URL to fetch' if string.empty?
|
|
77
|
+
|
|
78
|
+
parsed = parse(string)
|
|
79
|
+
unless SCHEMES.include?(parsed.scheme)
|
|
80
|
+
raise ArgumentError, "not an HTTP or HTTPS URL: #{string}"
|
|
81
|
+
end
|
|
82
|
+
unless parsed.host
|
|
83
|
+
raise ArgumentError, "HTTP or HTTPS URL has no host: #{string}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
parsed.normalize
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Whether a string is a URL this framework will fetch. For a plugin that
|
|
90
|
+
# skips an item rather than failing the run on one.
|
|
91
|
+
def fetchable?(url)
|
|
92
|
+
uri(url)
|
|
93
|
+
true
|
|
94
|
+
rescue ArgumentError, URI::InvalidURIError
|
|
95
|
+
false
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def parse(string)
|
|
101
|
+
URI.parse(string)
|
|
102
|
+
rescue URI::InvalidURIError
|
|
103
|
+
URI.parse(ESCAPER.escape(string))
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
data/lib/automatic/log.rb
CHANGED
|
@@ -1,31 +1,62 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
# Name::
|
|
3
|
-
# Author
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
2
|
+
# Name:: Automatic::Log
|
|
3
|
+
# Author: id774 (More info: http://id774.net)
|
|
4
|
+
# Source Code:: https://github.com/id774/automaticruby
|
|
5
|
+
# License:: The GPL version 3, or LGPL version 3 (Dual License).
|
|
6
|
+
# Contact:: idnanashi@gmail.com
|
|
7
|
+
# Created:: Feb 20, 2012
|
|
8
|
+
# Updated:: Aug 14, 2026
|
|
9
|
+
# Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
|
|
10
|
+
#
|
|
11
|
+
# One log, to standard output, behind a level filter set per Recipe by
|
|
12
|
+
# global.log.level. See doc/BASIC_DESIGN.md section 4.7.
|
|
8
13
|
|
|
9
14
|
require 'logger'
|
|
10
15
|
|
|
11
16
|
module Automatic
|
|
12
17
|
module Log
|
|
13
|
-
|
|
18
|
+
# In increasing order of severity. 'none' is the threshold that admits
|
|
19
|
+
# nothing; it is never used as the level of a message.
|
|
20
|
+
LOG_LEVELS = %w[info warn error none].freeze
|
|
14
21
|
|
|
15
|
-
|
|
16
|
-
@level = level
|
|
17
|
-
end
|
|
22
|
+
DEFAULT_LEVEL = 'info'
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
class << self
|
|
25
|
+
# Set the threshold. An unrecognised name, including nil, means the
|
|
26
|
+
# default: an unattended run should not be silenced by a typo in a
|
|
27
|
+
# Recipe, nor should it raise in the middle of a job.
|
|
28
|
+
def level(level = nil)
|
|
29
|
+
@level = normalize(level, DEFAULT_LEVEL)
|
|
30
|
+
end
|
|
22
31
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return
|
|
32
|
+
def logger
|
|
33
|
+
@logger ||= Logger.new($stdout)
|
|
26
34
|
end
|
|
27
|
-
logger.send(level, message)
|
|
28
|
-
end
|
|
29
35
|
|
|
36
|
+
# Replace the logger. Used by the specs; a plugin has no business
|
|
37
|
+
# calling this.
|
|
38
|
+
attr_writer :logger
|
|
39
|
+
|
|
40
|
+
# Emit a message at the given level, if the threshold admits it.
|
|
41
|
+
# Both 'info' and :info are accepted, because plugins pass both.
|
|
42
|
+
def puts(level, message)
|
|
43
|
+
name = normalize(level, DEFAULT_LEVEL)
|
|
44
|
+
return if name == 'none'
|
|
45
|
+
return if LOG_LEVELS.index(name) < LOG_LEVELS.index(current_level)
|
|
46
|
+
|
|
47
|
+
logger.public_send(name, message)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def current_level
|
|
53
|
+
@level ||= DEFAULT_LEVEL
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def normalize(level, fallback)
|
|
57
|
+
name = level.to_s
|
|
58
|
+
LOG_LEVELS.include?(name) ? name : fallback
|
|
59
|
+
end
|
|
60
|
+
end
|
|
30
61
|
end
|
|
31
62
|
end
|
data/lib/automatic/opml.rb
CHANGED
|
@@ -220,7 +220,9 @@ module Automatic
|
|
|
220
220
|
end # each_outline
|
|
221
221
|
|
|
222
222
|
def read_text
|
|
223
|
-
|
|
223
|
+
# +'' rather than "": this string is appended to below, and a literal
|
|
224
|
+
# is on its way to being frozen.
|
|
225
|
+
text = +''
|
|
224
226
|
while event = @p.pull
|
|
225
227
|
case event.event_type
|
|
226
228
|
when :end_element
|
data/lib/automatic/pipeline.rb
CHANGED
|
@@ -1,43 +1,74 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
# Name::
|
|
3
|
-
# Author
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
2
|
+
# Name:: Automatic::Pipeline
|
|
3
|
+
# Author: id774 (More info: http://id774.net)
|
|
4
|
+
# Source Code:: https://github.com/id774/automaticruby
|
|
5
|
+
# License:: The GPL version 3, or LGPL version 3 (Dual License).
|
|
6
|
+
# Contact:: idnanashi@gmail.com
|
|
7
|
+
# Created:: Feb 22, 2012
|
|
8
|
+
# Updated:: Aug 14, 2026
|
|
9
|
+
# Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
|
|
10
|
+
#
|
|
11
|
+
# The core: resolve a plugin's class name to a file, then run the Recipe's
|
|
12
|
+
# plugins in order, each receiving the previous one's output. See
|
|
13
|
+
# doc/BASIC_DESIGN.md section 4.6 and doc/PLUGINS.md section 3.
|
|
8
14
|
|
|
9
|
-
require 'active_support'
|
|
10
|
-
require 'active_support/core_ext'
|
|
11
|
-
require 'active_support/deprecation'
|
|
15
|
+
require 'active_support/core_ext/string/inflections'
|
|
12
16
|
|
|
13
17
|
module Automatic
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
# The namespace every plugin class lives in. Plugin files are registered
|
|
19
|
+
# here with autoload, so a file is read when its constant is first used.
|
|
20
|
+
module Plugin; end
|
|
17
21
|
|
|
18
22
|
module Pipeline
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
class << self
|
|
24
|
+
# Resolve a module name such as SubscriptionFeed to
|
|
25
|
+
# <search root>/subscription/feed.rb and register it for autoloading.
|
|
26
|
+
#
|
|
27
|
+
# The user directory is searched before the installation, so a plugin in
|
|
28
|
+
# ~/.automatic/plugins shadows a shipped plugin of the same name.
|
|
29
|
+
def load_plugin(module_name)
|
|
30
|
+
underscored = module_name.underscore
|
|
31
|
+
|
|
32
|
+
search_roots.each do |dir|
|
|
33
|
+
category = File.basename(dir)
|
|
34
|
+
next unless /\A#{Regexp.escape(category)}_(.*)\z/ =~ underscored
|
|
35
|
+
|
|
36
|
+
path = File.join(dir, "#{Regexp.last_match(1)}.rb")
|
|
37
|
+
next unless File.exist?(path)
|
|
38
|
+
|
|
39
|
+
return Automatic::Plugin.autoload(module_name.to_sym, path)
|
|
27
40
|
end
|
|
28
|
-
}
|
|
29
|
-
raise NoPluginError, "unknown plugin named #{module_name}"
|
|
30
|
-
end
|
|
31
41
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
raise NoPluginError, "unknown plugin named #{module_name}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Run a Recipe: each plugin's return value is the next plugin's input.
|
|
46
|
+
#
|
|
47
|
+
# Nothing here rescues a plugin's exception. A Recipe is a sequence in
|
|
48
|
+
# which each step consumes the previous one's output, so continuing past
|
|
49
|
+
# a failed step would run the rest on a value their author never
|
|
50
|
+
# intended. See doc/REQUIREMENTS.md section 12.
|
|
51
|
+
def run(recipe)
|
|
52
|
+
raise NoRecipeError, 'no recipe given' if recipe.nil?
|
|
53
|
+
|
|
54
|
+
pipeline = []
|
|
55
|
+
recipe.each_plugin do |plugin|
|
|
56
|
+
mod = plugin.module
|
|
57
|
+
load_plugin(mod)
|
|
58
|
+
klass = Automatic::Plugin.const_get(mod)
|
|
59
|
+
pipeline = klass.new(plugin.config, pipeline).run
|
|
60
|
+
end
|
|
61
|
+
pipeline
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def search_roots
|
|
67
|
+
Dir[File.join(Automatic.user_plugins_dir, '*'),
|
|
68
|
+
File.join(Automatic.plugins_dir, '*')].select { |path|
|
|
69
|
+
File.directory?(path)
|
|
70
|
+
}
|
|
71
|
+
end
|
|
41
72
|
end
|
|
42
73
|
end
|
|
43
74
|
end
|
data/lib/automatic/recipe.rb
CHANGED
|
@@ -1,37 +1,76 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
# Name::
|
|
3
|
-
# Author
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
2
|
+
# Name:: Automatic::Recipe
|
|
3
|
+
# Author: ainame
|
|
4
|
+
# Source Code:: https://github.com/id774/automaticruby
|
|
5
|
+
# License:: The GPL version 3, or LGPL version 3 (Dual License).
|
|
6
|
+
# Contact:: idnanashi@gmail.com
|
|
7
|
+
# Created:: Feb 18, 2012
|
|
8
|
+
# Updated:: Aug 14, 2026
|
|
9
|
+
# Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
|
|
10
|
+
#
|
|
11
|
+
# Turns a Recipe file into something the pipeline can iterate. The format is
|
|
12
|
+
# specified in doc/PLUGINS.md section 2.
|
|
9
13
|
|
|
14
|
+
require 'date'
|
|
10
15
|
require 'hashie'
|
|
11
16
|
require 'yaml'
|
|
12
17
|
|
|
13
18
|
module Automatic
|
|
14
19
|
class Recipe
|
|
20
|
+
# A Recipe is trusted local configuration, but it is still loaded safely:
|
|
21
|
+
# the document may name none of its own Ruby classes to instantiate. That
|
|
22
|
+
# costs nothing, since no Recipe needs it. It is a second line of defence
|
|
23
|
+
# and not the trust boundary; see doc/REQUIREMENTS.md section 17.
|
|
24
|
+
PERMITTED_CLASSES = [Date, Time, Symbol].freeze
|
|
25
|
+
|
|
15
26
|
attr_reader :procedure
|
|
16
27
|
|
|
17
|
-
def initialize(path =
|
|
28
|
+
def initialize(path = '')
|
|
18
29
|
load_recipe(path)
|
|
19
30
|
end
|
|
20
31
|
|
|
21
32
|
def load_recipe(path)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
resolved = resolve_path(path)
|
|
34
|
+
document = parse(resolved)
|
|
35
|
+
|
|
36
|
+
unless document.is_a?(Hash)
|
|
37
|
+
raise InvalidRecipeError,
|
|
38
|
+
"recipe #{resolved} is not a mapping"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
@procedure = Hashie::Mash.new(document)
|
|
42
|
+
|
|
43
|
+
unless @procedure.plugins.is_a?(Array)
|
|
44
|
+
raise InvalidRecipeError,
|
|
45
|
+
"recipe #{resolved} has no plugins sequence"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
Automatic::Log.level(@procedure.global&.log&.level)
|
|
49
|
+
Automatic::Log.puts('info', "Loading Recipe: #{resolved}")
|
|
28
50
|
@procedure
|
|
29
51
|
end
|
|
30
52
|
|
|
31
53
|
def each_plugin
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
54
|
+
return enum_for(:each_plugin) unless block_given?
|
|
55
|
+
|
|
56
|
+
@procedure.plugins.each { |plugin| yield plugin }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# A bare filename is looked for in the user's config directory first;
|
|
62
|
+
# anything not found there is used as a path exactly as given.
|
|
63
|
+
def resolve_path(path)
|
|
64
|
+
in_user_dir = File.join(Automatic.user_config_dir, path.to_s)
|
|
65
|
+
File.exist?(in_user_dir) ? in_user_dir : path.to_s
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def parse(path)
|
|
69
|
+
YAML.safe_load(
|
|
70
|
+
File.read(path),
|
|
71
|
+
permitted_classes: PERMITTED_CLASSES,
|
|
72
|
+
aliases: true
|
|
73
|
+
)
|
|
35
74
|
end
|
|
36
75
|
end
|
|
37
76
|
end
|
data/lib/automatic/version.rb
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Name:: Automatic::VERSION
|
|
3
|
+
# Author: id774 (More info: http://id774.net)
|
|
4
|
+
# Source Code:: https://github.com/id774/automaticruby
|
|
5
|
+
# License:: The GPL version 3, or LGPL version 3 (Dual License).
|
|
6
|
+
# Contact:: idnanashi@gmail.com
|
|
7
|
+
# Created:: Feb 18, 2012
|
|
8
|
+
# Updated:: Aug 14, 2026
|
|
9
|
+
# Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
|
|
10
|
+
|
|
1
11
|
module Automatic
|
|
2
|
-
VERSION
|
|
12
|
+
# The release version. The VERSION file at the repository root carries the
|
|
13
|
+
# same number and the gemspec reads it from there; a spec asserts that the
|
|
14
|
+
# two agree. See doc/POLICY.md section 10.
|
|
15
|
+
VERSION = '26.08'
|
|
3
16
|
end
|
data/lib/automatic.rb
CHANGED
|
@@ -1,54 +1,112 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
# Name::
|
|
3
|
-
# Author
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
2
|
+
# Name:: Automatic::Ruby
|
|
3
|
+
# Author: id774 (More info: http://id774.net)
|
|
4
|
+
# Source Code:: https://github.com/id774/automaticruby
|
|
5
|
+
# License:: The GPL version 3, or LGPL version 3 (Dual License).
|
|
6
|
+
# Contact:: idnanashi@gmail.com
|
|
7
|
+
# Created:: Feb 18, 2012
|
|
8
|
+
# Updated:: Aug 14, 2026
|
|
9
|
+
# Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
|
|
10
|
+
#
|
|
11
|
+
# The framework module: the two directories everything else resolves paths
|
|
12
|
+
# against, and the one method that runs a Recipe. See doc/BASIC_DESIGN.md
|
|
13
|
+
# section 4.3.
|
|
8
14
|
|
|
9
15
|
module Automatic
|
|
10
16
|
require 'automatic/environment'
|
|
17
|
+
require 'automatic/log'
|
|
11
18
|
require 'automatic/recipe'
|
|
12
19
|
require 'automatic/pipeline'
|
|
13
|
-
require 'automatic/
|
|
20
|
+
require 'automatic/http'
|
|
14
21
|
require 'automatic/feed_parser'
|
|
15
22
|
require 'automatic/feed_maker'
|
|
16
23
|
require 'automatic/version'
|
|
17
24
|
|
|
18
|
-
|
|
25
|
+
# The user directory, relative to the home directory.
|
|
26
|
+
USER_DIR = '/.automatic'
|
|
27
|
+
|
|
28
|
+
# Base class for every failure the framework raises itself, so that a caller
|
|
29
|
+
# can rescue these without rescuing everything.
|
|
30
|
+
class Error < StandardError; end
|
|
31
|
+
|
|
32
|
+
# Pipeline.run was given no Recipe.
|
|
33
|
+
class NoRecipeError < Error; end
|
|
34
|
+
|
|
35
|
+
# A Recipe named a module for which no plugin file could be found.
|
|
36
|
+
class NoPluginError < Error; end
|
|
37
|
+
|
|
38
|
+
# A Recipe parsed, but is not a document this framework can run.
|
|
39
|
+
class InvalidRecipeError < Error; end
|
|
19
40
|
|
|
20
41
|
class << self
|
|
21
42
|
attr_accessor :root_dir
|
|
22
43
|
|
|
23
|
-
|
|
44
|
+
# Require a gem that only one plugin, or one optional path, needs, and turn
|
|
45
|
+
# its absence into a message naming the gem, what wanted it and how to get
|
|
46
|
+
# it. Nothing here resolves, installs or tracks a dependency: the `require`
|
|
47
|
+
# is the plugin's own, made where the plugin makes it, and this only
|
|
48
|
+
# replaces `cannot load such file -- dalli` with a sentence an operator can
|
|
49
|
+
# act on. See doc/POLICY.md section 9.1 and doc/PLUGINS.md section 3.8.
|
|
50
|
+
#
|
|
51
|
+
# Automatic.require_optional('sanitize', needed_by: 'FilterSanitize')
|
|
52
|
+
#
|
|
53
|
+
# `gem_name` is given where it differs from the path required, as
|
|
54
|
+
# `active_record` does from the `activerecord` gem.
|
|
55
|
+
def require_optional(feature, needed_by:, gem_name: feature)
|
|
56
|
+
require feature
|
|
57
|
+
rescue LoadError => e
|
|
58
|
+
raise LoadError,
|
|
59
|
+
"The `#{gem_name}` gem is not installed. It is needed by #{needed_by}. " \
|
|
60
|
+
"Install it with `gem install #{gem_name}`, or in a source checkout add " \
|
|
61
|
+
'its group to the bundle; see the optional plugin dependencies in ' \
|
|
62
|
+
"doc/DEPLOYMENT.md. (#{e.message})"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Run one Recipe. root_dir is the installation root; user_dir is honoured
|
|
66
|
+
# only under AUTOMATIC_RUBY_ENV=test, which is how the specs point the
|
|
67
|
+
# plugin loader at a fixture directory.
|
|
68
|
+
def run(args = {})
|
|
24
69
|
self.root_dir = args[:root_dir]
|
|
25
70
|
self.user_dir = args[:user_dir]
|
|
26
71
|
Automatic::Pipeline.run(args[:recipe])
|
|
27
72
|
end
|
|
28
73
|
|
|
29
74
|
def plugins_dir
|
|
30
|
-
File.join(@root_dir,
|
|
75
|
+
File.join(@root_dir.to_s, 'plugins')
|
|
31
76
|
end
|
|
32
77
|
|
|
33
78
|
def config_dir
|
|
34
|
-
File.join(@root_dir,
|
|
79
|
+
File.join(@root_dir.to_s, 'config')
|
|
35
80
|
end
|
|
36
81
|
|
|
82
|
+
# Defaults to ~/.automatic when nothing has set it, so that a caller which
|
|
83
|
+
# never went through .run still resolves the user directory.
|
|
37
84
|
def user_dir
|
|
38
|
-
@user_dir
|
|
85
|
+
@user_dir ||= default_user_dir
|
|
39
86
|
end
|
|
40
87
|
|
|
41
|
-
def user_dir=(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
88
|
+
def user_dir=(dir)
|
|
89
|
+
@user_dir =
|
|
90
|
+
if ENV['AUTOMATIC_RUBY_ENV'] == 'test' && !dir.nil?
|
|
91
|
+
dir
|
|
92
|
+
else
|
|
93
|
+
default_user_dir
|
|
94
|
+
end
|
|
47
95
|
end
|
|
48
96
|
|
|
49
97
|
def user_plugins_dir
|
|
50
|
-
File.join(
|
|
98
|
+
File.join(user_dir.to_s, 'plugins')
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Where a Recipe named by a bare filename is looked for.
|
|
102
|
+
def user_config_dir
|
|
103
|
+
File.join(user_dir.to_s, 'config')
|
|
51
104
|
end
|
|
52
|
-
end
|
|
53
105
|
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def default_user_dir
|
|
109
|
+
File.expand_path('~') + USER_DIR
|
|
110
|
+
end
|
|
111
|
+
end
|
|
54
112
|
end
|