psychic-runner 0.0.7 → 0.0.8

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.
@@ -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