pakyow-support 0.11.3 → 1.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/{pakyow-support/CHANGELOG.md → CHANGELOG.md} +4 -0
- data/LICENSE +4 -0
- data/{pakyow-support/README.md → README.md} +1 -2
- data/lib/pakyow/support/aargv.rb +25 -0
- data/lib/pakyow/support/bindable.rb +19 -0
- data/lib/pakyow/support/class_state.rb +49 -0
- data/lib/pakyow/support/cli/runner.rb +106 -0
- data/lib/pakyow/support/cli/style.rb +13 -0
- data/lib/pakyow/support/configurable/config.rb +153 -0
- data/lib/pakyow/support/configurable/setting.rb +52 -0
- data/lib/pakyow/support/configurable.rb +103 -0
- data/lib/pakyow/support/core_refinements/array/ensurable.rb +25 -0
- data/lib/pakyow/support/core_refinements/method/introspection.rb +21 -0
- data/lib/pakyow/support/core_refinements/proc/introspection.rb +21 -0
- data/lib/pakyow/support/core_refinements/string/normalization.rb +50 -0
- data/lib/pakyow/support/deep_dup.rb +61 -0
- data/lib/pakyow/support/deep_freeze.rb +82 -0
- data/lib/pakyow/support/definable.rb +242 -0
- data/lib/pakyow/support/dependencies.rb +61 -0
- data/lib/pakyow/support/extension.rb +82 -0
- data/lib/pakyow/support/hookable.rb +227 -0
- data/lib/pakyow/support/indifferentize.rb +183 -0
- data/lib/pakyow/support/inflector.rb +13 -0
- data/lib/pakyow/support/inspectable.rb +88 -0
- data/lib/pakyow/support/logging.rb +31 -0
- data/lib/pakyow/support/makeable/object_maker.rb +30 -0
- data/lib/pakyow/support/makeable/object_name.rb +45 -0
- data/lib/pakyow/support/makeable/object_namespace.rb +28 -0
- data/lib/pakyow/support/makeable.rb +117 -0
- data/lib/pakyow/support/message_verifier.rb +74 -0
- data/lib/pakyow/support/path_version.rb +21 -0
- data/lib/pakyow/support/pipeline/object.rb +41 -0
- data/lib/pakyow/support/pipeline.rb +335 -0
- data/lib/pakyow/support/safe_string.rb +60 -0
- data/lib/pakyow/support/serializer.rb +49 -0
- data/lib/pakyow/support/silenceable.rb +21 -0
- data/lib/pakyow/support/string_builder.rb +62 -0
- data/lib/pakyow/support.rb +1 -0
- metadata +107 -26
- data/pakyow-support/LICENSE +0 -20
- data/pakyow-support/lib/pakyow/support/aargv.rb +0 -15
- data/pakyow-support/lib/pakyow/support/array.rb +0 -9
- data/pakyow-support/lib/pakyow/support/dir.rb +0 -32
- data/pakyow-support/lib/pakyow/support/dup.rb +0 -23
- data/pakyow-support/lib/pakyow/support/file.rb +0 -5
- data/pakyow-support/lib/pakyow/support/hash.rb +0 -47
- data/pakyow-support/lib/pakyow/support/kernel.rb +0 -9
- data/pakyow-support/lib/pakyow/support/string.rb +0 -36
- data/pakyow-support/lib/pakyow/support.rb +0 -8
- data/pakyow-support/lib/pakyow-support.rb +0 -1
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pakyow/support/hookable"
|
4
|
+
|
5
|
+
require "pakyow/support/makeable/object_maker"
|
6
|
+
require "pakyow/support/makeable/object_name"
|
7
|
+
|
8
|
+
module Pakyow
|
9
|
+
module Support
|
10
|
+
# @api private
|
11
|
+
module Makeable
|
12
|
+
def self.extended(base)
|
13
|
+
# Mixin the `make` event for objects that are hookable.
|
14
|
+
#
|
15
|
+
if base.ancestors.include?(Hookable)
|
16
|
+
base.events :make
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :__object_name
|
21
|
+
|
22
|
+
def make(object_name, within: nil, set_const: true, **kwargs, &block)
|
23
|
+
object_name = build_object_name(object_name, within: within)
|
24
|
+
object = find_or_define_object(object_name, kwargs, set_const)
|
25
|
+
|
26
|
+
local_eval_method = eval_method
|
27
|
+
object.send(eval_method) do
|
28
|
+
@__object_name = object_name
|
29
|
+
send(local_eval_method, &block) if block_given?
|
30
|
+
end
|
31
|
+
|
32
|
+
if object.ancestors.include?(Hookable)
|
33
|
+
object.call_hooks(:after, :make)
|
34
|
+
end
|
35
|
+
|
36
|
+
object
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def build_object_name(object_name, within:)
|
42
|
+
unless object_name.is_a?(ObjectName) || object_name.nil?
|
43
|
+
namespace = if within && within.respond_to?(:__object_name)
|
44
|
+
within.__object_name.namespace
|
45
|
+
elsif within.is_a?(ObjectNamespace)
|
46
|
+
within
|
47
|
+
else
|
48
|
+
ObjectNamespace.new
|
49
|
+
end
|
50
|
+
|
51
|
+
object_name_parts = object_name.to_s.gsub("-", "_").split("/").reject(&:empty?)
|
52
|
+
class_name = object_name_parts.pop || :index
|
53
|
+
|
54
|
+
object_name = Support::ObjectName.new(
|
55
|
+
Support::ObjectNamespace.new(
|
56
|
+
*(namespace.parts + object_name_parts)
|
57
|
+
), class_name
|
58
|
+
)
|
59
|
+
end
|
60
|
+
|
61
|
+
object_name
|
62
|
+
end
|
63
|
+
|
64
|
+
def find_or_define_object(object_name, kwargs, set_const)
|
65
|
+
if object_name && ::Object.const_defined?(object_name.constant, false)
|
66
|
+
existing_object = ::Object.const_get(object_name.constant)
|
67
|
+
|
68
|
+
if type_of_self?(existing_object)
|
69
|
+
existing_object
|
70
|
+
else
|
71
|
+
define_object(kwargs)
|
72
|
+
end
|
73
|
+
else
|
74
|
+
define_object(kwargs).tap do |defined_object|
|
75
|
+
if set_const
|
76
|
+
ObjectMaker.define_const_for_object_with_name(defined_object, object_name)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def type_of_self?(object)
|
83
|
+
object.ancestors.include?(ancestors[1])
|
84
|
+
end
|
85
|
+
|
86
|
+
def define_object(kwargs)
|
87
|
+
object = case self
|
88
|
+
when Class
|
89
|
+
Class.new(self)
|
90
|
+
when Module
|
91
|
+
Module.new do
|
92
|
+
def self.__object_name
|
93
|
+
@__object_name
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
object.send(eval_method) do
|
99
|
+
kwargs.each do |arg, value|
|
100
|
+
instance_variable_set(:"@#{arg}", value)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
object
|
105
|
+
end
|
106
|
+
|
107
|
+
def eval_method
|
108
|
+
case self
|
109
|
+
when Class
|
110
|
+
:class_exec
|
111
|
+
when Module
|
112
|
+
:module_exec
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "base64"
|
4
|
+
require "openssl"
|
5
|
+
require "securerandom"
|
6
|
+
|
7
|
+
module Pakyow
|
8
|
+
module Support
|
9
|
+
# Signs and verifes messages for a key.
|
10
|
+
#
|
11
|
+
class MessageVerifier
|
12
|
+
attr_reader :key
|
13
|
+
|
14
|
+
JOIN_CHARACTER = "--"
|
15
|
+
|
16
|
+
# TODO: support configuring the digest
|
17
|
+
# TODO: support rotations by calling `rotate` with options
|
18
|
+
#
|
19
|
+
def initialize(key = self.class.key)
|
20
|
+
@key = key
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns a signed message.
|
24
|
+
#
|
25
|
+
def sign(message)
|
26
|
+
[Base64.urlsafe_encode64(message), self.class.digest(message, key: @key)].join(JOIN_CHARACTER)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns the message if the signature is valid for the key, or raises `TamperedMessage`.
|
30
|
+
#
|
31
|
+
def verify(signed)
|
32
|
+
message, digest = signed.to_s.split(JOIN_CHARACTER, 2)
|
33
|
+
|
34
|
+
begin
|
35
|
+
message = Base64.urlsafe_decode64(message.to_s)
|
36
|
+
rescue ArgumentError
|
37
|
+
end
|
38
|
+
|
39
|
+
if self.class.valid?(digest, message: message, key: @key)
|
40
|
+
message
|
41
|
+
else
|
42
|
+
raise(TamperedMessage)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class << self
|
47
|
+
# Generates a random key.
|
48
|
+
#
|
49
|
+
def key
|
50
|
+
SecureRandom.hex(24)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Generates a digest for a message with a key.
|
54
|
+
#
|
55
|
+
def digest(message, key:)
|
56
|
+
Base64.urlsafe_encode64(
|
57
|
+
OpenSSL::HMAC.digest(
|
58
|
+
OpenSSL::Digest.new("sha256"), message.to_s, key.to_s
|
59
|
+
)
|
60
|
+
)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns true if the digest is valid for the message and key.
|
64
|
+
#
|
65
|
+
def valid?(digest, message:, key:)
|
66
|
+
digest == self.digest(message, key: key)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class TamperedMessage < StandardError
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "digest/sha1"
|
4
|
+
|
5
|
+
module Pakyow
|
6
|
+
module Support
|
7
|
+
class PathVersion
|
8
|
+
# Builds a version based on content at local paths.
|
9
|
+
#
|
10
|
+
def self.build(*paths)
|
11
|
+
paths.each_with_object(Digest::SHA1.new) { |path, digest|
|
12
|
+
Dir.glob(File.join(path, "/**/*")).sort.each do |fullpath|
|
13
|
+
if File.file?(fullpath)
|
14
|
+
digest.update(Digest::SHA1.file(fullpath).to_s)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
}.to_s
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pakyow
|
4
|
+
module Support
|
5
|
+
module Pipeline
|
6
|
+
# Makes an object passable through a pipeline.
|
7
|
+
#
|
8
|
+
module Object
|
9
|
+
def self.included(base)
|
10
|
+
base.prepend Initializer
|
11
|
+
end
|
12
|
+
|
13
|
+
def pipelined
|
14
|
+
tap do
|
15
|
+
@pipelined = true
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def pipelined?
|
20
|
+
@pipelined == true
|
21
|
+
end
|
22
|
+
|
23
|
+
def halt
|
24
|
+
@halted = true
|
25
|
+
throw :halt, true
|
26
|
+
end
|
27
|
+
|
28
|
+
def halted?
|
29
|
+
@halted == true
|
30
|
+
end
|
31
|
+
|
32
|
+
module Initializer
|
33
|
+
def initialize(*args)
|
34
|
+
@pipelined, @halted = false
|
35
|
+
super
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,335 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pakyow/support/class_state"
|
4
|
+
|
5
|
+
module Pakyow
|
6
|
+
module Support
|
7
|
+
# Provides pipeline behavior. Pipeline objects can define actions to be called in order on an
|
8
|
+
# instance of the pipelined object. Each action can act on the state passed to it. Any action
|
9
|
+
# can halt the pipeline, causing the result to be immediately returned without calling other
|
10
|
+
# actions. State passed through the pipeline should include {Pipelined::Object}.
|
11
|
+
#
|
12
|
+
# See {Pakyow::App} and {Pakyow::Controller} for examples of more complex pipelines.
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# class App
|
16
|
+
# include Pakyow::Support::Pipeline
|
17
|
+
#
|
18
|
+
# action :foo
|
19
|
+
# action :bar
|
20
|
+
#
|
21
|
+
# def foo(result)
|
22
|
+
# result << "foo"
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# def bar(result)
|
26
|
+
# result << "bar"
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# class Result
|
31
|
+
# include Pakyow::Support::Pipeline::Object
|
32
|
+
#
|
33
|
+
# attr_reader :results
|
34
|
+
#
|
35
|
+
# def initialize
|
36
|
+
# @results = []
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# def <<(result)
|
40
|
+
# @results << result
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# App.new.call(Result.new).results
|
45
|
+
# => ["foo", "bar"]
|
46
|
+
#
|
47
|
+
# = Modules
|
48
|
+
#
|
49
|
+
# Pipeline behavior can be added to a module and then used in a pipelined object.
|
50
|
+
#
|
51
|
+
# @example
|
52
|
+
# module VerifyRequest
|
53
|
+
# extend Pakyow::Support::Pipeline
|
54
|
+
#
|
55
|
+
# action :verify_request
|
56
|
+
#
|
57
|
+
# def verify_request
|
58
|
+
# ...
|
59
|
+
# end
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# class App
|
63
|
+
# include Pakyow::Support::Pipeline
|
64
|
+
#
|
65
|
+
# use_pipeline VerifyRequest
|
66
|
+
#
|
67
|
+
# ...
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
module Pipeline
|
71
|
+
# @api private
|
72
|
+
def self.extended(base)
|
73
|
+
base.extend ClassMethods
|
74
|
+
base.extend ClassState unless base.ancestors.include?(ClassState)
|
75
|
+
base.class_state :__pipelines, default: {}, inheritable: true
|
76
|
+
base.class_state :__pipeline, inheritable: true
|
77
|
+
|
78
|
+
base.instance_variable_set(:@__pipeline, Internal.new)
|
79
|
+
end
|
80
|
+
|
81
|
+
# @api private
|
82
|
+
def self.included(base)
|
83
|
+
base.extend ClassMethods
|
84
|
+
base.extend ClassState unless base.ancestors.include?(ClassState)
|
85
|
+
base.prepend Initializer
|
86
|
+
base.class_state :__pipelines, default: {}, inheritable: true
|
87
|
+
base.class_state :__pipeline, inheritable: true
|
88
|
+
|
89
|
+
# Define a default pipeline so that actions can be defined immediately without ceremony.
|
90
|
+
#
|
91
|
+
base.pipeline :default do; end
|
92
|
+
base.use_pipeline :default
|
93
|
+
end
|
94
|
+
|
95
|
+
# Calls the pipeline, passing +state+.
|
96
|
+
#
|
97
|
+
def call(state)
|
98
|
+
@__pipeline.call(state)
|
99
|
+
end
|
100
|
+
|
101
|
+
def initialize_copy(_)
|
102
|
+
super
|
103
|
+
|
104
|
+
@__pipeline = @__pipeline.dup
|
105
|
+
|
106
|
+
# rebind any methods to the new instance
|
107
|
+
@__pipeline.instance_variable_get(:@stack).map! { |action|
|
108
|
+
if action.is_a?(::Method) && action.receiver.is_a?(self.class)
|
109
|
+
action.unbind.bind(self)
|
110
|
+
else
|
111
|
+
action
|
112
|
+
end
|
113
|
+
}
|
114
|
+
end
|
115
|
+
|
116
|
+
# @api private
|
117
|
+
module Initializer
|
118
|
+
def initialize(*)
|
119
|
+
@__pipeline = self.class.__pipeline.callable(self)
|
120
|
+
super
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
module ClassMethods
|
125
|
+
# Defines a pipeline.
|
126
|
+
#
|
127
|
+
def pipeline(name, &block)
|
128
|
+
@__pipelines[name.to_sym] = Internal.new(&block)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Uses a pipeline.
|
132
|
+
#
|
133
|
+
def use_pipeline(name_or_pipeline)
|
134
|
+
pipeline = find_pipeline(name_or_pipeline)
|
135
|
+
include name_or_pipeline if name_or_pipeline.is_a?(Pipeline)
|
136
|
+
@__pipeline = pipeline
|
137
|
+
end
|
138
|
+
|
139
|
+
# Includes actions into the current pipeline.
|
140
|
+
#
|
141
|
+
def include_pipeline(name_or_pipeline)
|
142
|
+
pipeline = find_pipeline(name_or_pipeline)
|
143
|
+
include name_or_pipeline if name_or_pipeline.is_a?(Pipeline)
|
144
|
+
@__pipeline.include_actions(pipeline.actions)
|
145
|
+
end
|
146
|
+
|
147
|
+
# Excludes actions from the current pipeline.
|
148
|
+
#
|
149
|
+
def exclude_pipeline(name_or_pipeline)
|
150
|
+
pipeline = find_pipeline(name_or_pipeline)
|
151
|
+
@__pipeline.exclude_actions(pipeline.actions)
|
152
|
+
end
|
153
|
+
|
154
|
+
# Defines an action on the current pipeline.
|
155
|
+
#
|
156
|
+
def action(action = nil, *options, before: nil, after: nil, &block)
|
157
|
+
@__pipeline.action(action, *options, before: before, after: after, &block)
|
158
|
+
end
|
159
|
+
|
160
|
+
def skip(*actions)
|
161
|
+
@__pipeline.skip(*actions)
|
162
|
+
end
|
163
|
+
|
164
|
+
private
|
165
|
+
|
166
|
+
def find_pipeline(name_or_pipeline)
|
167
|
+
if name_or_pipeline.is_a?(Pipeline)
|
168
|
+
name_or_pipeline.instance_variable_get(:@__pipeline)
|
169
|
+
elsif name_or_pipeline.is_a?(Internal)
|
170
|
+
name_or_pipeline
|
171
|
+
else
|
172
|
+
name_or_pipeline = name_or_pipeline.to_sym
|
173
|
+
if @__pipelines.key?(name_or_pipeline)
|
174
|
+
@__pipelines[name_or_pipeline]
|
175
|
+
else
|
176
|
+
raise ArgumentError, "could not find a pipeline named `#{name_or_pipeline}'"
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# @api private
|
183
|
+
class Internal
|
184
|
+
attr_reader :actions
|
185
|
+
|
186
|
+
def initialize
|
187
|
+
@actions = []
|
188
|
+
|
189
|
+
if block_given?
|
190
|
+
instance_exec(&Proc.new)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def initialize_copy(_)
|
195
|
+
@actions = @actions.dup
|
196
|
+
super
|
197
|
+
end
|
198
|
+
|
199
|
+
def callable(context)
|
200
|
+
Callable.new(@actions, context)
|
201
|
+
end
|
202
|
+
|
203
|
+
def action(target, *options, before: nil, after: nil, &block)
|
204
|
+
Action.new(target, *options, &block).tap do |action|
|
205
|
+
if before
|
206
|
+
if i = @actions.index { |a| a.name == before }
|
207
|
+
@actions.insert(i, action)
|
208
|
+
else
|
209
|
+
@actions.unshift(action)
|
210
|
+
end
|
211
|
+
elsif after
|
212
|
+
if i = @actions.index { |a| a.name == after }
|
213
|
+
@actions.insert(i + 1, action)
|
214
|
+
else
|
215
|
+
@actions << action
|
216
|
+
end
|
217
|
+
else
|
218
|
+
@actions << action
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def skip(*actions)
|
224
|
+
@actions.delete_if { |action|
|
225
|
+
actions.include?(action.name)
|
226
|
+
}
|
227
|
+
end
|
228
|
+
|
229
|
+
def include_actions(actions)
|
230
|
+
@actions.concat(actions).uniq!
|
231
|
+
end
|
232
|
+
|
233
|
+
def exclude_actions(actions)
|
234
|
+
# Map input into a common denominator, to exclude both names and other action objects.
|
235
|
+
targets = actions.map { |action|
|
236
|
+
if action.is_a?(Action)
|
237
|
+
action.target
|
238
|
+
else
|
239
|
+
action
|
240
|
+
end
|
241
|
+
}
|
242
|
+
|
243
|
+
@actions.delete_if { |action|
|
244
|
+
targets.include?(action.target)
|
245
|
+
}
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
# @api private
|
250
|
+
class Callable
|
251
|
+
def initialize(actions, context)
|
252
|
+
@stack = actions.map { |action|
|
253
|
+
action.finalize(context)
|
254
|
+
}
|
255
|
+
end
|
256
|
+
|
257
|
+
def initialize_copy(_)
|
258
|
+
@stack = @stack.dup
|
259
|
+
|
260
|
+
super
|
261
|
+
end
|
262
|
+
|
263
|
+
def call(object, stack = @stack.dup)
|
264
|
+
catch :halt do
|
265
|
+
until stack.empty? || (object.respond_to?(:halted?) && object.halted?)
|
266
|
+
action = stack.shift
|
267
|
+
if action.arity == 0
|
268
|
+
action.call do
|
269
|
+
call(object, stack)
|
270
|
+
end
|
271
|
+
else
|
272
|
+
action.call(object) do
|
273
|
+
call(object, stack)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
object.pipelined
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
# @api private
|
284
|
+
class Action
|
285
|
+
attr_reader :target, :name, :options
|
286
|
+
|
287
|
+
def initialize(target, *options, &block)
|
288
|
+
@target, @options, @block = target, options, block
|
289
|
+
|
290
|
+
if target.is_a?(Symbol)
|
291
|
+
@name = target
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
def finalize(context = nil)
|
296
|
+
if @block
|
297
|
+
if context
|
298
|
+
if @block.arity == 0
|
299
|
+
Proc.new do
|
300
|
+
context.instance_exec(&@block)
|
301
|
+
end
|
302
|
+
else
|
303
|
+
Proc.new do |object|
|
304
|
+
context.instance_exec(object, &@block)
|
305
|
+
end
|
306
|
+
end
|
307
|
+
else
|
308
|
+
@block
|
309
|
+
end
|
310
|
+
elsif @target.is_a?(Symbol) && context.respond_to?(@target, true)
|
311
|
+
if context
|
312
|
+
context.method(@target)
|
313
|
+
else
|
314
|
+
raise "finalizing pipeline action #{@target} requires context"
|
315
|
+
end
|
316
|
+
else
|
317
|
+
target, target_options = if @target.is_a?(Symbol)
|
318
|
+
[@options[0], @options[1..-1]]
|
319
|
+
else
|
320
|
+
[@target, @options]
|
321
|
+
end
|
322
|
+
|
323
|
+
instance = if target.is_a?(Class)
|
324
|
+
target.new(*target_options)
|
325
|
+
else
|
326
|
+
target
|
327
|
+
end
|
328
|
+
|
329
|
+
instance.method(:call)
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "cgi"
|
4
|
+
|
5
|
+
module Pakyow
|
6
|
+
module Support
|
7
|
+
class SafeString < String
|
8
|
+
def initialize(*)
|
9
|
+
super; freeze
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s
|
13
|
+
self
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Helper methods for ensuring string safety.
|
18
|
+
#
|
19
|
+
module SafeStringHelpers
|
20
|
+
extend self
|
21
|
+
|
22
|
+
# Escapes the string unless it's marked as safe.
|
23
|
+
#
|
24
|
+
def ensure_html_safety(string)
|
25
|
+
html_safe?(string) ? string : html_escape(string)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns true if the string is marked as safe.
|
29
|
+
#
|
30
|
+
def html_safe?(string)
|
31
|
+
string.is_a?(SafeString)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Marks a string as safe.
|
35
|
+
#
|
36
|
+
def html_safe(string)
|
37
|
+
html_safe?(string) ? string : SafeString.new(string)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Escapes html characters in the string.
|
41
|
+
#
|
42
|
+
def html_escape(string)
|
43
|
+
html_safe(CGI.escape_html(string.to_s))
|
44
|
+
end
|
45
|
+
|
46
|
+
# Strips html tags from the string.
|
47
|
+
#
|
48
|
+
def strip_tags(string)
|
49
|
+
html_safe(string.to_s.gsub(/<[^>]*>/ui, ""))
|
50
|
+
end
|
51
|
+
|
52
|
+
# Strips html tags from the string, except for tags specified.
|
53
|
+
#
|
54
|
+
def sanitize(string, tags: [])
|
55
|
+
return strip_tags(string) if tags.empty?
|
56
|
+
html_safe(string.to_s.gsub(/((?!<((\/)?#{tags.join("|")}))<[^>]*>)/i, ""))
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fileutils"
|
4
|
+
|
5
|
+
require "pakyow/support/logging"
|
6
|
+
|
7
|
+
module Pakyow
|
8
|
+
module Support
|
9
|
+
# Persists state for an object.
|
10
|
+
#
|
11
|
+
class Serializer
|
12
|
+
attr_reader :object, :name, :path
|
13
|
+
|
14
|
+
def initialize(object, name:, path:)
|
15
|
+
@object, @name, @path = object, name, path
|
16
|
+
end
|
17
|
+
|
18
|
+
def serialize
|
19
|
+
FileUtils.mkdir_p(@path)
|
20
|
+
File.open(serialized_state_path, "w+") do |file|
|
21
|
+
file.write(Marshal.dump(@object.serialize))
|
22
|
+
end
|
23
|
+
rescue => error
|
24
|
+
Logging.yield_or_raise(error) do |logger|
|
25
|
+
logger.error "[Serializer] failed to serialize `#{@name}': #{error}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def deserialize
|
30
|
+
if File.exist?(serialized_state_path)
|
31
|
+
Marshal.load(File.read(serialized_state_path)).each do |ivar, value|
|
32
|
+
@object.instance_variable_set(ivar, value)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
rescue => error
|
36
|
+
FileUtils.rm(serialized_state_path)
|
37
|
+
Logging.yield_or_raise(error) do |logger|
|
38
|
+
logger.error "[Serializer] failed to deserialize `#{@name}': #{error}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def serialized_state_path
|
45
|
+
File.join(@path, "#{@name}.pwstate")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|