tasking 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f3d84bc5f691b364727b241743fb800bf6d571ac3885461124081e959fc6ac78
4
+ data.tar.gz: cd7411408417cd0942ba17e66ce38c3f707b4d76ca4f7171027b07bc670ce2cc
5
+ SHA512:
6
+ metadata.gz: d4efa63e1c58b9dac0997f1ff46ba067bcb724c052bbba2f221fde224f12e4f83983e3b56fab1e1a4c91759cfd76d5fc64f431fb0d45c671878f01ef5b3d7404
7
+ data.tar.gz: 7f25bef3273737f3655809b5af02268c88dc57bbbf1f70e67deca30cf21669584ed6161e02634839f6f0b8b23cbd59975da455fca0126341cd29e144b21ce143
data/README.rdoc ADDED
@@ -0,0 +1,94 @@
1
+ === Running tasks from the command line
2
+ tasking <task_name>
3
+
4
+ === The DSL
5
+ The DSL has the following commands:
6
+ * namespace
7
+ * task
8
+ * options
9
+ * before
10
+ * after
11
+ * execute
12
+
13
+ ==== Namespaces
14
+ Namespaces are used to organize tasks into logical units and isolate options
15
+ from each other. Namespaces can be nested. To reference a nested namespace you
16
+ can use the format "outer_name::inner_name". To reference a namespace with it's
17
+ absolute name, prefix the name with a "::", e.g. "::outer::inner".
18
+
19
+ Namespaces can be re-opened. In that case their content will be merged with the
20
+ original content.
21
+
22
+ ==== Tasks
23
+ Tasks contain commands to be executed. The execution of a task can be modified
24
+ by supplying before- and after-task chains (see the before/after sections
25
+ below).
26
+
27
+ Note that each task _must_ be contained in a namespace. Top-Level tasks will
28
+ raise an error.
29
+
30
+ Opening a task with a name that already exists within the same namespace will
31
+ overwrite the original task.
32
+
33
+ ==== Before/After
34
+ The before and after commands modify the execution chain of a task, by giving a
35
+ list of other tasks that should be executed before and after the main task is
36
+ executed. Note that these filters will be run each time the main task is run.
37
+
38
+ Also, while the main task name can be given relative to the current namespace,
39
+ the tasks in the chain are always interpreted in an absolute fashion.
40
+
41
+ Referencing a task that does not exist will raise an error.
42
+
43
+ Also note that the before and after commands can only be given within the scope
44
+ of a namespace, not within a task.
45
+
46
+ ==== Execute
47
+ aka run, invoke. Used to execute another task from within a task. The only
48
+ command that may be supplied within a task (and only from within a task).
49
+
50
+ The supplied task name will first be looked up in a fashion relative to the
51
+ current tasks parent namespace. If no task is found there, an absolute lookup
52
+ is performed. Will raise an error if a non-existant task is referenced.
53
+
54
+ ==== Options
55
+ Supplies a hash of options that can accessed from within a task. Supplying
56
+ options within nested namespaces will result in the inner namespace merging its
57
+ set of options with the outer namespaces options.
58
+
59
+ Note that the namespace, task and execute commands can also supply an explicit
60
+ hash of options.
61
+
62
+ At the time being, explicitly invoking a task from another task does not
63
+ automatically supply the invoking tasks options to the invoked task.
64
+
65
+ Options that have a value that responds to #call (procs, lambdas, methods) will
66
+ be resolved to the return value of that #call invocation right before a task
67
+ is executed. During the resolving of the options, the original set of
68
+ (unresolved) options is passed as an argument.
69
+
70
+ Option resolution happens every time a task is executed, and the results are
71
+ not persisted or memoized. The resolution happens independently for before
72
+ and after hook tasks from the main task being executed.
73
+
74
+ For example:
75
+
76
+ namespace "outer", :foo => :bar do
77
+ options :bar => :baz
78
+ namespace "inner", :baz => :quux do
79
+ options :quux => :narf
80
+ task "some task" do
81
+ execute "my task", :bla => :blubb
82
+ end
83
+
84
+ task "my task", :narf => :bla do |options|
85
+ # options now has the contents: { :foo => :bar,
86
+ :bar => :baz,
87
+ :baz => :quux,
88
+ :quux => :narf,
89
+ :narf => :bla,
90
+ :bla => :blubb }
91
+ end
92
+ end
93
+ end
94
+
data/bin/tasking ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ require 'lib/tasking'
3
+
4
+ include Tasking
5
+ task_name = ARGV[0]
6
+
7
+ load 'Taskfile'
8
+
9
+ execute task_name
10
+
11
+
@@ -0,0 +1,76 @@
1
+ module Tasking
2
+ class Namespace
3
+ attr_reader :name, :options
4
+
5
+ def self.namespaces
6
+ @namespaces ||= {}
7
+ end
8
+ private_class_method :namespaces
9
+
10
+ def self.all
11
+ namespaces.values
12
+ end
13
+
14
+ def self.add_namespace( ns )
15
+ namespaces[ns.name] = ns
16
+ end
17
+
18
+ def self.find_namespace( name )
19
+ namespaces[name]
20
+ end
21
+
22
+ def self.find_task_in_namespace( ns_name, task_name )
23
+ ns = find_namespace( ns_name )
24
+ ns&.find_task( task_name )
25
+ end
26
+
27
+ def self.find_task( full_name )
28
+ namespace_name, _, task_name = full_name.rpartition( '::' )
29
+
30
+ self.find_task_in_namespace( namespace_name, task_name )
31
+ end
32
+
33
+ def self.find_or_create( name, options = {} )
34
+ find_namespace( name ) || new( name, options )
35
+ end
36
+
37
+ def initialize( name, options = {} )
38
+ @tasks = {}
39
+ @name = name
40
+ @options = options
41
+
42
+ self.class.add_namespace( self )
43
+ end
44
+
45
+ def tasks
46
+ @tasks.values
47
+ end
48
+
49
+ def parent_namespace
50
+ parent_name, _, _ = @name.rpartition( '::' )
51
+
52
+ parent_name.empty? ? nil : self.class.find_namespace( parent_name )
53
+ end
54
+
55
+ def execute( options = {}, &block )
56
+ @options.merge!( options )
57
+ block.call if block
58
+ end
59
+
60
+ def merge_options( options )
61
+ @options.merge!( options )
62
+ end
63
+
64
+ def register_task( task )
65
+ @tasks[task.name] = task
66
+ end
67
+
68
+ def unregister_task( task )
69
+ @tasks.delete( task.name )
70
+ end
71
+
72
+ def find_task( name )
73
+ @tasks[name]
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,49 @@
1
+ module Tasking
2
+ class Task
3
+ attr_reader :name, :options, :block, :before_filters, :after_filters,
4
+ :parent_namespace
5
+
6
+ def initialize( name, parent_namespace, options = {}, &block )
7
+ @name = name
8
+ @parent_namespace = parent_namespace
9
+ @options = options
10
+ @block = block
11
+ @before_filters = []
12
+ @after_filters = []
13
+ end
14
+
15
+ def add_before_filters( *filters )
16
+ @before_filters.concat( filters.flatten )
17
+ end
18
+
19
+ def add_after_filters( *filters )
20
+ @after_filters.concat( filters.flatten )
21
+ end
22
+
23
+ def execute( options = {} )
24
+ total_options = @options.merge( options )
25
+ execute_task_chain( before_filters, total_options, "Unknown before task '%s' for task '#{@name}'" )
26
+ @block.call( resolve_options( total_options ) ) if @block
27
+ execute_task_chain( after_filters, total_options, "Unknown after task '%s' for task '#{@name}'" )
28
+ end
29
+
30
+ private
31
+
32
+ def resolve_options(options)
33
+ options.transform_values { |v| v.respond_to?(:call) ? v.call(options) : v }
34
+ end
35
+
36
+ def execute_task_chain( tasks, options, fail_message )
37
+ tasks.each do |t|
38
+ task = task_lookup( t )
39
+ abort( fail_message % t ) unless task
40
+ task.execute(options)
41
+ end
42
+ end
43
+
44
+ def task_lookup( name )
45
+ name.slice!( 0, 2 ) if name.start_with?( '::' )
46
+ Tasking::Namespace.find_task( name )
47
+ end
48
+ end
49
+ end
data/lib/tasking.rb ADDED
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module Tasking
4
+ def task( name, options = {}, &block )
5
+ abort( "Tasks with empty names are not allowed" ) if name.to_s.empty?
6
+
7
+ full_name = fully_qualified_name( name )
8
+ namespace_name, task_name = split_task_from_namespace( full_name )
9
+
10
+ abort( "Task '#{name}' is not in a namespace" ) if namespace_name.empty?
11
+
12
+ build_namespace_hierarchy( namespace_name )
13
+
14
+ parent_namespace = Tasking::Namespace.find_namespace( namespace_name )
15
+ task = Tasking::Task.new( task_name, parent_namespace, options, &block )
16
+ parent_namespace.register_task( task )
17
+ end
18
+
19
+ def namespace( name, options = {}, &block )
20
+ abort( "Namespaces with empty names are not allowed" ) if name.to_s.empty?
21
+ @__parent_namespace ||= []
22
+
23
+ full_name = fully_qualified_name( name )
24
+ parent_namespace_names, _ = split_task_from_namespace( full_name )
25
+ build_namespace_hierarchy( parent_namespace_names )
26
+
27
+ next_namespace = Tasking::Namespace.find_or_create( full_name, options )
28
+ @__parent_namespace.push( next_namespace )
29
+ next_namespace.execute( options, &block )
30
+ @__parent_namespace.pop
31
+ end
32
+
33
+ def options( options )
34
+ @__parent_namespace.last.merge_options( options )
35
+ end
36
+
37
+ def late_before( task_name, parent_namespace_name, *before_task_names )
38
+ task = Tasking::Namespace.find_task_in_namespace( parent_namespace_name, task_name ) ||
39
+ Tasking::Namespace.find_task( task_name )
40
+ abort( "Unknown task '#{task_name}' in before filter" ) unless task
41
+
42
+ task.add_before_filters( *before_task_names )
43
+ end
44
+
45
+ def late_after( task_name, parent_namespace_name, *after_task_names )
46
+ task = Tasking::Namespace.find_task_in_namespace( parent_namespace_name, task_name ) ||
47
+ Tasking::Namespace.find_task( task_name )
48
+ abort( "Unknown task '#{task_name}' in after filter" ) unless task
49
+
50
+ task.add_after_filters( *after_task_names )
51
+ end
52
+
53
+ def before( task_name, *before_task_names )
54
+ @__late_evaluations ||= {}
55
+ @__late_evaluations[:before] ||= []
56
+ parent_namespace_name = @__parent_namespace.last&.name.to_s
57
+ @__late_evaluations[:before] << [ task_name, parent_namespace_name, before_task_names.flatten ]
58
+ end
59
+
60
+ def after( task_name, *after_task_names )
61
+ @__late_evaluations ||= {}
62
+ @__late_evaluations[:after] ||= []
63
+ parent_namespace_name = @__parent_namespace.last&.name.to_s
64
+ @__late_evaluations[:after] << [ task_name, parent_namespace_name, after_task_names.flatten ]
65
+ end
66
+
67
+ def late_evaluations
68
+ return unless @__late_evaluations
69
+ @__late_evaluations.each_pair do |type, task_parameters|
70
+ task_parameters.each do |( task_name, parent_namespace_name, args )|
71
+ self.send( :"late_#{type}", task_name, parent_namespace_name, *args )
72
+ end
73
+ end
74
+ end
75
+
76
+ def execute( name, options = {} )
77
+ if !@__subsequent_executions
78
+ @__subsequent_executions = true
79
+ late_evaluations
80
+ end
81
+ task = task_lookup( name )
82
+
83
+ if !task
84
+ msg = "Unknown task '#{name}'"
85
+ msg << " or #{fully_qualified_name( name )}" if @__parent_namespace.size > 0
86
+ abort( msg )
87
+ end
88
+
89
+ namespace_hierarchy_options = gather_options_for( name, task )
90
+ namespace_hierarchy_options.merge!( options )
91
+ @__parent_namespace.push( task.parent_namespace )
92
+ task.execute( namespace_hierarchy_options )
93
+ @__parent_namespace.pop
94
+ end
95
+ alias_method :invoke, :execute
96
+ alias_method :run, :execute
97
+
98
+ private
99
+ def task_lookup( name )
100
+ @__parent_namespace ||= []
101
+ task = nil
102
+
103
+ if name.start_with?( '::' )
104
+ name.slice!( 0, 2 )
105
+ return Tasking::Namespace.find_task( name )
106
+ end
107
+
108
+ if @__parent_namespace.last
109
+ full_name = "#{@__parent_namespace.last.name}::#{name}"
110
+ task = Tasking::Namespace.find_task( full_name )
111
+ end
112
+
113
+ task || Tasking::Namespace.find_task( name )
114
+ end
115
+
116
+ def walk_namespace_tree_to( namespace_name, type = :namespace, &block )
117
+ ns_segments = namespace_name.split( '::' )
118
+ ns_segments.pop if type != :namespace
119
+
120
+ current_ns_hierarchy_level = nil
121
+ ns_segments.each do |segment|
122
+ if current_ns_hierarchy_level == nil
123
+ current_ns_hierarchy_level = segment
124
+ else
125
+ current_ns_hierarchy_level += "::#{segment}"
126
+ end
127
+
128
+ block.call( current_ns_hierarchy_level )
129
+ end
130
+ end
131
+
132
+ def gather_options_for( full_task_name, task )
133
+ final_options = {}
134
+
135
+ walk_namespace_tree_to( full_task_name, :task ) do |ns_name|
136
+ namespace = Tasking::Namespace.find_namespace( ns_name )
137
+ final_options.merge!( namespace.options )
138
+ end
139
+
140
+ final_options.merge!( task.options )
141
+ end
142
+
143
+ def fully_qualified_name( name )
144
+ @__parent_namespace&.last ?
145
+ "#{@__parent_namespace.last.name}::#{name}" :
146
+ name
147
+ end
148
+
149
+ def build_namespace_hierarchy( full_name )
150
+ walk_namespace_tree_to( full_name ) do |ns_name|
151
+ Tasking::Namespace.find_or_create( ns_name )
152
+ end
153
+ end
154
+
155
+ def split_task_from_namespace( full_name )
156
+ namespace_name, _, task_name = full_name.rpartition( '::' )
157
+
158
+ [ namespace_name, task_name ]
159
+ end
160
+ end
161
+
162
+ require_relative 'tasking/namespace'
163
+ require_relative 'tasking/task'
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tasking
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Sven Riedel
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-08-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec-its
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: byebug
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: A lightweight DSL for task definition and execution
56
+ email: sr@gimp.org
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - README.rdoc
62
+ - bin/tasking
63
+ - lib/tasking.rb
64
+ - lib/tasking/namespace.rb
65
+ - lib/tasking/task.rb
66
+ homepage: https://github.com/sriedel/tasking
67
+ licenses:
68
+ - GPL-2.0
69
+ metadata: {}
70
+ post_install_message:
71
+ rdoc_options: []
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ requirements: []
85
+ rubygems_version: 3.0.3
86
+ signing_key:
87
+ specification_version: 4
88
+ summary: A lightweight task runner DSL
89
+ test_files: []