prick 0.27.2 → 0.28.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d567ea292c77fde8d238f35dad37e284917bb54bdcfa044e9916fd771686c82c
4
- data.tar.gz: df8f4285e9d53fa9c907e1c43aa4a587d7b6709cda42540f1e1ba6092caf8bce
3
+ metadata.gz: 10bf0db9a65cd8441d8f17ea9ad055f177d49508f2445ee733722fcc0fb5c6dc
4
+ data.tar.gz: 693a602c5f57b6bdd97cdefa00c69a0c517cd54e4b32e2d10657fa1c8cfb8949
5
5
  SHA512:
6
- metadata.gz: aaca4641608807b55e8154db7d79aff8314b88aade591e0d5fd5920b9ada1984f9ecf07a1098d40a1afd3578dc51915b40bea334bd5eb4b36b33e45bb1d45c25
7
- data.tar.gz: 2e72149f29ca87b21f407491f3bb40e5bdf1bbda10908b5df25ac4f432d3776b2510e77592ad5cda45c6bb55447091cb53709788869f10bbb6a97403685bc28d
6
+ metadata.gz: 1ec3e792b312d572d9eb65feb3c595d94ec944b9854f12c9f16a805dfce24a45d0be6fe923c02fc1bf57c9a3ce0d6b221c28eaeaea5a1ab1a0f044e228a07b5e
7
+ data.tar.gz: a0cc6eb39133fbfc7eef170a173378bd685a3f43315ac633748dc1fafa02589dae100d1fe8f30ab4687a175e3d8a13d0fca793245fbb10fb2297fed5d8e4c659
data/TODO CHANGED
@@ -1,3 +1,4 @@
1
+ o eval vs exit
1
2
  o Make it possible to define (nested) environments
2
3
  o Use standard directory layout
3
4
 
data/exe/prick CHANGED
@@ -1,9 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- #$LOAD_PATH.unshift "/home/clr/prj/shellopts/lib"
4
- #$LOAD_PATH.unshift "/home/clr/prj/pg_graph/lib"
5
- #$LOAD_PATH.unshift "/home/clr/prj/fixture_fox/lib"
6
-
7
3
  require 'bootsnap'
8
4
  begin
9
5
  cache_dir = "/run/user/" + `id -u`.chomp
data/lib/builder/batch.rb CHANGED
@@ -32,8 +32,8 @@ module Prick
32
32
  super {
33
33
  begin
34
34
  sql = []
35
- # A SQL batch allows the first node to be an executable
36
- if nodes.first.is_a?(ExeNode)
35
+ # A SQL batch allows the first node to be an evaluated or executed node
36
+ if nodes.first.is_a?(CommandNode)
37
37
  time "Execute script" do
38
38
  sql = [nodes.first.source]
39
39
  end
data/lib/builder/node.rb CHANGED
@@ -41,7 +41,7 @@ module Prick
41
41
  @source_lines = nil
42
42
  end
43
43
 
44
- def to_s() [path, args].compact.join(" ") end
44
+ def to_s() @to_s ||= [path, args].compact.join(" ") end
45
45
  def inspect() to_s end
46
46
  def dump() puts "#{inspect} (#{schema})" end
47
47
 
@@ -121,41 +121,45 @@ module Prick
121
121
  def self.objects() @@objects end
122
122
  end
123
123
 
124
- class ExeNode < Node
125
- # Using a pipe instead of just executing the command shaves off some
126
- # deciseconds spent starting up bash. It expects the process to read
127
- # database/username from standard input
128
- attr_reader :pipe
124
+ class CommandNode < Node
125
+ alias_method :command, :to_s
126
+
127
+ def filename = File.basename(path)
128
+ def relpath = path.sub(/^#{Dir.getwd}\//, "")
129
129
 
130
130
  def initialize(parent, phase, path, args = nil)
131
+ constrain args, [String]
131
132
  super(parent, phase, :exe, path, args)
132
- @pipe = Command::Pipe.new(to_s, stderr: nil)
133
133
  end
134
134
 
135
135
  def inspect() "#{path} #{(args || []).join(" ")}" end
136
136
 
137
137
  protected
138
- def read_source
138
+ def execute_command
139
139
  begin
140
- pipe.puts [conn.name, conn.user]
141
- sql = pipe.wait
140
+ Command.command(Prick.state.bash_environment, command)
142
141
  rescue Command::Error => ex
143
- message = "prick: Error executing '#{to_s}'\n" + ex.stderr.map { |l| " #{l}" }.join("\n")
142
+ message = "Error executing '#{command}'\n" + ex.stderr.map { |l| " #{l}" }.join("\n")
144
143
  raise Prick::Error.new(message)
145
-
146
- rescue Errno::EPIPE
147
- raise Prick::Error, "Pipe fail in #{path} called from #{parent.path}\n" +
148
- "Tip: Read username/database from standard input"
149
- end
150
- if pipe.status != 0
151
- $stderr.puts pipe.error
152
- raise Prick::Error, "Failed executing #{path} called from #{parent.path}"
153
144
  end
145
+ end
146
+ end
147
+
148
+ class EvalNode < CommandNode
149
+ def read_source
150
+ sql = execute_command
154
151
  @source_lines = 1 + 1 + sql.count("\n")
155
152
  ["set search_path to #{schema}, pg_temp;\n", sql]
156
153
  end
157
154
  end
158
155
 
156
+ class ExecNode < CommandNode
157
+ def read_source
158
+ execute_command
159
+ []
160
+ end
161
+ end
162
+
159
163
  # A build.yml file node
160
164
  class BuildNode < Node
161
165
  def nodes() @nodes ||= init_nodes + decl_nodes + seed_nodes + term_nodes end
@@ -34,6 +34,91 @@ module Prick
34
34
  end
35
35
  end
36
36
 
37
+ # Search for an executable in path
38
+ def find_executable(filename) # ChatGPT
39
+ Prick.state.executable_search_path.split(File::PATH_SEPARATOR).each do |directory|
40
+ path = File.join(directory, filename)
41
+ return path if File.executable?(path)
42
+ end
43
+ nil
44
+ end
45
+
46
+ # Expand $ENVIRONMENT variable in file names. The function implements a
47
+ # hierarchy of environments:
48
+ #
49
+ # production
50
+ # (online)
51
+ # development
52
+ # online
53
+ # offline
54
+ # backend
55
+ # frontend
56
+ # test
57
+ #
58
+ # If an environment doesn't exist in #dir, then the higher level
59
+ # environments are tried in turn. Eg. if the current environment is
60
+ # 'test', the algorithm expands '$ENVIRONMENT' to 'test', 'offline', and
61
+ # 'development' until an existing file is found. If no file is found the
62
+ # the unexpanded value is returned
63
+ #
64
+ # The production environment acts as a online environment but not a
65
+ # development environment. This means it will first search for
66
+ # 'production', then 'online', and then stop without searching for
67
+ # 'development'
68
+ #
69
+ # NOTE: This is a hardcoded feature for an internal project. It is
70
+ # configurable in the next major version
71
+ #
72
+ # TODO: Refactor: dir is not handled well when the filename includes a
73
+ # $PWD variblea
74
+ #
75
+ def expand_filename(dir, filename)
76
+ # Expand variables
77
+ path = expand_variables(filename, Prick.state.bash_environment)
78
+
79
+ # Use only filename as template if expansion is an absolute path
80
+ if path.start_with? "/"
81
+ template = filename
82
+ else
83
+ template = "#{dir}/#{filename}"
84
+ path = "#{dir}/#{path}"
85
+ end
86
+ return path if File.exist?(path)
87
+
88
+ # Iterate through environments
89
+ env = Prick.state.environment.to_s
90
+ while true
91
+ return path if File.exist?(path)
92
+ case env
93
+ when "production"
94
+ file = expand_variables(path, ENVIRONMENT: "online") or return nil
95
+ return file if File.exist? file
96
+ break
97
+ when "development"; break
98
+ when "online"; env = "development"
99
+ when "offline"; env = "development"
100
+ when "backend"; env = "offline"
101
+ when "frontend"; env = "offline"
102
+ when "test"; env = "offline"
103
+ else
104
+ raise Error, "Illegal env: '#{env}'"
105
+ end
106
+ path = expand_variables(template, Prick.state.bash_environment.merge(ENVIRONMENT: env))
107
+ end
108
+
109
+ # Look for executable in PATH
110
+ if filename !~ /\// && (path = find_executable(filename))
111
+ return path
112
+ else
113
+ return nil
114
+ end
115
+ end
116
+
117
+ # Expand $ENVIRONMENT variable
118
+ def expand_string(string)
119
+ expand_variables(string, ENVIRONMENT: Prick.state.environment.to_s, PWD: Dir.getwd)
120
+ end
121
+
37
122
  def parse_directory(parent, dir)
38
123
  build_file = "#{dir}/build.yml".sub(/\/\//, "/")
39
124
  if File.exist? build_file
@@ -62,11 +147,11 @@ module Prick
62
147
  when "seed"; unit.seed_nodes
63
148
  else
64
149
  raise Error, "Illegal key in #{unit.path}: #{key}"
65
- end.concat(Array(value).map { |value| parse_entry(unit, key.to_sym, dir, value) })
150
+ end.concat(Array(value).map { |value| parse_entry(unit, key.to_sym, dir, value) }.compact)
66
151
  end
67
152
  }
68
153
  else
69
- node = parse_entry(unit, :decl, dir, entry)
154
+ node = parse_entry(unit, :decl, dir, entry) or next
70
155
  if node.kind == :fox
71
156
  unit.seed_nodes << node
72
157
  else
@@ -77,58 +162,22 @@ module Prick
77
162
  unit
78
163
  end
79
164
 
80
- # Expand $ENVIRONMENT variable. The function implements a hierarchy of
81
- # environments:
82
- #
83
- # production
84
- # online
85
- # development
86
- # online
87
- # offline
88
- # backend
89
- # frontend
90
- # test
91
- #
92
- # If an environment doesn't exist in #dir, then the higher level
93
- # environments are tried in turn. Eg. if the current environment is
94
- # 'test', the algorithm expands '$ENVIRONMENT' to 'test', 'offline', and
95
- # 'development' until an existing file is found. If no file is found, the
96
- # algorithm returns the unexpanded value
97
- #
98
- def expand_environment(dir, name)
99
- env = Prick.state.environment.to_s
100
- file = name
101
- if !File.exist?("#{dir}/#{file}") && name =~ /^(.*?)\$ENVIRONMENT/
102
- while true
103
- file = expand_variables(name, ENVIRONMENT: env)
104
- break if File.exist? "#{dir}/#{file}"
105
- case env
106
- when "production"; break
107
- when "development"; break
108
- when "online"; env = "development"
109
- when "offline"; env = "development"
110
- when "backend"; env = "offline"
111
- when "frontend"; env = "offline"
112
- when "test"; env = "offline"
113
- else
114
- raise Error, "Illegal env: '#{env}'"
115
- end
116
- end
117
- end
118
- return file
119
- end
120
-
121
- def parse_file_entry(dir, entry)
122
- name = expand_environment(dir, entry)
123
- name.sub!(/\/$/, "")
124
- if name =~ /^(\S+)\s+(.+)$/ # has arguments -> exe
125
- file = $1
126
- args = $2.split
165
+ # Returns path, filename, and an array of arguments. It is an error if
166
+ # the file can't be found unless #optional is true. In that case a nil
167
+ # value is returned
168
+ def parse_file_entry(unit, dir, entry)
169
+ entry = entry.sub(/\/$/, "")
170
+ if entry =~ /^(\S+?)(\?)?(?:\s+(.+))?\s*$/
171
+ command = $1
172
+ optional = !$2.nil?
173
+ args = expand_string($3 || '').split
174
+ path = expand_filename(dir, command)
175
+ path || optional or raise Error, "Can't find '#{entry}' in #{dir}/ from #{unit}"
176
+ !path.nil? or return nil
127
177
  else
128
- file = name
178
+ raise Error, "Not a file name: '#{entry}'"
129
179
  end
130
- path = "#{dir}/#{file}"
131
- [path, name, file, args]
180
+ [path, Array(args).flatten]
132
181
  end
133
182
 
134
183
  def parse_entry(unit, phase, dir, entry)
@@ -137,22 +186,29 @@ module Prick
137
186
  key, value = entry.first
138
187
  case key
139
188
  when "sql"
140
- InlineNode.new(unit, phase, unit.path, value)
141
- when "call"
142
- args = value.split(/\s+/)
143
- args.size >= 3 or raise "Illegal number of arguments: #{value}"
144
- file, klass, command, args = *args
145
- ModuleNode.new(unit, phase, "#{dir}/#{file}", klass, command, args)
189
+ InlineNode.new(unit, phase, unit.path, expand_string(value))
190
+ when "call"
191
+ (path, args = parse_file_entry(unit, dir, value)) or return nil
192
+ args.size >= 2 or raise Error, "Illegal number of arguments"
193
+ klass = args.shift
194
+ command = args.shift
195
+ klass && command or raise "Illegal number of arguments: #{value}"
196
+ ModuleNode.new(unit, phase, path, klass, command, args)
197
+ when "eval"
198
+ (path, args = parse_file_entry(unit, dir, value)) or return nil
199
+ EvalNode.new(unit, phase, path, args)
146
200
  when "exec"
147
- path, name, file, args = parse_file_entry(dir, value)
148
- ExeNode.new(unit, phase, path, args)
201
+ (path, args = parse_file_entry(unit, dir, value)) or return nil
202
+ ExecNode.new(unit, phase, path, args)
149
203
  else
150
204
  raise Error, "Illegal key: #{key}"
151
205
  end
152
206
  else
153
- path, name, file, args = parse_file_entry(dir, entry)
207
+ (path, args = parse_file_entry(unit, dir, entry)) or return nil
154
208
  if File.directory? path
155
209
  parse_directory(unit, path)
210
+ elsif File.executable? path
211
+ ExecNode.new(unit, phase, path, args)
156
212
  elsif File.file? path
157
213
  case path
158
214
  when /\.sql$/
@@ -160,14 +216,11 @@ module Prick
160
216
  when /\.fox$/
161
217
  FoxNode.new(unit, :seed, path)
162
218
  else
163
- if File.executable? path
164
- ExeNode.new(unit, phase, path, args)
165
- else
166
- raise Error, "Unrecognized file type #{File.basename(path)} in #{dir}"
167
- end
219
+ raise Error, "Expected executable, fox, or sql file: #{File.basename(path)} in #{dir}"
168
220
  end
169
221
  else
170
- raise Error, "Can't find #{name} in #{dir} from #{unit}"
222
+ path =
223
+ raise Error, "Can't find '#{entry}' in #{dir}/ from #{unit}"
171
224
  end
172
225
  end
173
226
  end
data/lib/local/command.rb CHANGED
@@ -97,16 +97,15 @@ module Command
97
97
  # command on standard-input, if it is a IO object that IO object is piped to
98
98
  # the command
99
99
  #
100
- # By default #command pass through standard-error but if :stderr is true,
101
- # #command will return a tuple of standard-output, standard-error lines. If
102
- # :stderr is false, standard-error is ignored and is the same as adding
103
- # "2>/dev/null" to the command
100
+ # By default #command pass through stderr but if :stderr is true, #command
101
+ # will instead return a tuple of stdout/stderr lines. If :stderr is false,
102
+ # stderr is ignored and is the same as adding "2>/dev/null" to the command
104
103
  #
105
- # #command raises a Command::Error exception if the command return with an
106
- # exit code != 0 unless :fail is false. In that case the the exit code can be
104
+ # #command raises a Command::Error exception if the command returns with an exit
105
+ # code != 0 unless :fail is false. In that case the the exit code can be
107
106
  # fetched from Command::status
108
107
  #
109
- def command(cmd, stdin: nil, stderr: nil, fail: true)
108
+ def command(env = {}, cmd, stdin: nil, stderr: nil, fail: true)
110
109
  cmd = "set -o errexit\nset -o pipefail\n#{cmd}"
111
110
 
112
111
  pw = IO::pipe # pipe[0] for read, pipe[1] for write
@@ -129,7 +128,8 @@ module Command
129
128
  STDERR.reopen(pe[1])
130
129
  pe[1].close
131
130
 
132
- exec(cmd)
131
+ env
132
+ exec(env.map { |k,v| [k.to_s, v.to_s] }.to_h, cmd)
133
133
  }
134
134
 
135
135
  pw[0].close
@@ -143,15 +143,14 @@ module Command
143
143
  when Array; pw[1].write(stdin.join("\n") + "\n")
144
144
  end
145
145
  pw[1].flush
146
- pw[1].close
147
146
  end
147
+ pw[1].close # Closing standard input so the command doesn't hang on read
148
148
 
149
149
  @status = Process.waitpid2(pid)[1].exitstatus
150
150
 
151
151
  out = pr[0].readlines.map(&:chomp)
152
152
  err = pe[0].readlines.map(&:chomp)
153
153
 
154
- pw[1].close if !stdin
155
154
  pr[0].close
156
155
  pe[0].close
157
156
 
@@ -171,13 +170,6 @@ module Command
171
170
  end
172
171
  end
173
172
 
174
- # Exit status of the last command. FIXME: This is not usable because the
175
- # methods raise an exception if the command exited with anything but 0
176
- def status() @status end
177
-
178
- # Stored exception when #command is called with :fail true
179
- def exception() @exception end
180
-
181
173
  # Like command but returns true if the command exited with the expected
182
174
  # status. Note that it suppresses standard-error by default
183
175
  #
@@ -186,6 +178,12 @@ module Command
186
178
  @status == expect
187
179
  end
188
180
 
181
+ # Exit status of the last command
182
+ def status() @status end
183
+
184
+ # Exception of the last command if it failed, otherwise nil TODO What is this used for?
185
+ def exception() @exception end
186
+
189
187
  module_function :command
190
188
  module_function :status
191
189
  module_function :exception
@@ -14,8 +14,10 @@ module Prick
14
14
  DIRS = [
15
15
  MIGRATION_DIR = "migration",
16
16
  SCHEMA_DIR = "schema",
17
- PRICK_DIR = "#{SCHEMA_DIR}/prick",
17
+ SCHEMA_PRICK_DIR = "#{SCHEMA_DIR}/prick",
18
18
  PUBLIC_DIR = "#{SCHEMA_DIR}/public",
19
+ BIN_DIR = "bin",
20
+ LIBEXEC_DIR = "libexec",
19
21
  VAR_DIR = "var",
20
22
  CACHE_DIR = "#{VAR_DIR}/cache",
21
23
  SPOOL_DIR = "#{VAR_DIR}/spool",
@@ -24,6 +26,12 @@ module Prick
24
26
  SPEC_DIR = "spec"
25
27
  ]
26
28
 
29
+ # Prick project root directory
30
+ PRICK_DIR = Dir.getwd
31
+
32
+ # Search path for executables
33
+ PRICK_PATH = "#{ENV['PATH']}:#{PRICK_DIR}/#{BIN_DIR}:#{PRICK_DIR}/#{LIBEXEC_DIR}"
34
+
27
35
  # Project specification file
28
36
  PRICK_PROJECT_FILE = "prick.yml"
29
37
  PRICK_PROJECT_PATH = PRICK_PROJECT_FILE
@@ -42,7 +50,7 @@ module Prick
42
50
 
43
51
  # Schema data file
44
52
  SCHEMA_VERSION_FILE = "data.sql"
45
- SCHEMA_VERSION_PATH = File.join(PRICK_DIR, SCHEMA_VERSION_FILE)
53
+ SCHEMA_VERSION_PATH = File.join(SCHEMA_PRICK_DIR, SCHEMA_VERSION_FILE)
46
54
 
47
55
  # Rspec temporary directory
48
56
  SPEC_TMP_DIR = "spec"
data/lib/prick/state.rb CHANGED
@@ -36,6 +36,35 @@ module Prick
36
36
  # Name of database user
37
37
  attr_accessor :username
38
38
 
39
+ # Prick project dir. This is not a constant because exe/prick can change directory
40
+ def prick_dir
41
+ @prick_dir ||= Dir.getwd
42
+ end
43
+
44
+ # Prick executable search_path
45
+ def executable_search_path
46
+ @executable_search_path ||= "#{ENV['PATH']}:#{prick_dir}/#{BIN_DIR}:#{prick_dir}/#{LIBEXEC_DIR}"
47
+ end
48
+
49
+ def bash_environment
50
+ ENV.to_h.merge({
51
+ DATABASE: Prick.state.database,
52
+ USERNAME: Prick.state.username,
53
+ ENVIRONMENT: Prick.state.environment.to_s,
54
+ PRICK_DIR: Prick.state.prick_dir,
55
+ PRICK_SCHEMADIR: File.join(Prick.state.prick_dir, SCHEMA_DIR),
56
+ PRICK_BINDIR: File.join(Prick.state.prick_dir, BIN_DIR),
57
+ PRICK_LIBEXECDIR: File.join(Prick.state.prick_dir, LIBEXEC_DIR),
58
+ PRICK_VARDIR: File.join(Prick.state.prick_dir, VAR_DIR),
59
+ PRICK_CACHEDIR: File.join(Prick.state.prick_dir, CACHE_DIR),
60
+ PRICK_SPOOLDIR: File.join(Prick.state.prick_dir, SPOOL_DIR),
61
+ PRICK_TMPDIR: File.join(Prick.state.prick_dir, TMP_DIR),
62
+ PRICK_CLONEDIR: File.join(Prick.state.prick_dir, CLONE_DIR),
63
+ PRICK_SPECDIR: File.join(Prick.state.prick_dir, BIN_DIR),
64
+ PATH: Prick.state.executable_search_path
65
+ })
66
+ end
67
+
39
68
  def self.load
40
69
  begin
41
70
  h = YAML.load(File.read PRICK_PROJECT_PATH)
data/lib/prick/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Prick
4
- VERSION = "0.27.2"
4
+ VERSION = "0.28.1"
5
5
  end
@@ -29,6 +29,7 @@ module Prick::SubCommand
29
29
  if force
30
30
  # Drop all schemas but re-creates the public schema
31
31
  super_conn.rdbms.empty!(database)
32
+ conn.schema.drop("public") # drop it again FIXME
32
33
 
33
34
  else
34
35
  # Find schemas to refresh. This includes all schemas in the
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prick
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.27.2
4
+ version: 0.28.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Claus Rasmussen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-01-06 00:00:00.000000000 Z
11
+ date: 2024-01-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: semantic