prick 0.27.2 → 0.28.1

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