tasking 0.2.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 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: []