psychic-runner 0.0.7 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,131 @@
1
+ module Psychic
2
+ class Runner
3
+ module CodeHelper
4
+ class Highlighter
5
+ def initialize(opts)
6
+ require 'rouge'
7
+ @lexer = Rouge::Lexer.find(opts[:language]) || Rouge::Lexer.guess_by_filename(opts[:filename])
8
+ @formatter = opts[:formatter]
9
+ rescue LoadError # rubocop:disable Lint/HandleExceptions
10
+ # No highlighting support
11
+ end
12
+
13
+ def highlight(source)
14
+ if defined?(Rouge)
15
+ Rouge.highlight(source, @lexer, @formatter)
16
+ else
17
+ # No highlighting support
18
+ source
19
+ end
20
+ end
21
+ end
22
+
23
+ class ReStructuredTextHelper
24
+ def self.code_block(source, language)
25
+ buffer = StringIO.new
26
+ buffer.puts ".. code-block:: #{language}"
27
+ indented_source = source.lines.map do|line|
28
+ " #{line}"
29
+ end.join("\n")
30
+ buffer.puts indented_source
31
+ buffer.string
32
+ end
33
+ end
34
+ class MarkdownHelper
35
+ def self.code_block(source, language)
36
+ buffer = StringIO.new
37
+ buffer.puts # I've seen lots of rendering issues without a dividing newline
38
+ buffer.puts "```#{language}"
39
+ buffer.puts source
40
+ buffer.puts '```'
41
+ buffer.puts # Put a dividing newline after as well, to be safe...
42
+ buffer.string
43
+ end
44
+ end
45
+
46
+ def absolute_source_file
47
+ return nil if source_file.nil?
48
+
49
+ if basedir
50
+ File.expand_path source_file, basedir
51
+ else
52
+ source_file
53
+ end
54
+ end
55
+
56
+ def source
57
+ File.read absolute_source_file
58
+ end
59
+
60
+ def source?
61
+ !absolute_source_file.nil?
62
+ end
63
+
64
+ def language
65
+ @language ||= detect_language
66
+ end
67
+
68
+ def detect_language
69
+ File.extname(source_file)
70
+ end
71
+
72
+ def highlighted_code(formatter = 'terminal256')
73
+ Highlighter.new(language: language, filename: absolute_source_file,
74
+ formatter: formatter).highlight(source)
75
+ end
76
+
77
+ def code_block(source_code, language, opts = { format: :markdown })
78
+ case opts[:format]
79
+ when :rst
80
+ ReStructuredTextHelper.code_block source_code, language
81
+ when :markdown
82
+ MarkdownHelper.code_block source_code, language
83
+ else
84
+ fail IllegalArgumentError, "Unknown format: #{format}"
85
+ end
86
+ end
87
+
88
+ # TODO: Segmentation support...
89
+ # def initialize(*args)
90
+ # @segmenter = Polytrix::Documentation::CodeSegmenter.new
91
+ # super
92
+ # end
93
+
94
+ # # Loses proper indentation on comments
95
+ # def snippet_after(matcher)
96
+ # segments = @segmenter.segment(source)
97
+ # buffer = StringIO.new
98
+ # segment = segments.find do |s|
99
+ # doc_segment_content = s.first.join
100
+ # doc_segment_content.match matcher
101
+ # end
102
+ # buffer.print segment[1].join "\n" if segment # return code segment
103
+ # buffer.string
104
+ # end
105
+
106
+ # def snippet_between(before_matcher, after_matcher)
107
+ # segments = @segmenter.segment(source)
108
+ # start_segment = find_segment_index segments, before_matcher
109
+ # end_segment = find_segment_index segments, after_matcher
110
+ # buffer = StringIO.new
111
+ # if start_segment && end_segment
112
+ # segments[start_segment...end_segment].each do |segment|
113
+ # buffer.puts @segmenter.comment(segment[0]) unless segment == segments[start_segment]
114
+ # buffer.puts segment[1].join
115
+ # end
116
+ # end
117
+ # buffer.puts "\n"
118
+ # buffer.string
119
+ # end
120
+
121
+ # private
122
+
123
+ # def find_segment_index(segments, matcher)
124
+ # segments.find_index do |s|
125
+ # doc_segment_content = s.first.join
126
+ # doc_segment_content.match matcher
127
+ # end
128
+ # end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,70 @@
1
+ require 'psychic/runner/code_helper'
2
+
3
+ module Psychic
4
+ class Runner
5
+ class CodeSample < Struct.new(:name, :source_file, :basedir)
6
+ include CodeHelper
7
+ include Psychic::OutputHelper
8
+ # property :name
9
+ # property :basedir
10
+ # property :source_file
11
+
12
+ def token_handler
13
+ # Default token pattern/replacement (used by php-opencloud) should be configurable
14
+ @token_handler ||= RegexpTokenHandler.new(source, /'\{(\w+)\}'/, "'\\1'")
15
+ end
16
+
17
+ def command(runner)
18
+ command = runner.task_for(:run_sample)
19
+ # FIXME: Shouldn't this be relative to runner's cwd?
20
+ # command ||= Psychic::Util.relativize(source_file, runner.cwd)
21
+ command ||= "./#{source_file}"
22
+
23
+ command_params = { sample: name, sample_file: source_file }
24
+ command_params.merge!(@parameters) unless @parameters.nil?
25
+ Psychic::Util.replace_tokens(command, command_params)
26
+ end
27
+
28
+ def to_s(verbose = false)
29
+ build_string do
30
+ status('Sample Name', name)
31
+ display_tokens
32
+ status('Source File', formatted_file_name)
33
+ display_source if verbose
34
+ end
35
+ end
36
+
37
+ def to_path
38
+ # So coercion to Pathname is possible
39
+ source_file.to_s
40
+ end
41
+
42
+ private
43
+
44
+ def display_source
45
+ return unless source?
46
+ status 'Source Code'
47
+ say highlighted_code
48
+ end
49
+
50
+ def display_tokens
51
+ return status 'Tokens', '(None)' if token_handler.tokens.empty?
52
+
53
+ status 'Tokens'
54
+ indent do
55
+ token_handler.tokens.each do | token |
56
+ say "- #{token}"
57
+ end
58
+ end
59
+ end
60
+
61
+ def formatted_file_name
62
+ if source?
63
+ Psychic::Util.relativize(absolute_source_file, Dir.pwd)
64
+ else
65
+ colorize('<No code sample>', :red)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,16 @@
1
+ module Psychic
2
+ class Runner
3
+ module Factories
4
+ class BundlerTaskFactory < MagicTaskFactory
5
+ magic_file 'Gemfile'
6
+ magic_file '.bundle/config'
7
+ magic_env_var 'BUNDLE_GEMFILE'
8
+ register_task_factory
9
+
10
+ task :bootstrap do
11
+ 'bundle install'
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -1,11 +1,11 @@
1
1
  module Psychic
2
2
  class Runner
3
- module Cold
4
- class ShellScriptRunner
3
+ module Factories
4
+ class ShellScriptTaskFactory < MagicTaskFactory
5
5
  include BaseRunner
6
6
  EXTENSIONS = ['.sh', '']
7
7
  magic_file 'scripts/*'
8
- register_runner
8
+ register_task_factory
9
9
 
10
10
  def initialize(opts)
11
11
  super
@@ -14,7 +14,7 @@ module Psychic
14
14
  end
15
15
  end
16
16
 
17
- def [](task_name)
17
+ def task_for(task_name)
18
18
  task = task_name.to_s
19
19
  script = Dir["#{@cwd}/scripts/#{task}{.sh,}"].first
20
20
  if script
@@ -1,6 +1,6 @@
1
1
  module Psychic
2
2
  class Runner
3
- class HotRunner
3
+ class HotReadTaskFactory
4
4
  include BaseRunner
5
5
  def initialize(opts = {})
6
6
  super
@@ -8,8 +8,9 @@ module Psychic
8
8
  @known_tasks = @tasks.keys
9
9
  end
10
10
 
11
- def [](task_name)
12
- @tasks[task_name]
11
+ def task_for(task_name)
12
+ return @tasks[task_name.to_s] if @tasks.include? task_name.to_s
13
+ super
13
14
  end
14
15
  end
15
16
  end
@@ -0,0 +1,95 @@
1
+ module Psychic
2
+ class Runner
3
+ class MagicTaskFactory
4
+ include Psychic::Logger
5
+
6
+ attr_reader :known_tasks, :tasks, :cwd, :env, :hints
7
+
8
+ class << self
9
+ def register_task_factory
10
+ Psychic::Runner::TaskFactoryRegistry.register(self)
11
+ end
12
+
13
+ def magic_file_patterns
14
+ @magic_file_patterns ||= []
15
+ end
16
+
17
+ def magic_file(pattern) # rubocop:disable Style/TrivialAccessors
18
+ magic_file_patterns << pattern
19
+ end
20
+
21
+ def magic_env_vars
22
+ @magic_env_vars ||= []
23
+ end
24
+
25
+ def magic_env_var(var)
26
+ magic_env_vars << var
27
+ end
28
+
29
+ def known_tasks
30
+ @known_tasks ||= []
31
+ end
32
+
33
+ def tasks
34
+ @tasks ||= {}
35
+ end
36
+
37
+ def task(name, &block)
38
+ name = name.to_s
39
+ tasks[name] = block
40
+ known_tasks << name
41
+ end
42
+ end
43
+
44
+ def initialize(opts = {})
45
+ @opts = opts
46
+ init_attr(:cwd) { Dir.pwd }
47
+ init_attr(:known_tasks) { self.class.known_tasks }
48
+ init_attr(:tasks) { self.class.tasks }
49
+ init_attr(:logger) { new_logger }
50
+ init_attr(:env) { ENV.to_hash }
51
+ end
52
+
53
+ def task_for(task_name)
54
+ tasks[task_name] if tasks.include? task_name
55
+ end
56
+
57
+ def build_task(task_name, *_args)
58
+ task_name = task_name.to_s
59
+ task = task_for(task_name)
60
+ task = task.call if task.respond_to? :call
61
+ fail Psychic::Runner::TaskNotImplementedError, task_name if task.nil?
62
+ task
63
+ end
64
+
65
+ def active?
66
+ self.class.magic_file_patterns.each do | pattern |
67
+ return true unless Dir["#{@cwd}/#{pattern}"].empty?
68
+ end
69
+ self.class.magic_env_vars.each do | var |
70
+ return true if ENV[var]
71
+ end
72
+ false
73
+ end
74
+
75
+ def known_task?(task_name)
76
+ known_tasks.include?(task_name.to_s)
77
+ end
78
+
79
+ private
80
+
81
+ def init_attr(var)
82
+ var_name = "@#{var}"
83
+ var_value = @opts[var]
84
+ var_value = yield if var_value.nil? && block_given?
85
+ instance_variable_set(var_name, var_value)
86
+ end
87
+
88
+ def init_attrs(*vars)
89
+ vars.each do | var |
90
+ init_attr var
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,36 @@
1
+ module Psychic
2
+ class Runner
3
+ class SampleFinder
4
+ attr_accessor :hints
5
+
6
+ def initialize(search_dir = Dir.pwd, hints = nil)
7
+ @search_dir = search_dir
8
+ @hints = hints || {}
9
+ end
10
+
11
+ def known_samples
12
+ hints.map do | name, file |
13
+ CodeSample.new(name, file, @search_dir)
14
+ end
15
+ end
16
+
17
+ def find_sample(name)
18
+ file = find_in_hints(name) || Psychic::Util.find_file_by_alias(name, @search_dir)
19
+ CodeSample.new(name, file, @search_dir)
20
+ end
21
+
22
+ # Find multiple samples by a regex or glob pattern
23
+ # def find_samples(pattern)
24
+ # end
25
+
26
+ private
27
+
28
+ def find_in_hints(name)
29
+ hints.each do |k, v|
30
+ return v if k.downcase == name.downcase
31
+ end
32
+ nil
33
+ end
34
+ end
35
+ end
36
+ end
@@ -2,23 +2,14 @@ module Psychic
2
2
  class Runner
3
3
  module SampleRunner
4
4
  def find_sample(code_sample)
5
- find_in_hints(code_sample) || Psychic::Util.find_file_by_alias(code_sample, cwd)
5
+ @sample_finder.find_sample(code_sample)
6
6
  end
7
7
 
8
- def run_sample(code_sample, *args)
9
- sample_file = find_sample(code_sample)
10
- absolute_sample_file = File.expand_path(sample_file, cwd)
8
+ def run_sample(code_sample_name, *args)
9
+ code_sample = find_sample(code_sample_name)
10
+ absolute_sample_file = code_sample.absolute_source_file
11
11
  process_parameters(absolute_sample_file)
12
- command = build_command(code_sample, sample_file)
13
- if command
14
- execute(command, *args)
15
- else
16
- run_sample_file(sample_file)
17
- end
18
- end
19
-
20
- def run_sample_file(sample_file, *args)
21
- execute("./#{sample_file}", *args) # Assuming Bash, but should detect Windows and use PowerShell
12
+ execute(code_sample.command(self), *args)
22
13
  end
23
14
 
24
15
  def process_parameters(sample_file)
@@ -52,7 +43,7 @@ module Psychic
52
43
  end
53
44
 
54
45
  def build_command(code_sample, sample_file)
55
- command = command_for_task('run_sample')
46
+ command = task_for(:run_sample)
56
47
  return nil if command.nil?
57
48
 
58
49
  command_params = { sample: code_sample, sample_file: sample_file }
@@ -80,6 +71,7 @@ module Psychic
80
71
  end
81
72
 
82
73
  def prompt(key)
74
+ value = @parameters[key]
83
75
  if value
84
76
  return value unless @interactive_mode == 'always'
85
77
  new_value = @cli.ask "Please set a value for #{key} (or enter to confirm #{value.inspect}): "
@@ -0,0 +1,31 @@
1
+ module Psychic
2
+ class Runner
3
+ class TaskFactoryRegistry
4
+ include Psychic::Logger
5
+
6
+ BUILT_IN_DIR = File.expand_path('../factories', __FILE__)
7
+
8
+ class << self
9
+ def autoload_task_factories!
10
+ # Load built-in task factories
11
+ Dir["#{BUILT_IN_DIR}/*.rb"].each do |task_factory_file|
12
+ require task_factory_file
13
+ end
14
+ end
15
+
16
+ def task_factory_classes
17
+ @task_factory_classes ||= Set.new
18
+ end
19
+
20
+ def register(klass)
21
+ task_factory_classes.add klass
22
+ end
23
+
24
+ def active_task_factories(opts)
25
+ task_factories = task_factory_classes.map { |k| k.new(opts) }
26
+ task_factories.select(&:active?)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end