/
processor_utils.rb
180 lines (157 loc) · 5.7 KB
/
processor_utils.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
require 'set'
module Sprockets
# Functional utilities for dealing with Processor functions.
#
# A Processor is a general function that my modify or transform an asset as
# part of the pipeline. CoffeeScript to JavaScript conversion, Minification
# or Concatenation are all implemented as seperate Processor steps.
#
# Processors maybe any object that responds to call. So procs or a class that
# defines a self.call method.
#
# For ergonomics, processors may return a number of shorthand values.
# Unfortunately, this means that processors can not compose via ordinary
# function composition. The composition helpers here can help.
module ProcessorUtils
extend self
# Public: Compose processors in right to left order.
#
# processors - Array of processors callables
#
# Returns a composed Proc.
def compose_processors(*processors)
context = self
if processors.length == 1
obj = method(:call_processor).to_proc.curry[processors.first]
else
obj = method(:call_processors).to_proc.curry[processors]
end
metaclass = (class << obj; self; end)
metaclass.send(:define_method, :cache_key) do
context.processors_cache_keys(processors)
end
obj
end
# Public: Invoke list of processors in right to left order.
#
# The right to left order processing mirrors standard function composition.
# Think about:
#
# bundle.call(uglify.call(coffee.call(input)))
#
# processors - Array of processor callables
# input - Hash of input data to pass to each processor
#
# Returns a Hash with :data and other processor metadata key/values.
def call_processors(processors, input)
data = input[:data] || ""
metadata = (input[:metadata] || {}).dup
processors.reverse_each do |processor|
result = call_processor(processor, input.merge(data: data, metadata: metadata))
data = result.delete(:data)
metadata.merge!(result)
end
metadata.merge(data: data)
end
# Public: Invoke processor.
#
# processor - Processor callables
# input - Hash of input data to pass to processor
#
# Returns a Hash with :data and other processor metadata key/values.
def call_processor(processor, input)
metadata = (input[:metadata] || {}).dup
metadata[:data] = input[:data]
case result = processor.call({data: "", metadata: {}}.merge(input))
when NilClass
metadata
when Hash
metadata.merge(result)
when String
metadata.merge(data: result)
else
raise TypeError, "invalid processor return type: #{result.class}"
end
end
# Internal: Get processor defined cached key.
#
# processor - Processor function
#
# Returns JSON serializable key or nil.
def processor_cache_key(processor)
processor.cache_key if processor.respond_to?(:cache_key)
end
# Internal: Get combined cache keys for set of processors.
#
# processors - Array of processor functions
#
# Returns Array of JSON serializable keys.
def processors_cache_keys(processors)
processors.map { |processor| processor_cache_key(processor) }
end
# Internal: Set of all "simple" value types allowed to be returned in
# processor metadata.
VALID_METADATA_VALUE_TYPES = Set.new([
String,
Symbol,
TrueClass,
FalseClass,
NilClass
] + (0.class == Integer ? [Integer] : [Bignum, Fixnum])).freeze
# Internal: Set of all nested compound metadata types that can nest values.
VALID_METADATA_COMPOUND_TYPES = Set.new([
Array,
Hash,
Set
]).freeze
# Internal: Hash of all "simple" value types allowed to be returned in
# processor metadata.
VALID_METADATA_VALUE_TYPES_HASH = VALID_METADATA_VALUE_TYPES.each_with_object({}) do |type, hash|
hash[type] = true
end.freeze
# Internal: Hash of all nested compound metadata types that can nest values.
VALID_METADATA_COMPOUND_TYPES_HASH = VALID_METADATA_COMPOUND_TYPES.each_with_object({}) do |type, hash|
hash[type] = true
end.freeze
# Internal: Set of all allowed metadata types.
VALID_METADATA_TYPES = (VALID_METADATA_VALUE_TYPES + VALID_METADATA_COMPOUND_TYPES).freeze
# Internal: Validate returned result of calling a processor pipeline and
# raise a friendly user error message.
#
# result - Metadata Hash returned from call_processors
#
# Returns result or raises a TypeError.
def validate_processor_result!(result)
if !result.instance_of?(Hash)
raise TypeError, "processor metadata result was expected to be a Hash, but was #{result.class}"
end
if !result[:data].instance_of?(String)
raise TypeError, "processor :data was expected to be a String, but as #{result[:data].class}"
end
result.each do |key, value|
if !key.instance_of?(Symbol)
raise TypeError, "processor metadata[#{key.inspect}] expected to be a Symbol"
end
if !valid_processor_metadata_value?(value)
raise TypeError, "processor metadata[:#{key}] returned a complex type: #{value.inspect}\n" +
"Only #{VALID_METADATA_TYPES.to_a.join(", ")} maybe used."
end
end
result
end
# Internal: Validate object is in validate metadata whitelist.
#
# value - Any Object
#
# Returns true if class is in whitelist otherwise false.
def valid_processor_metadata_value?(value)
if VALID_METADATA_VALUE_TYPES_HASH[value.class]
true
elsif VALID_METADATA_COMPOUND_TYPES_HASH[value.class]
value.all? { |v| valid_processor_metadata_value?(v) }
else
false
end
end
end
end