saper 0.5.0
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +126 -0
- data/Rakefile +17 -0
- data/bin/saper +60 -0
- data/lib/lib/json_search.rb +54 -0
- data/lib/lib/mechanize.rb +26 -0
- data/lib/lib/nokogiri.rb +12 -0
- data/lib/saper.rb +37 -0
- data/lib/saper/actions/append_with.rb +14 -0
- data/lib/saper/actions/convert_to_html.rb +14 -0
- data/lib/saper/actions/convert_to_json.rb +14 -0
- data/lib/saper/actions/convert_to_markdown.rb +13 -0
- data/lib/saper/actions/convert_to_time.rb +15 -0
- data/lib/saper/actions/convert_to_xml.rb +14 -0
- data/lib/saper/actions/create_atom.rb +18 -0
- data/lib/saper/actions/fetch.rb +17 -0
- data/lib/saper/actions/find.rb +18 -0
- data/lib/saper/actions/find_first.rb +16 -0
- data/lib/saper/actions/get_attribute.rb +15 -0
- data/lib/saper/actions/get_contents.rb +14 -0
- data/lib/saper/actions/get_text.rb +14 -0
- data/lib/saper/actions/prepend_with.rb +14 -0
- data/lib/saper/actions/remove_after.rb +14 -0
- data/lib/saper/actions/remove_before.rb +14 -0
- data/lib/saper/actions/remove_matching.rb +14 -0
- data/lib/saper/actions/remove_tags.rb +15 -0
- data/lib/saper/actions/replace.rb +15 -0
- data/lib/saper/actions/run_recipe.rb +24 -0
- data/lib/saper/actions/run_recipe_and_save.rb +22 -0
- data/lib/saper/actions/save.rb +14 -0
- data/lib/saper/actions/select_matching.rb +14 -0
- data/lib/saper/actions/set_input.rb +19 -0
- data/lib/saper/actions/skip_tags.rb +15 -0
- data/lib/saper/actions/split.rb +24 -0
- data/lib/saper/arguments/attribute.rb +11 -0
- data/lib/saper/arguments/recipe.rb +42 -0
- data/lib/saper/arguments/text.rb +11 -0
- data/lib/saper/arguments/timezone.rb +11 -0
- data/lib/saper/arguments/variable.rb +11 -0
- data/lib/saper/arguments/xpath.rb +11 -0
- data/lib/saper/core/action.rb +209 -0
- data/lib/saper/core/argument.rb +106 -0
- data/lib/saper/core/browser.rb +87 -0
- data/lib/saper/core/dsl.rb +68 -0
- data/lib/saper/core/error.rb +47 -0
- data/lib/saper/core/item.rb +70 -0
- data/lib/saper/core/keychain.rb +18 -0
- data/lib/saper/core/logger.rb +74 -0
- data/lib/saper/core/namespace.rb +139 -0
- data/lib/saper/core/recipe.rb +134 -0
- data/lib/saper/core/runtime.rb +237 -0
- data/lib/saper/core/type.rb +45 -0
- data/lib/saper/items/atom.rb +64 -0
- data/lib/saper/items/document.rb +66 -0
- data/lib/saper/items/html.rb +85 -0
- data/lib/saper/items/json.rb +67 -0
- data/lib/saper/items/markdown.rb +36 -0
- data/lib/saper/items/nothing.rb +15 -0
- data/lib/saper/items/text.rb +54 -0
- data/lib/saper/items/time.rb +42 -0
- data/lib/saper/items/url.rb +34 -0
- data/lib/saper/items/xml.rb +79 -0
- data/lib/saper/version.rb +3 -0
- data/spec/actions/append_with_spec.rb +30 -0
- data/spec/actions/convert_to_html_spec.rb +24 -0
- data/spec/actions/convert_to_json_spec.rb +24 -0
- data/spec/actions/convert_to_markdown_spec.rb +24 -0
- data/spec/actions/convert_to_time_spec.rb +37 -0
- data/spec/actions/convert_to_xml_spec.rb +24 -0
- data/spec/actions/create_atom_spec.rb +31 -0
- data/spec/actions/fetch_spec.rb +7 -0
- data/spec/actions/find_first_spec.rb +7 -0
- data/spec/actions/find_spec.rb +7 -0
- data/spec/actions/get_attribute_spec.rb +7 -0
- data/spec/actions/get_contents.rb +7 -0
- data/spec/actions/get_text.rb +7 -0
- data/spec/actions/prepend_with_spec.rb +30 -0
- data/spec/actions/remove_after.rb +7 -0
- data/spec/actions/remove_before.rb +7 -0
- data/spec/actions/replace_spec.rb +7 -0
- data/spec/actions/run_recipe_and_save_spec.tmp.rb +52 -0
- data/spec/actions/run_recipe_spec.tmp.rb +53 -0
- data/spec/actions/save_spec.rb +7 -0
- data/spec/actions/select_matching_spec.rb +7 -0
- data/spec/actions/set_input_spec.rb +7 -0
- data/spec/actions/skip_tags_spec.rb +7 -0
- data/spec/actions/split_spec.rb +7 -0
- data/spec/core/action_spec.rb +151 -0
- data/spec/core/argument_spec.rb +79 -0
- data/spec/core/browser_spec.rb +7 -0
- data/spec/core/dsl_spec.rb +7 -0
- data/spec/core/item_spec.rb +7 -0
- data/spec/core/keychain_spec.rb +7 -0
- data/spec/core/logger_spec.rb +7 -0
- data/spec/core/namespace_spec.rb +18 -0
- data/spec/core/recipe_spec.rb +81 -0
- data/spec/core/runtime_spec.rb +165 -0
- data/spec/core/type_spec.rb +7 -0
- data/spec/items/atom_spec.rb +7 -0
- data/spec/items/document_spec.rb +7 -0
- data/spec/items/html_spec.rb +7 -0
- data/spec/items/json_spec.rb +7 -0
- data/spec/items/markdown_spec.rb +7 -0
- data/spec/items/nothing_spec.rb +7 -0
- data/spec/items/text_spec.rb +17 -0
- data/spec/items/time_spec.rb +7 -0
- data/spec/items/url_spec.rb +7 -0
- data/spec/items/xml_spec.rb +17 -0
- data/spec/spec_helper.rb +22 -0
- metadata +355 -0
@@ -0,0 +1,237 @@
|
|
1
|
+
module Saper
|
2
|
+
class Runtime
|
3
|
+
|
4
|
+
# Returns action input.
|
5
|
+
attr_reader :input
|
6
|
+
|
7
|
+
# Returns action output.
|
8
|
+
attr_reader :output
|
9
|
+
|
10
|
+
# Returns error instance (if any)
|
11
|
+
attr_reader :error
|
12
|
+
|
13
|
+
# Returns action instance.
|
14
|
+
attr_reader :action
|
15
|
+
|
16
|
+
# Returns embedded recipes
|
17
|
+
attr_reader :subrecipes
|
18
|
+
|
19
|
+
# Returns subsequent runtime instances.
|
20
|
+
attr_reader :children
|
21
|
+
|
22
|
+
# Returns the size of action chain.
|
23
|
+
attr_reader :depth
|
24
|
+
|
25
|
+
# Returns a new Saper::Runtime instance.
|
26
|
+
# @param stack [Array<Saper::Action>] chain of actions
|
27
|
+
# @param input [Object, nil] action input
|
28
|
+
# @param options [Hash]
|
29
|
+
# @return [Saper::Runtime]
|
30
|
+
def initialize(stack = [], input = nil, options = {})
|
31
|
+
if stack.is_a?(Recipe)
|
32
|
+
stack = stack.actions
|
33
|
+
end
|
34
|
+
@input = input
|
35
|
+
@output = nil
|
36
|
+
@error = nil
|
37
|
+
@action = nil
|
38
|
+
@children = []
|
39
|
+
@subrecipes = []
|
40
|
+
@options = options
|
41
|
+
@depth = stack.size
|
42
|
+
@options[:keychain] ||= Keychain.new
|
43
|
+
@options[:logger] ||= Logger.new(@options[:log])
|
44
|
+
@options[:browser] ||= Browser.new(:logger => logger)
|
45
|
+
@options[:variables] ||= {}
|
46
|
+
if block_given?
|
47
|
+
yield self
|
48
|
+
end
|
49
|
+
execute(stack.dup)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns volume of incoming traffic (in bytes)
|
53
|
+
# @return [Integer]
|
54
|
+
def bytes_received
|
55
|
+
browser.received
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns volume of outgoing traffic (in bytes)
|
59
|
+
# @return [Integer]
|
60
|
+
def bytes_sent
|
61
|
+
browser.sent
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns number of HTTP requests
|
65
|
+
# @return [Integer]
|
66
|
+
def http_requests
|
67
|
+
browser.requests
|
68
|
+
end
|
69
|
+
|
70
|
+
# Get variable value
|
71
|
+
# @param name [Symbol]
|
72
|
+
# @return [Object]
|
73
|
+
def [](name)
|
74
|
+
@options[:variables][name]
|
75
|
+
end
|
76
|
+
|
77
|
+
# Set variable value
|
78
|
+
# @param name [Symbol]
|
79
|
+
# @param value [Object]
|
80
|
+
# @return [Object]
|
81
|
+
def []=(name, value)
|
82
|
+
@options[:variables][name] = value
|
83
|
+
end
|
84
|
+
|
85
|
+
# Return a hash with all variables
|
86
|
+
# @return [Hash]
|
87
|
+
def variables
|
88
|
+
@options[:variables].dup
|
89
|
+
end
|
90
|
+
|
91
|
+
# Returns descendant runtime instances at a given depth level.
|
92
|
+
# @param level [Integer]
|
93
|
+
# @return [Array<Saper::Runtime>]
|
94
|
+
def descendants(level = 0)
|
95
|
+
level < 1 ? [self] : children.map { |i| i.descendants(level - 1) }.flatten
|
96
|
+
end
|
97
|
+
|
98
|
+
# Returns runtime results.
|
99
|
+
# @return [Array<Object>, Object]
|
100
|
+
def results
|
101
|
+
native_results
|
102
|
+
end
|
103
|
+
|
104
|
+
# Returns an array of results.
|
105
|
+
# @return [Array]
|
106
|
+
def result_array
|
107
|
+
native_result_array
|
108
|
+
end
|
109
|
+
|
110
|
+
# Returns `true` if this or any of the subsequent actions produces multiple results.
|
111
|
+
# @return [Boolean]
|
112
|
+
def multiple?
|
113
|
+
(action.nil? ? false : action.multiple?) || children.any?(&:multiple?)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Returns Saper::Browser instance used by runtime.
|
117
|
+
# @return [Saper::Browser]
|
118
|
+
def browser
|
119
|
+
@options[:browser]
|
120
|
+
end
|
121
|
+
|
122
|
+
# Returns Saper::Keychain instance used by runtime.
|
123
|
+
# @return [Saper::Keychain]
|
124
|
+
def keychain
|
125
|
+
@options[:keychain]
|
126
|
+
end
|
127
|
+
|
128
|
+
# Returns Saper::Logger.
|
129
|
+
# @return [Saper::Logger]
|
130
|
+
def logger
|
131
|
+
@options[:logger]
|
132
|
+
end
|
133
|
+
|
134
|
+
# Writes full runtime log to logger instance.
|
135
|
+
# @return [void]
|
136
|
+
def backtrace
|
137
|
+
logger.runtime(self)
|
138
|
+
end
|
139
|
+
|
140
|
+
# Returns access credentials for specified service
|
141
|
+
# @param service [Symbol]
|
142
|
+
# @return [Object, nil]
|
143
|
+
def credentials(service)
|
144
|
+
keychain[service]
|
145
|
+
end
|
146
|
+
|
147
|
+
# Runs an arbitrary recipe, specified by ID, block or as instance.
|
148
|
+
# @param recipe [Saper::Recipe]
|
149
|
+
# @param input [Object, nil]
|
150
|
+
# @return [Saper::Runtime]
|
151
|
+
def recipe(recipe, input = nil, &block)
|
152
|
+
unless recipe.is_a?(Recipe)
|
153
|
+
raise RecipeNotFound, recipe
|
154
|
+
end
|
155
|
+
runtime = Runtime.new(recipe, input, @options.merge(:variables => {}))
|
156
|
+
if runtime.error?
|
157
|
+
@error = runtime.error
|
158
|
+
end
|
159
|
+
subrecipes << runtime
|
160
|
+
runtime.non_native_results
|
161
|
+
end
|
162
|
+
|
163
|
+
# Returns `true` if action raised no errors.
|
164
|
+
# @return [Boolean]
|
165
|
+
def success?
|
166
|
+
error.nil?
|
167
|
+
end
|
168
|
+
|
169
|
+
# Returns `true` if action raised at least one error.
|
170
|
+
# @return [Boolean]
|
171
|
+
def error?
|
172
|
+
!success?
|
173
|
+
end
|
174
|
+
|
175
|
+
# Returns runtime results as JSON.
|
176
|
+
# @return [Array]
|
177
|
+
def to_json(*args)
|
178
|
+
JSON.generate(non_native_results)
|
179
|
+
end
|
180
|
+
|
181
|
+
# Returns runtime results as Saper::Item instances.
|
182
|
+
# @return [Saper::Item, Array<Saper::Item>]
|
183
|
+
def non_native_results
|
184
|
+
multiple? ? non_native_result_array : non_native_result_array.first
|
185
|
+
end
|
186
|
+
|
187
|
+
# Returns runtime results as native Ruby objects.
|
188
|
+
# @return [Object, Array<Object>]
|
189
|
+
def native_results
|
190
|
+
multiple? ? native_result_array : native_result_array.first
|
191
|
+
end
|
192
|
+
|
193
|
+
private
|
194
|
+
|
195
|
+
# Returns runtime results as an array of Saper::Item instances.
|
196
|
+
# @return [Array<Saper::Item>]
|
197
|
+
def non_native_result_array
|
198
|
+
descendants(depth - 1).map(&:output).flatten.compact
|
199
|
+
end
|
200
|
+
|
201
|
+
# Returns runtime results as an array of native Ruby objects.
|
202
|
+
# @return [Array<Object>]
|
203
|
+
def native_result_array
|
204
|
+
non_native_result_array.map { |result| result.is_a?(Item) ? result.to_native : result }
|
205
|
+
end
|
206
|
+
|
207
|
+
# Executes recipes.
|
208
|
+
# @return [self]
|
209
|
+
def execute(stack)
|
210
|
+
@output = input
|
211
|
+
@action = stack.shift
|
212
|
+
if action.nil?
|
213
|
+
return self
|
214
|
+
end
|
215
|
+
unless action.is_a?(Saper::Action)
|
216
|
+
raise ActionExpected, action
|
217
|
+
end
|
218
|
+
begin
|
219
|
+
@output = action.run(input, self)
|
220
|
+
rescue Saper::Error => e
|
221
|
+
@output = nil
|
222
|
+
@error = e
|
223
|
+
end
|
224
|
+
unless @output.is_a?(Array)
|
225
|
+
@output = [@output]
|
226
|
+
end
|
227
|
+
if stack.empty?
|
228
|
+
return self
|
229
|
+
end
|
230
|
+
@children = @output.map do |item|
|
231
|
+
Runtime.new(stack, item, @options.dup)
|
232
|
+
end
|
233
|
+
self
|
234
|
+
end
|
235
|
+
|
236
|
+
end
|
237
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Saper
|
2
|
+
class Type
|
3
|
+
|
4
|
+
# Returns a new Type instance of a given type.
|
5
|
+
# @param type [Symbol]
|
6
|
+
# @return [Saper::Type]
|
7
|
+
def self.[](type)
|
8
|
+
case type
|
9
|
+
when Type then type
|
10
|
+
when :attribute then AttributeType.new
|
11
|
+
when :document then DocumentType.new
|
12
|
+
when :html then HTMLType.new
|
13
|
+
when :json then JSONType.new
|
14
|
+
when :options then OptionsType.new
|
15
|
+
when :regex then RegexType.new
|
16
|
+
when :recipe then RecipeType.new
|
17
|
+
when :tag then TagType.new
|
18
|
+
when :text then TextType.new
|
19
|
+
when :url then URLType.new
|
20
|
+
when :variable then VariableType.new
|
21
|
+
when :xml then XMLType.new
|
22
|
+
when :xpath then XPathType.new
|
23
|
+
else raise(InvalidType, "Invalid action argument: %s" % type)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns normalize value
|
28
|
+
def normalize(value)
|
29
|
+
value # Note: subclass may override this method.
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns `true` if specified value matches type.
|
33
|
+
# @return [Boolean]
|
34
|
+
def valid?(value)
|
35
|
+
true # Note: subclass must override this method.
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns `true` if specified value does not match type.
|
39
|
+
# @return [Boolean]
|
40
|
+
def invalid?(value)
|
41
|
+
not valid?(value)
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Saper
|
2
|
+
module Items
|
3
|
+
class Atom < Item
|
4
|
+
|
5
|
+
def self.new(item)
|
6
|
+
super case item
|
7
|
+
when Hash
|
8
|
+
item
|
9
|
+
else
|
10
|
+
raise(InvalidItem, item)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(hash)
|
15
|
+
@atts = hash
|
16
|
+
end
|
17
|
+
|
18
|
+
def serialize
|
19
|
+
@atts.dup
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_hash
|
23
|
+
@atts.dup
|
24
|
+
end
|
25
|
+
|
26
|
+
def delete(name)
|
27
|
+
@atts.delete(name)
|
28
|
+
end
|
29
|
+
|
30
|
+
def [](name)
|
31
|
+
@atts[name]
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_yaml(*args)
|
35
|
+
@atts.to_yaml(*args)
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_native(object = nil)
|
39
|
+
if object.nil?
|
40
|
+
return to_native(to_hash)
|
41
|
+
end
|
42
|
+
if object.is_a?(Hash)
|
43
|
+
return Hash[object.map { |key, value| [key, to_native(value)] }]
|
44
|
+
end
|
45
|
+
if object.is_a?(Array)
|
46
|
+
return object.map { |key, value| to_native(value) }
|
47
|
+
end
|
48
|
+
if object.is_a?(Item)
|
49
|
+
return object.to_native
|
50
|
+
end
|
51
|
+
object
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_json(*args)
|
55
|
+
to_hash.to_json(*args)
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_s
|
59
|
+
"{ %s }" % @atts.map { |key, value| "%s = %s" % [key, value] }.join(", ")
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Saper
|
2
|
+
module Items
|
3
|
+
class Document < Item
|
4
|
+
|
5
|
+
def self.new(item)
|
6
|
+
super case item
|
7
|
+
when Mechanize::File
|
8
|
+
item
|
9
|
+
else
|
10
|
+
raise(InvalidItem, item)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(mechanize)
|
15
|
+
@mech = mechanize
|
16
|
+
end
|
17
|
+
|
18
|
+
def uri
|
19
|
+
@mech.uri.to_s
|
20
|
+
end
|
21
|
+
|
22
|
+
def body
|
23
|
+
@mech.body
|
24
|
+
end
|
25
|
+
|
26
|
+
def size
|
27
|
+
body.size
|
28
|
+
end
|
29
|
+
|
30
|
+
def mime
|
31
|
+
content_type.split(";").first
|
32
|
+
end
|
33
|
+
|
34
|
+
def charset
|
35
|
+
content_type.include?("charset=") ? content_type.split("charset=").last : nil
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_text
|
39
|
+
body
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_html
|
43
|
+
HTML.new(body)
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_xml
|
47
|
+
XML.new(body)
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_json
|
51
|
+
JSON.new(body)
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_native
|
55
|
+
@body
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def content_type
|
61
|
+
@mech['content-type'] || ""
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Saper
|
2
|
+
module Items
|
3
|
+
class HTML < Item
|
4
|
+
|
5
|
+
def self.new(item)
|
6
|
+
super case item
|
7
|
+
when Nokogiri::XML::Element
|
8
|
+
item
|
9
|
+
when Nokogiri::HTML
|
10
|
+
item
|
11
|
+
when Text
|
12
|
+
parse(item.to_s)
|
13
|
+
when String
|
14
|
+
parse(item)
|
15
|
+
else
|
16
|
+
raise(InvalidItem, item)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.parse(string, uri = nil, charset = nil)
|
21
|
+
begin
|
22
|
+
Nokogiri::HTML.parse(string, uri, charset)
|
23
|
+
rescue
|
24
|
+
raise(InvalidItem, string)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(noko)
|
29
|
+
@noko = noko
|
30
|
+
# Force UTF-8 encoding
|
31
|
+
# https://github.com/sparklemotion/nokogiri/issues/117
|
32
|
+
@noko.document.encoding = 'UTF-8'
|
33
|
+
end
|
34
|
+
|
35
|
+
def name
|
36
|
+
@noko.name
|
37
|
+
end
|
38
|
+
|
39
|
+
def [](name)
|
40
|
+
@noko[name]
|
41
|
+
end
|
42
|
+
|
43
|
+
def find(xpath)
|
44
|
+
find_all(xpath).first
|
45
|
+
end
|
46
|
+
|
47
|
+
def find_all(xpath)
|
48
|
+
@noko.search(xpath).map { |element| HTML.new(element) }
|
49
|
+
end
|
50
|
+
|
51
|
+
def remove_children_with_content(xpath)
|
52
|
+
@noko.search(xpath).each { |item| item.remove }; self
|
53
|
+
end
|
54
|
+
|
55
|
+
def remove_children_preserving_content(xpath)
|
56
|
+
@noko.search(xpath).each { |item| item.replace(item.children) }; self
|
57
|
+
end
|
58
|
+
|
59
|
+
def remove(tag)
|
60
|
+
remove_children_preserving_content(tag)
|
61
|
+
end
|
62
|
+
|
63
|
+
def inner_html
|
64
|
+
@noko.inner_html
|
65
|
+
end
|
66
|
+
|
67
|
+
def inner_text
|
68
|
+
@noko.inner_text
|
69
|
+
end
|
70
|
+
|
71
|
+
def to_markdown
|
72
|
+
Markdown.new self
|
73
|
+
end
|
74
|
+
|
75
|
+
def to_native
|
76
|
+
inner_html
|
77
|
+
end
|
78
|
+
|
79
|
+
def to_s
|
80
|
+
inner_html
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|