saper 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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