site_maps 0.0.1.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/main.yml +45 -0
- data/.gitignore +16 -0
- data/.rspec +1 -0
- data/.rubocop.yml +36 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +8 -0
- data/Gemfile +18 -0
- data/Gemfile.lock +134 -0
- data/LICENSE.txt +21 -0
- data/README.md +186 -0
- data/Rakefile +4 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/exec/site_maps +9 -0
- data/lib/site-maps.rb +3 -0
- data/lib/site_maps/adapters/adapter.rb +80 -0
- data/lib/site_maps/adapters/aws_sdk/config.rb +51 -0
- data/lib/site_maps/adapters/aws_sdk/location.rb +9 -0
- data/lib/site_maps/adapters/aws_sdk/storage.rb +52 -0
- data/lib/site_maps/adapters/aws_sdk.rb +31 -0
- data/lib/site_maps/adapters/file_system/config.rb +5 -0
- data/lib/site_maps/adapters/file_system/location.rb +35 -0
- data/lib/site_maps/adapters/file_system/storage.rb +61 -0
- data/lib/site_maps/adapters/file_system.rb +26 -0
- data/lib/site_maps/adapters/noop.rb +18 -0
- data/lib/site_maps/atomic_repository.rb +24 -0
- data/lib/site_maps/builder/link.rb +27 -0
- data/lib/site_maps/builder/normalizer.rb +48 -0
- data/lib/site_maps/builder/sitemap_index/item.rb +35 -0
- data/lib/site_maps/builder/sitemap_index.rb +40 -0
- data/lib/site_maps/builder/url.rb +152 -0
- data/lib/site_maps/builder/url_set.rb +92 -0
- data/lib/site_maps/cli.rb +68 -0
- data/lib/site_maps/configuration.rb +119 -0
- data/lib/site_maps/incremental_location.rb +62 -0
- data/lib/site_maps/notification/bus.rb +90 -0
- data/lib/site_maps/notification/event.rb +50 -0
- data/lib/site_maps/notification/publisher.rb +78 -0
- data/lib/site_maps/notification.rb +36 -0
- data/lib/site_maps/primitives/array.rb +15 -0
- data/lib/site_maps/primitives/output.rb +66 -0
- data/lib/site_maps/primitives/string.rb +43 -0
- data/lib/site_maps/process.rb +29 -0
- data/lib/site_maps/railtie.rb +18 -0
- data/lib/site_maps/runner/event_listener.rb +78 -0
- data/lib/site_maps/runner.rb +136 -0
- data/lib/site_maps/sitemap_builder.rb +75 -0
- data/lib/site_maps/sitemap_reader.rb +56 -0
- data/lib/site_maps/version.rb +5 -0
- data/lib/site_maps.rb +112 -0
- data/site_maps.gemspec +44 -0
- metadata +172 -0
@@ -0,0 +1,68 @@
|
|
1
|
+
require "thor"
|
2
|
+
|
3
|
+
module SiteMaps
|
4
|
+
class CLI < Thor
|
5
|
+
method_option :debug, type: :boolean, default: false
|
6
|
+
method_option :logfile, type: :string, default: nil
|
7
|
+
method_option :pidfile, type: :string, default: nil
|
8
|
+
method_option :config_file, type: :string, aliases: "-r", default: nil
|
9
|
+
method_option :max_threads, type: :numeric, aliases: "-c", default: 4
|
10
|
+
method_option :context, type: :hash, default: {}
|
11
|
+
method_option :enqueue_remaining, type: :boolean, default: false
|
12
|
+
|
13
|
+
desc "generate 1st_process,2nd_process ... ,Nth_process", "Generate sitemap.xml files for the given processes"
|
14
|
+
default_command :start
|
15
|
+
|
16
|
+
def generate(processes = "")
|
17
|
+
load_rails if rails_app?
|
18
|
+
|
19
|
+
opts = (@options || {}).transform_keys(&:to_sym)
|
20
|
+
if (logfile = opts[:logfile])
|
21
|
+
SiteMaps.logger = Logger.new(logfile)
|
22
|
+
end
|
23
|
+
if opts[:debug]
|
24
|
+
SiteMaps.logger.level = Logger::DEBUG
|
25
|
+
end
|
26
|
+
|
27
|
+
SiteMaps::Notification.subscribe(SiteMaps::Runner::EventListener)
|
28
|
+
|
29
|
+
runner = SiteMaps.generate(
|
30
|
+
config_file: opts[:config_file],
|
31
|
+
max_threads: opts[:max_threads]
|
32
|
+
)
|
33
|
+
if processes.empty?
|
34
|
+
runner.enqueue_all
|
35
|
+
else
|
36
|
+
kwargs = (opts[:context] || {}).transform_keys(&:to_sym)
|
37
|
+
processes.split(",").each do |process|
|
38
|
+
runner.enqueue(process.strip.to_sym, **kwargs)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
if opts[:enqueue_remaining]
|
42
|
+
runner.enqueue_remaining
|
43
|
+
end
|
44
|
+
|
45
|
+
runner.run
|
46
|
+
end
|
47
|
+
|
48
|
+
desc "version", "Print the version"
|
49
|
+
def version
|
50
|
+
puts "SiteMaps version: #{SiteMaps::VERSION}"
|
51
|
+
end
|
52
|
+
|
53
|
+
default_task :help
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def rails_app?
|
58
|
+
File.exist?(File.join(Dir.pwd, "config", "application.rb"))
|
59
|
+
end
|
60
|
+
|
61
|
+
def load_rails
|
62
|
+
require File.expand_path(File.join(Dir.pwd, "config", "application.rb"))
|
63
|
+
require_relative "railtie"
|
64
|
+
|
65
|
+
::Rails.application.require_environment!
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SiteMaps
|
4
|
+
class Configuration
|
5
|
+
class << self
|
6
|
+
def attributes
|
7
|
+
@attributes || {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def attribute(name, default: nil)
|
11
|
+
@attributes ||= {}
|
12
|
+
@attributes[name] = default
|
13
|
+
|
14
|
+
unless method_defined?(name)
|
15
|
+
define_method(name) do
|
16
|
+
instance_variable_get(:"@#{name}")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
unless method_defined?(:"#{name}=")
|
21
|
+
define_method(:"#{name}=") do |value|
|
22
|
+
instance_variable_set(:"@#{name}", value)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
unless method_defined?(:"#{name}?")
|
27
|
+
define_method(:"#{name}?") do
|
28
|
+
!!send(name)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def inherited(subclass)
|
34
|
+
subclass.instance_variable_set(:@attributes, attributes.dup)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
attribute :url
|
39
|
+
attribute :directory, default: "/tmp/sitemaps"
|
40
|
+
|
41
|
+
def initialize(**options)
|
42
|
+
default_attributes.merge(options).each do |key, value|
|
43
|
+
send(:"#{key}=", value)
|
44
|
+
rescue NoMethodError
|
45
|
+
raise ConfigurationError, <<~ERROR
|
46
|
+
Unknown configuration option: #{key}
|
47
|
+
ERROR
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def becomes(klass, **options)
|
52
|
+
klass.new(**to_h, **options)
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_h
|
56
|
+
instance_variables.each_with_object({}) do |var, hash|
|
57
|
+
hash[var.to_s.delete("@").to_sym] = instance_variable_get(var)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def url
|
62
|
+
@url || validate_url!
|
63
|
+
end
|
64
|
+
|
65
|
+
def base_uri
|
66
|
+
::URI.parse(url).tap do |uri|
|
67
|
+
uri.path = ""
|
68
|
+
uri.query = nil
|
69
|
+
uri.fragment = nil
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def local_sitemap_path
|
74
|
+
filename = ::File.basename(url)
|
75
|
+
Pathname.new(directory).join(filename)
|
76
|
+
end
|
77
|
+
|
78
|
+
def fetch_sitemap_index_links
|
79
|
+
doc = SiteMaps::SitemapReader.new(local_sitemap_path.exist? ? local_sitemap_path : url).to_doc
|
80
|
+
|
81
|
+
doc.css("sitemapindex sitemap").map do |url|
|
82
|
+
SiteMaps::Builder::SitemapIndex::Item.new(
|
83
|
+
url.at_css("loc").text,
|
84
|
+
url.at_css("lastmod")&.text
|
85
|
+
)
|
86
|
+
end
|
87
|
+
rescue SiteMaps::SitemapReader::Error
|
88
|
+
[]
|
89
|
+
end
|
90
|
+
|
91
|
+
def remote_sitemap_directory
|
92
|
+
path = ::URI.parse(url).path
|
93
|
+
path = path[1..-1] if path.start_with?("/")
|
94
|
+
path.split("/")[0..-2].join("/")
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def validate_url!
|
100
|
+
return if @url
|
101
|
+
|
102
|
+
raise ConfigurationError, <<~ERROR
|
103
|
+
You must set a sitemap URL in your configuration to use the add method.
|
104
|
+
|
105
|
+
Example:
|
106
|
+
SiteMaps.configure do |config|
|
107
|
+
config.url = "https://example.com/sitemap.xml"
|
108
|
+
end
|
109
|
+
ERROR
|
110
|
+
end
|
111
|
+
|
112
|
+
def default_attributes
|
113
|
+
self.class.attributes.each_with_object({}) do |(key, default), hash|
|
114
|
+
value = default.respond_to?(:call) ? default.call : default
|
115
|
+
hash[key] = value unless value.nil?
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SiteMaps
|
4
|
+
class IncrementalLocation
|
5
|
+
FILENAME = "sitemap.xml"
|
6
|
+
PLACEHOLDER = "%{index}"
|
7
|
+
|
8
|
+
def initialize(main_url, process_location)
|
9
|
+
@main_uri = URI(main_url)
|
10
|
+
@index = Concurrent::AtomicFixnum.new(0)
|
11
|
+
normalize(process_location || @main_uri.to_s)
|
12
|
+
end
|
13
|
+
|
14
|
+
def url
|
15
|
+
placeholder_url % {index: @index.value}
|
16
|
+
end
|
17
|
+
|
18
|
+
def next
|
19
|
+
@index.increment
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def main_url
|
24
|
+
main_uri.to_s
|
25
|
+
end
|
26
|
+
|
27
|
+
def relative_directory
|
28
|
+
File.dirname(@uri.path).sub(%r{^/}, "")
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_reader :main_uri, :placeholder_url
|
34
|
+
|
35
|
+
def base_url
|
36
|
+
main_uri.dup.tap { |uri| uri.path = "" }
|
37
|
+
end
|
38
|
+
|
39
|
+
def base_dir
|
40
|
+
File.dirname(main_uri.path)
|
41
|
+
end
|
42
|
+
|
43
|
+
def normalize(loc)
|
44
|
+
uri = if %r{^https?://}.match?(loc)
|
45
|
+
URI(loc)
|
46
|
+
elsif loc.start_with?("/")
|
47
|
+
main_uri.dup.tap { |uri| uri.path = loc }
|
48
|
+
else
|
49
|
+
main_uri.dup.tap { |uri| uri.path = File.join(base_dir, loc) }
|
50
|
+
end
|
51
|
+
unless %w[.xml .xml.gz].include?(File.extname(uri.path))
|
52
|
+
uri.path = File.join(uri.path, FILENAME)
|
53
|
+
end
|
54
|
+
base = uri.dup.tap { |v| v.path = "" }.to_s
|
55
|
+
basename = File.basename(uri.path)
|
56
|
+
index_basename = basename.sub(/[\.](xml|xml\.gz)$/, "#{PLACEHOLDER}.\\1")
|
57
|
+
|
58
|
+
@placeholder_url = File.join(base, File.join(File.dirname(uri.path), index_basename))
|
59
|
+
@uri = URI(File.join(base, File.join(File.dirname(uri.path), basename)))
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SiteMaps
|
4
|
+
module Notification
|
5
|
+
class Bus
|
6
|
+
attr_reader :listeners, :events
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@listeners = Concurrent::Hash.new { |h, k| h[k] = Concurrent::Array.new }
|
10
|
+
@events = Concurrent::Hash.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def publish(event_id, payload)
|
14
|
+
raise UnregisteredEventError, event_id unless can_handle?(event_id)
|
15
|
+
|
16
|
+
process(event_id, payload) do |event, listener|
|
17
|
+
# Concurrent::Future.execute { listener.call(event) }
|
18
|
+
listener.call(event)
|
19
|
+
end
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def attach(listener)
|
24
|
+
events.each do |id, event|
|
25
|
+
method_name = event.listener_method
|
26
|
+
next unless listener.respond_to?(method_name)
|
27
|
+
|
28
|
+
listeners[id] << listener.method(method_name)
|
29
|
+
end
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
def unsubscribe(listener)
|
34
|
+
listeners.each do |id, arr|
|
35
|
+
arr.each do |func|
|
36
|
+
listeners[id].delete(func) if func.receiver == listener
|
37
|
+
end
|
38
|
+
end
|
39
|
+
self
|
40
|
+
end
|
41
|
+
alias_method :detach, :unsubscribe
|
42
|
+
|
43
|
+
def subscribe(object_or_event_id, &block)
|
44
|
+
raise(InvalidSubscriberError, object_or_event_id) unless can_handle?(object_or_event_id)
|
45
|
+
|
46
|
+
if block
|
47
|
+
listeners[object_or_event_id] << block
|
48
|
+
else
|
49
|
+
attach(object_or_event_id)
|
50
|
+
end
|
51
|
+
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
# rubocop:disable Performance/RedundantEqualityComparisonBlock
|
56
|
+
def subscribed?(listener)
|
57
|
+
listeners.values.any? { |value| value.any? { |func| func == listener } } ||
|
58
|
+
(
|
59
|
+
methods = events.values.map(&:listener_method)
|
60
|
+
.select { |method_name| listener.respond_to?(method_name) }
|
61
|
+
.map { |method_name| listener.method(method_name) }
|
62
|
+
methods && listeners.values.any? { |value| (methods & value).size > 0 }
|
63
|
+
)
|
64
|
+
end
|
65
|
+
# rubocop:enable Performance/RedundantEqualityComparisonBlock
|
66
|
+
|
67
|
+
def can_handle?(object_or_event_id)
|
68
|
+
case object_or_event_id
|
69
|
+
when String, Symbol
|
70
|
+
events.key?(object_or_event_id)
|
71
|
+
else
|
72
|
+
events
|
73
|
+
.values
|
74
|
+
.map(&:listener_method)
|
75
|
+
.any? { |method_name| object_or_event_id.respond_to?(method_name) }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
protected
|
80
|
+
|
81
|
+
def process(event_id, payload)
|
82
|
+
listeners[event_id].each do |listener|
|
83
|
+
event = events[event_id].payload(payload)
|
84
|
+
|
85
|
+
yield(event, listener)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SiteMaps
|
4
|
+
module Notification
|
5
|
+
class Event
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
def_delegators :@payload, :[], :fetch, :to_h, :key?
|
9
|
+
alias_method :to_hash, :to_h
|
10
|
+
|
11
|
+
attr_reader :id
|
12
|
+
|
13
|
+
# Initialize a new event
|
14
|
+
#
|
15
|
+
# @param [Symbol, String] id The event identifier
|
16
|
+
# @param [Hash] payload
|
17
|
+
#
|
18
|
+
# @return [Event]
|
19
|
+
#
|
20
|
+
# @api private
|
21
|
+
def initialize(id, payload = {})
|
22
|
+
@id = id
|
23
|
+
@payload = payload
|
24
|
+
end
|
25
|
+
|
26
|
+
# Get or set a payload
|
27
|
+
#
|
28
|
+
# @overload
|
29
|
+
# @return [Hash] payload
|
30
|
+
#
|
31
|
+
# @overload payload(data)
|
32
|
+
# @param [Hash] data A new payload
|
33
|
+
# @return [Event] A copy of the event with the provided payload
|
34
|
+
#
|
35
|
+
# @api public
|
36
|
+
def payload(data = nil)
|
37
|
+
if data
|
38
|
+
self.class.new(id, @payload.merge(data))
|
39
|
+
else
|
40
|
+
@payload
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# @api private
|
45
|
+
def listener_method
|
46
|
+
@listener_method ||= Primitives::String.new("on_#{id}").underscore.to_sym
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SiteMaps::Notification
|
4
|
+
module Publisher
|
5
|
+
def self.included(klass)
|
6
|
+
klass.extend(ClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
# Class interface for publishers
|
10
|
+
#
|
11
|
+
# @api public
|
12
|
+
module ClassMethods
|
13
|
+
extend Forwardable
|
14
|
+
def_delegators :bus, :publish, :subscribed?, :unsubscribe
|
15
|
+
|
16
|
+
# Register a new event type
|
17
|
+
#
|
18
|
+
# @param [Symbol,String] event_id The event identifier
|
19
|
+
# @param [Hash] payload Optional default payload
|
20
|
+
#
|
21
|
+
# @return [self]
|
22
|
+
#
|
23
|
+
# @api public
|
24
|
+
def register_event(event_id, payload = {})
|
25
|
+
bus.events[event_id] = Event.new(event_id, payload)
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
# Publish an event with extra runtime information to the payload
|
30
|
+
#
|
31
|
+
# @param [String] event_id The event identifier
|
32
|
+
# @param [Hash] payload An optional payload
|
33
|
+
# @raise [UnregisteredEventError] if the event is not registered
|
34
|
+
#
|
35
|
+
# @api public
|
36
|
+
def instrument(event_id, payload = {})
|
37
|
+
publish_event = false # ensure block is also called on error
|
38
|
+
raise(UnregisteredEventError, event_id) unless bus.can_handle?(event_id)
|
39
|
+
|
40
|
+
payload[:__started_at__] = Time.now
|
41
|
+
yield(payload).tap { publish_event = true }
|
42
|
+
ensure
|
43
|
+
if publish_event
|
44
|
+
payload[:runtime] ||= Time.now - payload.delete(:__started_at__) if payload[:__started_at__]
|
45
|
+
bus.publish(event_id, payload)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Subscribe to events.
|
50
|
+
#
|
51
|
+
# @param [Symbol,String,Object] object_or_event_id The event identifier or a listener object
|
52
|
+
# @param [Hash] filter_hash An optional event filter
|
53
|
+
#
|
54
|
+
# @raise [SiteMaps::Notification::InvalidSubscriberError] if the subscriber is not registered
|
55
|
+
# @return [Object] self
|
56
|
+
#
|
57
|
+
#
|
58
|
+
# @api public
|
59
|
+
def subscribe(object_or_event_id, &block)
|
60
|
+
if bus.can_handle?(object_or_event_id)
|
61
|
+
if block
|
62
|
+
bus.subscribe(object_or_event_id, &block)
|
63
|
+
else
|
64
|
+
bus.attach(object_or_event_id)
|
65
|
+
end
|
66
|
+
|
67
|
+
self
|
68
|
+
else
|
69
|
+
raise InvalidSubscriberError, object_or_event_id
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def bus
|
74
|
+
@bus ||= Bus.new
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SiteMaps
|
4
|
+
module Notification
|
5
|
+
Error = Class.new(SiteMaps::Error)
|
6
|
+
|
7
|
+
class UnregisteredEventError < Error
|
8
|
+
def initialize(object_or_event_id)
|
9
|
+
case object_or_event_id
|
10
|
+
when String, Symbol
|
11
|
+
super("You are trying to publish an unregistered event: `#{object_or_event_id}`")
|
12
|
+
else
|
13
|
+
super("You are trying to publish an unregistered event")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class InvalidSubscriberError < Error
|
19
|
+
def initialize(object_or_event_id)
|
20
|
+
case object_or_event_id
|
21
|
+
when String, Symbol
|
22
|
+
super("you are trying to subscribe to an event: `#{object_or_event_id}` that has not been registered")
|
23
|
+
else
|
24
|
+
super("you try use subscriber object that will never be executed")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
include Publisher
|
30
|
+
|
31
|
+
register_event "sitemaps.builder.finalize_urlset"
|
32
|
+
register_event "sitemaps.runner.before_process_execution"
|
33
|
+
register_event "sitemaps.runner.enqueue_process"
|
34
|
+
register_event "sitemaps.runner.process_execution"
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "rainbow"
|
5
|
+
rescue LoadError
|
6
|
+
end
|
7
|
+
|
8
|
+
module SiteMaps
|
9
|
+
module Primitives
|
10
|
+
module Output
|
11
|
+
module_function
|
12
|
+
|
13
|
+
def formatted_runtime(number)
|
14
|
+
colorize(sprintf("%.3f ms", number), :lightgray)
|
15
|
+
end
|
16
|
+
|
17
|
+
def runtime_padding(number, extra = 2)
|
18
|
+
" " * (extra + sprintf("%.3f ms", number).size)
|
19
|
+
end
|
20
|
+
|
21
|
+
def colorize(text, *attributes)
|
22
|
+
if defined? Rainbow
|
23
|
+
attributes.reduce(Rainbow(text)) { |p, a| p.public_send(a) }
|
24
|
+
else
|
25
|
+
text
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def print_error(message_or_error, backtrace: false, **options)
|
30
|
+
options[:level] ||= :error
|
31
|
+
message = message_or_error.to_s
|
32
|
+
|
33
|
+
print_message(message, output: :stderr, **options)
|
34
|
+
|
35
|
+
if message_or_error.is_a?(Exception) && backtrace
|
36
|
+
limit = backtrace.is_a?(Integer) ? backtrace : -1
|
37
|
+
print_backtrace(message_or_error, limit: limit, level: options[:level])
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def print_backtrace(error, limit: -1, **options)
|
42
|
+
return unless error.respond_to?(:backtrace)
|
43
|
+
return if error.backtrace.nil?
|
44
|
+
|
45
|
+
error.backtrace[0..limit].each { |frame| print_error(frame, **options) }
|
46
|
+
end
|
47
|
+
|
48
|
+
def print_message(message, level: :info, output: $stdout, newline: true, **fields)
|
49
|
+
output =
|
50
|
+
case output
|
51
|
+
when :stdout, "stdout"
|
52
|
+
$stdout
|
53
|
+
when :stderr, "stderr"
|
54
|
+
$stderr
|
55
|
+
when IO, StringIO
|
56
|
+
output
|
57
|
+
else
|
58
|
+
raise ArgumentError, "Invalid output #{output.inspect}"
|
59
|
+
end
|
60
|
+
|
61
|
+
message = format(message, **fields)
|
62
|
+
newline ? output.puts(message) : output.print(message)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "dry/inflector"
|
5
|
+
rescue LoadError
|
6
|
+
# noop
|
7
|
+
end
|
8
|
+
|
9
|
+
begin
|
10
|
+
require "active_support/inflector"
|
11
|
+
rescue LoadError
|
12
|
+
# noop
|
13
|
+
end
|
14
|
+
|
15
|
+
module SiteMaps::Primitives
|
16
|
+
class String < ::String
|
17
|
+
def classify
|
18
|
+
new_str = if defined?(Dry::Inflector)
|
19
|
+
Dry::Inflector.new.classify(self)
|
20
|
+
elsif defined?(ActiveSupport::Inflector)
|
21
|
+
ActiveSupport::Inflector.classify(self)
|
22
|
+
else
|
23
|
+
split("_").map(&:capitalize).join
|
24
|
+
end
|
25
|
+
|
26
|
+
self.class.new(new_str)
|
27
|
+
end
|
28
|
+
|
29
|
+
def underscore
|
30
|
+
new_str = sub(/^::/, "")
|
31
|
+
.gsub("::", "/")
|
32
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
33
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
34
|
+
.tr("-", "_")
|
35
|
+
.tr(".", "_")
|
36
|
+
.gsub(/\s/, "_")
|
37
|
+
.gsub(/__+/, "_")
|
38
|
+
.downcase
|
39
|
+
|
40
|
+
self.class.new(new_str)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SiteMaps
|
4
|
+
Process = Concurrent::ImmutableStruct.new(:name, :location_template, :kwargs_template, :block) do
|
5
|
+
def location(**kwargs)
|
6
|
+
return unless location_template
|
7
|
+
|
8
|
+
location_template % keyword_arguments(kwargs)
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(builder, **kwargs)
|
12
|
+
return unless block
|
13
|
+
|
14
|
+
block.call(builder, **keyword_arguments(kwargs))
|
15
|
+
end
|
16
|
+
|
17
|
+
def static?
|
18
|
+
!dynamic?
|
19
|
+
end
|
20
|
+
|
21
|
+
def dynamic?
|
22
|
+
kwargs_template.is_a?(Hash) && kwargs_template.any?
|
23
|
+
end
|
24
|
+
|
25
|
+
def keyword_arguments(given)
|
26
|
+
(kwargs_template || {}).merge(given || {})
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "singleton"
|
4
|
+
Kernel.require "rails/railtie"
|
5
|
+
|
6
|
+
module SiteMaps
|
7
|
+
class Railtie < ::Rails::Railtie
|
8
|
+
initializer "site_maps.named_routes" do
|
9
|
+
named_route = Class.new do
|
10
|
+
include Singleton
|
11
|
+
include ::Rails.application.routes.url_helpers
|
12
|
+
end
|
13
|
+
SiteMaps::Adapters::Adapter.prepend(Module.new do
|
14
|
+
define_method(:route) { named_route.instance }
|
15
|
+
end)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|