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,47 @@
1
+ module Saper
2
+
3
+ # Generic exception acting as a parent class for all exceptions in Saper library.
4
+ class Error < Exception; end
5
+
6
+ # FileUnreadable is raised whenever a recipe file cannot be read.
7
+ class FileUnreadable < Error; end
8
+
9
+ # NoAction is raised whenever an implementation of Saper::Action is not found.
10
+ class ActionNotFound < Error; end
11
+
12
+ # NamespaceMissing is raised whenever Saper::Runtime requires a missing namespace.
13
+ class NamespaceMissing < Error; end
14
+
15
+ # NoAction is raised whenever an unsupported object is added to Saper::Recipe.
16
+ class ActionExpected < Error; end
17
+
18
+ # InvalidAction is raised whenever serialized representation of Saper::Action is invalid.
19
+ class InvalidAction < Error; end
20
+
21
+ # InvalidAction is raised whenever serialized representation of Saper::Recipe is invalid.
22
+ class InvalidRecipe < Error; end
23
+
24
+ # InvalidNamespace is raised whenever serialized representation of Saper::Namespace is invalid.
25
+ class InvalidNamespace < Error; end
26
+
27
+ # InvalidType is raised whenever argument type is not recognized.
28
+ class InvalidType < Error; end
29
+
30
+ # InvalidArgument is raised whenever an argument has incorrect type or value.
31
+ class InvalidArgument < Error; end
32
+
33
+ # InvalidInput is raised whenever Saper::Action does not support this type of input.
34
+ class InvalidInput < Error; end
35
+
36
+ # RecipeNotFound is raised whenever action calls a recipe that cannot be found.
37
+ class RecipeNotFound < Error; end
38
+
39
+ # UnreachableURL is raised whenever a connection error occurs.
40
+ class UnreachableURL < Error; end
41
+
42
+ # InvalidItem is raised whenever Saper::Item is initialized with an invalid underlying.
43
+ class InvalidItem < Error; end
44
+
45
+ # @todo
46
+ class RuntimeMissing < Error; end
47
+ end
@@ -0,0 +1,70 @@
1
+ module Saper
2
+ class Item
3
+
4
+ # Tracks subclasses of Saper::Argument.
5
+ # @return [Class]
6
+ def self.inherited(base)
7
+ subclasses[base.type] = base
8
+ end
9
+
10
+ # Returns a hash of subclasses.
11
+ # @return [Hash]
12
+ def self.subclasses
13
+ @subclasses ||= {}
14
+ end
15
+
16
+ # Returns class name as an underscored string.
17
+ # @return [String]
18
+ def self.type
19
+ name.split("::").last.gsub(/([a-z])([A-Z])/,'\1_\2').downcase
20
+ end
21
+
22
+ # Returns a subclass with specified type.
23
+ # @param type [Symbol] action type
24
+ # @return [Saper::Argument]
25
+ def self.[](type)
26
+ subclasses[type.to_s] || raise(InvalidItem, "Invalid action argument: %s" % type)
27
+ end
28
+
29
+ # Returns `true` if there is a subclass with specified type.
30
+ # @param type [Symbol] action type
31
+ # @return [Boolean]
32
+ def self.exists?(type)
33
+ subclasses.keys.include?(type.to_s)
34
+ end
35
+
36
+ # Returns a new instance of Saper::Argument. Returns nil and fails
37
+ # silently if there is an error during initialization.
38
+ # @return [Saper::Argument]
39
+ def self.try(*args, &block)
40
+ begin
41
+ new(*args, &block)
42
+ rescue InvalidItem, ArgumentError
43
+ nil
44
+ end
45
+ end
46
+
47
+ # Returns a new instance of Saper::Argument.
48
+ # @return [Saper::Argument]
49
+ def self.new(*args, &block)
50
+ if self == Item
51
+ self[args.shift].new(*args, &block)
52
+ else
53
+ super(*args, &block)
54
+ end
55
+ end
56
+
57
+ # Returns `true` if other item contains the same data.
58
+ # @return [Boolean]
59
+ def ==(other)
60
+ to_s == other.to_s
61
+ end
62
+
63
+ # Returns class name as a Symbol.
64
+ # @return [Symbol]
65
+ def type
66
+ self.class.type.to_sym
67
+ end
68
+
69
+ end
70
+ end
@@ -0,0 +1,18 @@
1
+ module Saper
2
+ class Keychain
3
+
4
+ # Returns a new keychain instance
5
+ # @return [Saper::Keychain]
6
+ def initialize
7
+ # TODO: not yet implemented
8
+ end
9
+
10
+ # Returns access credentials for the specified service.
11
+ # @param service [String, Symbol] service name
12
+ # @return [Hash, nil]
13
+ def [](service)
14
+ # TODO: not yet implemented
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,74 @@
1
+ module Saper
2
+ class Logger
3
+
4
+ # TODO: requires serious overhaul
5
+
6
+ attr_reader :io
7
+
8
+ def initialize(io = nil)
9
+ @io = io || StringIO.new
10
+ end
11
+
12
+ def download(url)
13
+ io.write "%s %s\r\n" % [bold("Downloading"), url]
14
+ end
15
+
16
+ def runtime(instance, indent = 0)
17
+ io.write bold("Recipe tree\r\n") if indent == 0
18
+ io.write "%s%s\r\n" % [" " * indent, action(instance)]
19
+ instance.children.each do |child|
20
+ runtime(child, indent + 1)
21
+ end
22
+ end
23
+
24
+ def bold(string)
25
+ "\033[1m%s\033[0m" % string
26
+ end
27
+
28
+ def red(string)
29
+ "\033[31m%s\033[0m" % string
30
+ end
31
+
32
+ def green(string)
33
+ "\033[32m%s\033[0m" % string
34
+ end
35
+
36
+ def action(instance)
37
+ "%s %s > %s" % [action_name(instance), action_input(instance), action_output(instance)]
38
+ end
39
+
40
+ def action_input(instance)
41
+ item(instance.input)
42
+ end
43
+
44
+ def action_output(instance)
45
+ item(instance.action.multiple? ? instance.output : instance.output.first)
46
+ end
47
+
48
+ def action_name(instance)
49
+ instance.error? ? red(instance.action.name) : green(instance.action.name)
50
+ end
51
+
52
+ def item(instance)
53
+ case instance
54
+ when Array
55
+ "[%s]" % instance.map { |i| item(i) }.join(",")
56
+ when nil
57
+ 'nil'
58
+ when Items::Document
59
+ 'Document'
60
+ when Items::HTML
61
+ 'HTML'
62
+ when String
63
+ '%s' % instance
64
+ when Items::Text
65
+ '%s' % instance.to_s
66
+ when Items::Atom
67
+ '%s' % instance.to_hash
68
+ else
69
+ instance.class
70
+ end
71
+ end
72
+
73
+ end
74
+ end
@@ -0,0 +1,139 @@
1
+ module Saper
2
+
3
+ def run(file, recipe, input = nil)
4
+ load(file).run(recipe, input)
5
+ end
6
+
7
+ def load(file)
8
+ parse File.read(path)
9
+ end
10
+
11
+ # Namespace is a parsing / storage utility for recipes
12
+ # that were meant to work as a group. It must be initiated
13
+ # for some actions to work properly (e.g. `run_chain`).
14
+ class Namespace
15
+
16
+ # Parses namespace specified as a string or a block.
17
+ # @param string [String] list of recipes
18
+ # @return [Saper::Namespace]
19
+ def self.parse(string = nil, &block)
20
+ instance = DSL.new
21
+ if string.is_a?(String)
22
+ instance.instance_eval(string)
23
+ end
24
+ if block_given?
25
+ instance.instance_eval(&block)
26
+ end
27
+ instance.namespace
28
+ end
29
+
30
+ # Returns a new instance of Saper::Namespace
31
+ # @param data [Hash]
32
+ # @return [Saper::Namespace]
33
+ def self.unserialize(data, &block)
34
+ unless data.is_a?(Hash)
35
+ raise(InvalidNamespace, data)
36
+ end
37
+ unless data[:recipes].is_a?(Array)
38
+ raise(InvalidNamespace, data)
39
+ end
40
+ new do |namespace|
41
+ data[:recipes].each do |recipe|
42
+ namespace << Recipe.unserialize(recipe, namespace)
43
+ end
44
+ if block_given?
45
+ yield namespace
46
+ end
47
+ end
48
+ end
49
+
50
+ # Returns a new Namespace instance
51
+ # @return [Saper::Namespace]
52
+ def initialize
53
+ @recipes = {}
54
+ if block_given?
55
+ yield self
56
+ end
57
+ end
58
+
59
+ # @todo
60
+ def run_by_default(symbol = nil)
61
+ @default ||= symbol
62
+ end
63
+
64
+ # Runs an recipe and returns resulting Saper:Runtime.
65
+ # @param input [Object]
66
+ # @param name [String, Symbol] action name
67
+ # @param options [Hash] options for Runtime instance
68
+ # @return [Saper::Runtime]
69
+ def run(name = nil, input = nil, options ={})
70
+ self[name].run(input, options.merge(:namespace => self))
71
+ end
72
+
73
+ # Returns recipe with specified ID or fails silently if not found.
74
+ # @param id [String, Symbol] recipe ID
75
+ # @return [Saper::Recipe, nil]
76
+ def try(id)
77
+ begin
78
+ self[id]
79
+ rescue
80
+ nil
81
+ end
82
+ end
83
+
84
+ # Returns recipe with specified ID.
85
+ # @param id [String, Symbol] recipe ID
86
+ # @return [Saper::Recipe]
87
+ def [](id)
88
+ if @recipes.key?(id.to_s)
89
+ value = @recipes[id.to_s]
90
+ if value.is_a?(Recipe)
91
+ return value
92
+ end
93
+ if value.respond_to?(:to_recipe)
94
+ return @recipes[id.to_s] = value.to_recipe
95
+ end
96
+ else
97
+ find(id)
98
+ end
99
+ end
100
+
101
+ # Caches recipe instance.
102
+ # @param id [String, Symbol] recipe ID
103
+ # @param recipe [Saper::Recipe]
104
+ # @return [Saper::Recipe]
105
+ def []=(id, recipe)
106
+ unless recipe.is_a?(Saper::Recipe)
107
+ unless recipe.respond_to?(:to_recipe)
108
+ raise ActionExpected
109
+ end
110
+ end
111
+ @recipes[id.to_s] = recipe
112
+ end
113
+
114
+ # Searches for and returns a recipe.
115
+ # @return [Saper::Recipe]
116
+ def find(id)
117
+ raise RecipeNotFound, id
118
+ end
119
+
120
+ # Returns the number of recipes within this namespace.
121
+ # @return [Integer]
122
+ def size
123
+ @recipes.size
124
+ end
125
+
126
+ # Returns a serialized representation of this Namespace.
127
+ # @return [Hash]
128
+ def serialize
129
+ { :recipes => @recipes.values.map(&:serialize) }
130
+ end
131
+
132
+ # Returns a JSON representation of this recipe.
133
+ # @return [String]
134
+ def to_json(*args)
135
+ serialize.to_json(*args)
136
+ end
137
+
138
+ end
139
+ end
@@ -0,0 +1,134 @@
1
+ module Saper
2
+ class Recipe
3
+
4
+ # Returns a new instance of Saper::Recipe
5
+ # @param data [Hash]
6
+ # @return [Saper::Recipe]
7
+ def self.unserialize(data, namespace = nil, &block)
8
+ if data.is_a?(Array)
9
+ return data.map { |item| unserialize(item, namespace) }
10
+ end
11
+ unless data.is_a?(Hash)
12
+ raise(InvalidRecipe, data)
13
+ end
14
+ new(data[:id], :name => data[:name], :namespace => namespace) do |recipe|
15
+ unless data[:actions].nil?
16
+ recipe.push *Action.unserialize(data[:actions], namespace)
17
+ end
18
+ if block_given?
19
+ yield recipe
20
+ end
21
+ end
22
+ end
23
+
24
+ # Parses block and returns a Recipe instance.
25
+ # @param id [Symbol] recipe ID
26
+ # @return [Saper::Recipe]
27
+ def self.parse(id = nil, name = nil, &block)
28
+ DSL::Recipe.parse(id, name, &block)
29
+ end
30
+
31
+ # Returns unique ID of the recipe.
32
+ attr_reader :id
33
+
34
+ # Returns list of recipe actions.
35
+ attr_reader :actions
36
+
37
+ # Returns a new instance of Saper::Recipe
38
+ # @param id [String]
39
+ # @return [Saper::Recipe]
40
+ def initialize(id = nil, options = {})
41
+ @id = id || SecureRandom.hex
42
+ @actions = []
43
+ @options = options
44
+ if block_given?
45
+ yield self
46
+ end
47
+ end
48
+
49
+ # Returns recipe name.
50
+ # @return [String, nil]
51
+ def name
52
+ @options[:name].nil? ? nil : @options[:name].to_s
53
+ end
54
+
55
+ # Returns Saper::Namespace instance.
56
+ # @return [Namespace]
57
+ def namespace
58
+ @options[:namespace].is_a?(Namespace) ? @options[:namespace] : nil
59
+ end
60
+
61
+ # Returns a list of data types required as input.
62
+ # @return [Array<Symbol>]
63
+ def input_required
64
+ empty? ? [] : actions.first.requires
65
+ end
66
+
67
+ # Returns `true` if recipe requires some input.
68
+ # @return [Boolean]
69
+ def input_required?
70
+ !input_required.empty?
71
+ end
72
+
73
+ # Adds a new action to the end of the recipe and returns self.
74
+ # @param action [Saper::Action]
75
+ # @return [self]
76
+ def <<(action)
77
+ if action.is_a?(Action)
78
+ @actions.push(action)
79
+ else
80
+ raise ActionExpected, action
81
+ end
82
+ self
83
+ end
84
+
85
+ # Adds new actions to the end of the recipe and returns self.
86
+ # @param actions [Array<Saper::Action>]
87
+ # @return [self]
88
+ def push(*actions)
89
+ actions.compact.each do |action|
90
+ self << action
91
+ end
92
+ self
93
+ end
94
+
95
+ # Runs recipe and returns Runtime instance.
96
+ # @param input [object] input for the first action
97
+ # @param options [Hash] options for Runtime object
98
+ # @return [Saper::Runtime]
99
+ def run(input = nil, options = {})
100
+ Runtime.new(actions, input, { :namespace => namespace }.merge(options))
101
+ end
102
+
103
+ # Returns `true` if recipe contains no actions.
104
+ # @return [Boolean]
105
+ def empty?
106
+ actions.empty?
107
+ end
108
+
109
+ # Returns `true` if recipe produces multiple results.
110
+ # @return [Boolean]
111
+ def multiple?
112
+ empty? ? false : actions.any?(&:multiple?)
113
+ end
114
+
115
+ # Returns a serialized representation of this Recipe.
116
+ # @return [Hash]
117
+ def serialize
118
+ { :id => id, :name => name, :actions => @actions.map(&:serialize) }
119
+ end
120
+
121
+ # Returns a string representation of this recipe.
122
+ # @return [String]
123
+ def to_string
124
+ "recipe %s do\n%s\nend" % [id.inspect, actions.map(&:to_string).join("\n")]
125
+ end
126
+
127
+ # Returns a JSON representation of this recipe.
128
+ # @return [String]
129
+ def to_json(*args)
130
+ serialize.to_json(*args)
131
+ end
132
+
133
+ end
134
+ end