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.
Files changed (111) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +126 -0
  4. data/Rakefile +17 -0
  5. data/bin/saper +60 -0
  6. data/lib/lib/json_search.rb +54 -0
  7. data/lib/lib/mechanize.rb +26 -0
  8. data/lib/lib/nokogiri.rb +12 -0
  9. data/lib/saper.rb +37 -0
  10. data/lib/saper/actions/append_with.rb +14 -0
  11. data/lib/saper/actions/convert_to_html.rb +14 -0
  12. data/lib/saper/actions/convert_to_json.rb +14 -0
  13. data/lib/saper/actions/convert_to_markdown.rb +13 -0
  14. data/lib/saper/actions/convert_to_time.rb +15 -0
  15. data/lib/saper/actions/convert_to_xml.rb +14 -0
  16. data/lib/saper/actions/create_atom.rb +18 -0
  17. data/lib/saper/actions/fetch.rb +17 -0
  18. data/lib/saper/actions/find.rb +18 -0
  19. data/lib/saper/actions/find_first.rb +16 -0
  20. data/lib/saper/actions/get_attribute.rb +15 -0
  21. data/lib/saper/actions/get_contents.rb +14 -0
  22. data/lib/saper/actions/get_text.rb +14 -0
  23. data/lib/saper/actions/prepend_with.rb +14 -0
  24. data/lib/saper/actions/remove_after.rb +14 -0
  25. data/lib/saper/actions/remove_before.rb +14 -0
  26. data/lib/saper/actions/remove_matching.rb +14 -0
  27. data/lib/saper/actions/remove_tags.rb +15 -0
  28. data/lib/saper/actions/replace.rb +15 -0
  29. data/lib/saper/actions/run_recipe.rb +24 -0
  30. data/lib/saper/actions/run_recipe_and_save.rb +22 -0
  31. data/lib/saper/actions/save.rb +14 -0
  32. data/lib/saper/actions/select_matching.rb +14 -0
  33. data/lib/saper/actions/set_input.rb +19 -0
  34. data/lib/saper/actions/skip_tags.rb +15 -0
  35. data/lib/saper/actions/split.rb +24 -0
  36. data/lib/saper/arguments/attribute.rb +11 -0
  37. data/lib/saper/arguments/recipe.rb +42 -0
  38. data/lib/saper/arguments/text.rb +11 -0
  39. data/lib/saper/arguments/timezone.rb +11 -0
  40. data/lib/saper/arguments/variable.rb +11 -0
  41. data/lib/saper/arguments/xpath.rb +11 -0
  42. data/lib/saper/core/action.rb +209 -0
  43. data/lib/saper/core/argument.rb +106 -0
  44. data/lib/saper/core/browser.rb +87 -0
  45. data/lib/saper/core/dsl.rb +68 -0
  46. data/lib/saper/core/error.rb +47 -0
  47. data/lib/saper/core/item.rb +70 -0
  48. data/lib/saper/core/keychain.rb +18 -0
  49. data/lib/saper/core/logger.rb +74 -0
  50. data/lib/saper/core/namespace.rb +139 -0
  51. data/lib/saper/core/recipe.rb +134 -0
  52. data/lib/saper/core/runtime.rb +237 -0
  53. data/lib/saper/core/type.rb +45 -0
  54. data/lib/saper/items/atom.rb +64 -0
  55. data/lib/saper/items/document.rb +66 -0
  56. data/lib/saper/items/html.rb +85 -0
  57. data/lib/saper/items/json.rb +67 -0
  58. data/lib/saper/items/markdown.rb +36 -0
  59. data/lib/saper/items/nothing.rb +15 -0
  60. data/lib/saper/items/text.rb +54 -0
  61. data/lib/saper/items/time.rb +42 -0
  62. data/lib/saper/items/url.rb +34 -0
  63. data/lib/saper/items/xml.rb +79 -0
  64. data/lib/saper/version.rb +3 -0
  65. data/spec/actions/append_with_spec.rb +30 -0
  66. data/spec/actions/convert_to_html_spec.rb +24 -0
  67. data/spec/actions/convert_to_json_spec.rb +24 -0
  68. data/spec/actions/convert_to_markdown_spec.rb +24 -0
  69. data/spec/actions/convert_to_time_spec.rb +37 -0
  70. data/spec/actions/convert_to_xml_spec.rb +24 -0
  71. data/spec/actions/create_atom_spec.rb +31 -0
  72. data/spec/actions/fetch_spec.rb +7 -0
  73. data/spec/actions/find_first_spec.rb +7 -0
  74. data/spec/actions/find_spec.rb +7 -0
  75. data/spec/actions/get_attribute_spec.rb +7 -0
  76. data/spec/actions/get_contents.rb +7 -0
  77. data/spec/actions/get_text.rb +7 -0
  78. data/spec/actions/prepend_with_spec.rb +30 -0
  79. data/spec/actions/remove_after.rb +7 -0
  80. data/spec/actions/remove_before.rb +7 -0
  81. data/spec/actions/replace_spec.rb +7 -0
  82. data/spec/actions/run_recipe_and_save_spec.tmp.rb +52 -0
  83. data/spec/actions/run_recipe_spec.tmp.rb +53 -0
  84. data/spec/actions/save_spec.rb +7 -0
  85. data/spec/actions/select_matching_spec.rb +7 -0
  86. data/spec/actions/set_input_spec.rb +7 -0
  87. data/spec/actions/skip_tags_spec.rb +7 -0
  88. data/spec/actions/split_spec.rb +7 -0
  89. data/spec/core/action_spec.rb +151 -0
  90. data/spec/core/argument_spec.rb +79 -0
  91. data/spec/core/browser_spec.rb +7 -0
  92. data/spec/core/dsl_spec.rb +7 -0
  93. data/spec/core/item_spec.rb +7 -0
  94. data/spec/core/keychain_spec.rb +7 -0
  95. data/spec/core/logger_spec.rb +7 -0
  96. data/spec/core/namespace_spec.rb +18 -0
  97. data/spec/core/recipe_spec.rb +81 -0
  98. data/spec/core/runtime_spec.rb +165 -0
  99. data/spec/core/type_spec.rb +7 -0
  100. data/spec/items/atom_spec.rb +7 -0
  101. data/spec/items/document_spec.rb +7 -0
  102. data/spec/items/html_spec.rb +7 -0
  103. data/spec/items/json_spec.rb +7 -0
  104. data/spec/items/markdown_spec.rb +7 -0
  105. data/spec/items/nothing_spec.rb +7 -0
  106. data/spec/items/text_spec.rb +17 -0
  107. data/spec/items/time_spec.rb +7 -0
  108. data/spec/items/url_spec.rb +7 -0
  109. data/spec/items/xml_spec.rb +17 -0
  110. data/spec/spec_helper.rb +22 -0
  111. 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