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.
Files changed (51) hide show
  1. checksums.yaml +5 -5
  2. data/{pakyow-support/CHANGELOG.md → CHANGELOG.md} +4 -0
  3. data/LICENSE +4 -0
  4. data/{pakyow-support/README.md → README.md} +1 -2
  5. data/lib/pakyow/support/aargv.rb +25 -0
  6. data/lib/pakyow/support/bindable.rb +19 -0
  7. data/lib/pakyow/support/class_state.rb +49 -0
  8. data/lib/pakyow/support/cli/runner.rb +106 -0
  9. data/lib/pakyow/support/cli/style.rb +13 -0
  10. data/lib/pakyow/support/configurable/config.rb +153 -0
  11. data/lib/pakyow/support/configurable/setting.rb +52 -0
  12. data/lib/pakyow/support/configurable.rb +103 -0
  13. data/lib/pakyow/support/core_refinements/array/ensurable.rb +25 -0
  14. data/lib/pakyow/support/core_refinements/method/introspection.rb +21 -0
  15. data/lib/pakyow/support/core_refinements/proc/introspection.rb +21 -0
  16. data/lib/pakyow/support/core_refinements/string/normalization.rb +50 -0
  17. data/lib/pakyow/support/deep_dup.rb +61 -0
  18. data/lib/pakyow/support/deep_freeze.rb +82 -0
  19. data/lib/pakyow/support/definable.rb +242 -0
  20. data/lib/pakyow/support/dependencies.rb +61 -0
  21. data/lib/pakyow/support/extension.rb +82 -0
  22. data/lib/pakyow/support/hookable.rb +227 -0
  23. data/lib/pakyow/support/indifferentize.rb +183 -0
  24. data/lib/pakyow/support/inflector.rb +13 -0
  25. data/lib/pakyow/support/inspectable.rb +88 -0
  26. data/lib/pakyow/support/logging.rb +31 -0
  27. data/lib/pakyow/support/makeable/object_maker.rb +30 -0
  28. data/lib/pakyow/support/makeable/object_name.rb +45 -0
  29. data/lib/pakyow/support/makeable/object_namespace.rb +28 -0
  30. data/lib/pakyow/support/makeable.rb +117 -0
  31. data/lib/pakyow/support/message_verifier.rb +74 -0
  32. data/lib/pakyow/support/path_version.rb +21 -0
  33. data/lib/pakyow/support/pipeline/object.rb +41 -0
  34. data/lib/pakyow/support/pipeline.rb +335 -0
  35. data/lib/pakyow/support/safe_string.rb +60 -0
  36. data/lib/pakyow/support/serializer.rb +49 -0
  37. data/lib/pakyow/support/silenceable.rb +21 -0
  38. data/lib/pakyow/support/string_builder.rb +62 -0
  39. data/lib/pakyow/support.rb +1 -0
  40. metadata +107 -26
  41. data/pakyow-support/LICENSE +0 -20
  42. data/pakyow-support/lib/pakyow/support/aargv.rb +0 -15
  43. data/pakyow-support/lib/pakyow/support/array.rb +0 -9
  44. data/pakyow-support/lib/pakyow/support/dir.rb +0 -32
  45. data/pakyow-support/lib/pakyow/support/dup.rb +0 -23
  46. data/pakyow-support/lib/pakyow/support/file.rb +0 -5
  47. data/pakyow-support/lib/pakyow/support/hash.rb +0 -47
  48. data/pakyow-support/lib/pakyow/support/kernel.rb +0 -9
  49. data/pakyow-support/lib/pakyow/support/string.rb +0 -36
  50. data/pakyow-support/lib/pakyow/support.rb +0 -8
  51. 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