pakyow-support 0.11.3 → 1.0.0.rc1
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/{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
|