prick 0.2.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +6 -5
  3. data/Gemfile +4 -1
  4. data/TODO +10 -0
  5. data/doc/prick.txt +114 -0
  6. data/exe/prick +328 -402
  7. data/lib/ext/fileutils.rb +18 -0
  8. data/lib/ext/forward_method.rb +18 -0
  9. data/lib/ext/shortest_path.rb +44 -0
  10. data/lib/prick.rb +20 -10
  11. data/lib/prick/branch.rb +254 -0
  12. data/lib/prick/builder.rb +164 -0
  13. data/lib/prick/cache.rb +34 -0
  14. data/lib/prick/command.rb +19 -11
  15. data/lib/prick/constants.rb +122 -48
  16. data/lib/prick/database.rb +28 -20
  17. data/lib/prick/diff.rb +125 -0
  18. data/lib/prick/exceptions.rb +15 -3
  19. data/lib/prick/git.rb +77 -30
  20. data/lib/prick/head.rb +183 -0
  21. data/lib/prick/migration.rb +40 -200
  22. data/lib/prick/program.rb +493 -0
  23. data/lib/prick/project.rb +523 -351
  24. data/lib/prick/rdbms.rb +4 -13
  25. data/lib/prick/schema.rb +16 -90
  26. data/lib/prick/share.rb +64 -0
  27. data/lib/prick/state.rb +192 -0
  28. data/lib/prick/version.rb +62 -29
  29. data/libexec/strip-comments +33 -0
  30. data/make_releases +48 -345
  31. data/make_schema +10 -0
  32. data/prick.gemspec +14 -23
  33. data/share/diff/diff.after-tables.sql +4 -0
  34. data/share/diff/diff.before-tables.sql +4 -0
  35. data/share/diff/diff.tables.sql +8 -0
  36. data/share/migration/diff.tables.sql +8 -0
  37. data/share/migration/features.yml +6 -0
  38. data/share/migration/migrate.sql +3 -0
  39. data/share/migration/migrate.yml +8 -0
  40. data/share/migration/tables.sql +3 -0
  41. data/share/schema/build.yml +14 -0
  42. data/share/schema/schema.sql +5 -0
  43. data/share/schema/schema/build.yml +3 -0
  44. data/share/schema/schema/prick/build.yml +14 -0
  45. data/share/schema/schema/prick/data.sql +7 -0
  46. data/share/schema/schema/prick/schema.sql +5 -0
  47. data/share/{schemas/prick/schema.sql → schema/schema/prick/tables.sql} +2 -5
  48. data/{file → share/schema/schema/public/.keep} +0 -0
  49. data/share/schema/schema/public/build.yml +14 -0
  50. data/share/schema/schema/public/schema.sql +3 -0
  51. data/test_assorted +192 -0
  52. data/test_feature +112 -0
  53. data/test_refactor +34 -0
  54. data/test_single_dev +83 -0
  55. metadata +43 -68
  56. data/lib/prick/build.rb +0 -376
  57. data/lib/prick/migra.rb +0 -22
  58. data/share/schemas/prick/data.sql +0 -8
data/lib/prick/rdbms.rb CHANGED
@@ -1,11 +1,10 @@
1
1
  require 'prick/command.rb'
2
- require 'prick/ensure.rb'
3
2
 
4
3
  require 'csv'
5
4
 
6
5
  module Prick
7
6
  module Rdbms
8
- extend Ensure
7
+ # extend Ensure
9
8
 
10
9
  ### EXECUTE SQL
11
10
 
@@ -15,7 +14,9 @@ module Prick
15
14
  {
16
15
  echo "set role #{user};"
17
16
  echo "set search_path to public;"
18
- echo "#{sql}"
17
+ cat <<'EOF'
18
+ #{sql}
19
+ EOF
19
20
  } | psql --csv --tuples-only --quiet -v ON_ERROR_STOP=1 -d #{db}
20
21
  )
21
22
  CSV.new(stdout.join("\n")).read
@@ -131,16 +132,6 @@ module Prick
131
132
  data_opt = (data ? "" : "--schema-only")
132
133
  Command.command "pg_dump --no-owner #{data_opt} #{db} | gzip --to-stdout >#{file}"
133
134
  end
134
-
135
- private
136
- @ensure_states = {
137
- exist_user: [:create_user, :drop_user],
138
- exist_database: [
139
- lambda { |this, db, user| this.ensure_state(:exist_user, user) },
140
- lambda { |this, db, user| this.create_database(db, owner: user) },
141
- :drop_database
142
- ]
143
- }
144
135
  end
145
136
  end
146
137
 
data/lib/prick/schema.rb CHANGED
@@ -1,100 +1,26 @@
1
- require "prick/constants.rb"
2
- require "prick/exceptions.rb"
1
+ require "prick/state.rb"
3
2
 
4
3
  module Prick
5
- # TODO: Resolve dependencies by extracting \i includes and then execute them
6
- # in topological order. This means the must be no 'if' constructs in the
7
- # files because otherwise dependencies are not static. The rule is that if a
8
- # file \i includes another file, then the included file can always be execute
9
- # before the current file
10
- #
11
- # Alternatively add some special tags:
12
- #
13
- # -- require prick (meaning include the prick schema)
14
- # -- require public/user-schema (include this file)
15
- #
16
- class Schema
17
- # Enclosing project
18
- attr_reader :project
19
-
20
- # Path to data file
21
- def Schema.data_file() DATA_SQL_PATH end
22
- def data_file() Schema.data_file end
23
-
24
- # Version read from schemas/prick/data.sql
25
- def version()
26
- @version ||= begin
27
- File.open(data_file, "r") { |file|
28
- while !file.eof? && file.gets.chomp != COPY_STMT
29
- ;
30
- end
31
- !file.eof? or raise Fail, "No COPY statement in #{data_file}"
32
- l = file.gets.chomp
33
- a = l.split("\t")[1..].map { |val| val == '\N' ? nil : val }
34
- a.size == FIELDS.size or raise Fail, "Illegal data format in #{data_file}"
35
- custom, major, minor, patch, pre = a[0], *a[1..-2].map { |val| val && val.to_i }
36
- v = Version.new("0.0.0", custom: (custom == '\N' ? nil : custom))
37
- v.major = major
38
- v.minor = minor
39
- v.patch = patch
40
- v.pre = (pre == "null" ? nil : pre)
41
- v
42
- }
43
- end
44
- end
4
+ PRICK_SCHEMA = "prick"
5
+ SCHEMA_NAMES = %w(schema roles types tables data constraints indexes views functions comments grants)
6
+ BUILD_BASE_NAME = "build"
7
+ BUILD_SQL_FILE = BUILD_BASE_NAME + ".sql"
8
+ BUILD_YML_FILE = BUILD_BASE_NAME + ".yml"
45
9
 
46
- # Write version number into schemas/prick/data.sql
47
- def version=(version)
48
- @version = Version.new(version)
49
- File.open(data_file, "w") { |f|
50
- f.puts "--"
51
- f.puts "-- This file is auto-generated by prick(1). Please don't touch"
52
- f.puts COPY_STMT
53
- f.print \
54
- "1\t",
55
- FIELDS[..-2].map { |f| @version.send(f.to_sym) || '\N' }.join("\t"),
56
- "\t#{@version}\n"
57
- f.puts "\\."
58
- }
59
- Git.add(data_file)
60
- @version
61
- end
62
-
63
- def initialize(project)
64
- @project = project
65
- end
66
-
67
- # Path to the schemas directory
68
- def path() "#{project.path}/#{SCHEMA_DIR}" end
10
+ # Note this models the SCHEMA_DIR directory, not a database schema
11
+ class Schema
12
+ def yml_file() SchemaBuilder.yml_file(SCHEMA_DIR) end
69
13
 
70
- def built?(database = project.database)
71
- database.exist? && database.version == version
72
- end
14
+ def version() SchemaVersion.load end
15
+ def version=(version) SchemaVersion.new(version).write end
16
+ def version_file() SchemaVersion.new.path end
73
17
 
74
- # Build a release from the files in schemas/
75
- def build(database = project.database)
76
- files = collect("schema.sql") + collect("data.sql")
77
- Dir.chdir(path) {
78
- files.each { |file| Rdbms.exec_file(database.name, file, user: project.user) }
79
- }
80
- end
18
+ def built?(database) database.exist? && database.version == version end
81
19
 
82
- # Collects instances of `filename` in sub-directories of schemas/
83
- def collect(filename)
84
- Dir.chdir(path) {
85
- if File.exist?(filename)
86
- [filename]
87
- else
88
- Dir["*/#{filename}"]
89
- end
90
- }
20
+ # `subject` can be a subpath of schema/ (ie. 'public/tables')
21
+ def build(database, subject = nil)
22
+ SchemaBuilder.new(database, SCHEMA_DIR).build(subject)
91
23
  end
92
-
93
- private
94
- FIELDS = %w(custom major minor patch pre feature version)
95
- COPY_STMT = "COPY prick.versions (id, #{FIELDS.join(', ')}) FROM stdin;"
96
-
97
- DATA_SQL_PATH = "#{Prick::PRICK_DIR}/data.sql"
98
24
  end
99
25
  end
100
26
 
@@ -0,0 +1,64 @@
1
+
2
+ module Prick
3
+ class Share
4
+ # Procedural object for templating and copying share/ files
5
+ class Copier
6
+ attr_reader :clobber, :templates
7
+
8
+ def initialize(clobber, templates)
9
+ @clobber = clobber
10
+ @templates = templates
11
+ end
12
+
13
+ def cp(from, to)
14
+ if File.directory?(from)
15
+ cp_dir(from, to)
16
+ elsif File.file?(from)
17
+ cp_file(from, to)
18
+ else
19
+ raise Fail, "Can't copy #{from}"
20
+ end
21
+ end
22
+
23
+ def cp_file(from, to)
24
+ if clobber || !File.exist?(to)
25
+ if templates.empty?
26
+ FileUtils.copy_file(from, to)
27
+ else
28
+ File.open(to, "w") { |f|
29
+ File.readlines(from).each { |l|
30
+ templates.each { |key, value| l.gsub!(/\[<#{key}>\]/, value) }
31
+ f.puts l
32
+ }
33
+ }
34
+ end
35
+ [to]
36
+ else
37
+ []
38
+ end
39
+ end
40
+
41
+ def cp_dir(from, to)
42
+ FileUtils.mkdir_p(to)
43
+ Dir.children(from).map { |name|
44
+ cp(File.join(from, name), File.join(to, name))
45
+ }.flatten
46
+ end
47
+ end
48
+
49
+ def self.cp(pattern, dest, clobber: true, templates: {})
50
+ copier = Copier.new(clobber, templates)
51
+ matches = Dir.glob(File.join(SHARE_PATH, pattern))
52
+ if File.directory?(dest)
53
+ matches.map { |from| copier.cp(from, File.join(dest, File.basename(from))) }.flatten
54
+ elsif matches.size == 1
55
+ copier.cp(matches.first, dest)
56
+ elsif matches.size == 0
57
+ []
58
+ else
59
+ raise Internal, "Destination is not a directory: #{destdir}"
60
+ end
61
+ end
62
+ end
63
+ end
64
+
@@ -0,0 +1,192 @@
1
+
2
+ require 'yaml'
3
+
4
+ module Prick
5
+ # General interface for prick state files. Comments are automatically removed
6
+ class PrickFile
7
+ attr_reader :path
8
+ def initialize(path) @path = path end
9
+ def exist?() File.exist?(path) end
10
+ def self.exist?() self.new.exist? end # Require an #initializer with no arguments
11
+
12
+ def self.load(*args) self.new(*args).read end
13
+
14
+ def self.read() raise NotThis end
15
+
16
+ protected
17
+ # Read file from disk or from the given branch or tag
18
+ def general_read(method, branch: nil, tag: nil)
19
+ !(branch && tag) or raise Internal, "Not both of `branch` and `tag` can be defined"
20
+ branch || tag ? Git.send(method, path, branch: branch, tag: tag) : File.open(path, "r").send(method)
21
+ end
22
+
23
+ def do_read(**opts) general_read(:read, **opts) end
24
+ def do_readline(**opts) do_readlines(**opts).first end
25
+ def do_readlines(**opts) general_read(:readlines, **opts).reject { |l| l =~ /^\s*#/ } end
26
+ end
27
+
28
+ # Models the .prick-version file. It contains just one line with the version
29
+ # of prick itself
30
+ class PrickVersion < PrickFile
31
+ def initialize() super PRICK_VERSION_FILE end
32
+
33
+ # Return the version
34
+ def read(branch: nil, tag: nil)
35
+ Version.new(do_readline(branch: branch, tag: tag).chomp.sub(/^prick-/, ""))
36
+ end
37
+
38
+ # Write prick version
39
+ def write(version)
40
+ File.open(path, "w") { |file| file.puts "prick-#{version}" }
41
+ end
42
+ end
43
+
44
+ # Models the schema version that is stored in the data.sql file in the prick
45
+ # schema directory. Note that SchemaVersion caches the version. Use #clear to
46
+ # reset the cache
47
+ class SchemaVersion < PrickFile
48
+ def initialize(version = nil)
49
+ @version = version
50
+ super SCHEMA_VERSION_PATH
51
+ end
52
+
53
+ def clear() @version = nil end
54
+
55
+ def create() raise Internal, "This should not happen" end
56
+
57
+ def read(**opts)
58
+ return @version if @version
59
+ lines = do_readlines
60
+ while !lines.empty?
61
+ line = lines.shift.chomp
62
+ next if line != COPY_STMT
63
+ data = lines.shift.chomp
64
+ a = data.split("\t").map { |val| parse_sql_literal(val) }
65
+ a.size == FIELDS.size + 1 or raise Fail, "Illegal data format in #{path}"
66
+ fork, major, minor, patch, pre, feature, version = *a[1..-1]
67
+ @version = Version.new("0.0.0", fork: fork, feature: feature)
68
+ @version.major = major
69
+ @version.minor = minor
70
+ @version.patch = patch
71
+ @version.pre = pre
72
+ return @version
73
+ end
74
+ raise Fail, "No COPY statement in #{path}"
75
+ end
76
+
77
+ def write(version = @version)
78
+ # puts "Writing #{path}"
79
+ version_string = version.truncate(:pre).to_s
80
+ File.open(path, "w") { |f|
81
+ f.puts "--"
82
+ f.puts "-- This file is auto-generated by prick(1). Please don't touch"
83
+ f.puts COPY_STMT
84
+ f.print \
85
+ "1\t",
86
+ FIELDS[..-2].map { |f| version.send(f.to_sym) || '\N' }.join("\t"),
87
+ "\t#{version_string}\n"
88
+ f.puts "\\."
89
+ }
90
+ Git.add(path)
91
+ version
92
+ end
93
+
94
+ private
95
+ FIELDS = %w(fork major minor patch pre feature version)
96
+ COPY_STMT = "COPY prick.versions (id, #{FIELDS.join(', ')}) FROM stdin;"
97
+
98
+ def parse_sql_literal(s)
99
+ case s
100
+ when '\N', 'null'; nil
101
+ when/^\d+$/; s.to_i
102
+ else
103
+ s
104
+ end
105
+ end
106
+ end
107
+
108
+ # General interface for prick state files in YAML format
109
+ class State < PrickFile
110
+ # `fields` is a Hash from field name (Symbol) to field type (class) where a
111
+ # class can be a class known to YAML (Integer, String, etc.) or Version.
112
+ # The #initialize method generates an accessor methods for each field
113
+ def initialize(path, **fields)
114
+ super(path)
115
+ @fields = fields
116
+ @fields.each_key { |k| self.class.attr_accessor k }
117
+ @loaded = false
118
+ end
119
+
120
+ def create() write end
121
+
122
+ def set(**fields)
123
+ for field, value in fields
124
+ self.send(:"#{field}=", value)
125
+ end
126
+ self
127
+ end
128
+
129
+ def read(branch: nil, tag: nil)
130
+ return self if @loaded
131
+ hash = YAML.load(do_read(branch: branch, tag: tag))
132
+ for field, klass in @fields
133
+ value = hash[field.to_s]
134
+ value = Version.new(value) if klass == Version && !value.nil?
135
+ self.instance_variable_set("@#{field}", value)
136
+ end
137
+ @loaded = true
138
+ self
139
+ end
140
+
141
+ def write(**fields)
142
+ set(**fields)
143
+ hash = @fields.map { |field, klass|
144
+ value = self.send(field)
145
+ value = value.to_s if klass == Version && !value.nil?
146
+ [field.to_s, value]
147
+ }.to_h
148
+ IO.write(@path, YAML.dump(hash))
149
+ raise if @version.is_a?(Array)
150
+ self
151
+ end
152
+ end
153
+
154
+ class MigrationState < State
155
+ def initialize(version: nil, base_version: nil)
156
+ super(PRICK_MIGRATION_PATH, version: Version, base_version: Version)
157
+ set(version: version, base_version: base_version)
158
+ end
159
+ end
160
+
161
+ class HeadState < State
162
+ def initialize(name: nil)
163
+ super(PRICK_HEAD_PATH, name: String)
164
+ set(name: name)
165
+ end
166
+ end
167
+
168
+ class FeatureState < State
169
+ def initialize(feature: nil)
170
+ super(PRICK_FEATURE_PATH, feature: String)
171
+ set(feature: feature)
172
+ end
173
+ end
174
+
175
+ # class FeaturesState < State
176
+ # def initialize(directory, features: [])
177
+ # super(File.join(directory, FEATURES_STATE_FILE), features: Array)
178
+ # set(features: features)
179
+ # end
180
+ # end
181
+ #
182
+
183
+ # Models the .prick-project file that contains the name of the project and
184
+ # the database user
185
+ class ProjectState < State
186
+ def initialize(name: nil, user: nil)
187
+ super(PROJECT_STATE_FILE, name: String, user: String)
188
+ set(name: name, user: user)
189
+ end
190
+ end
191
+
192
+ end
data/lib/prick/version.rb CHANGED
@@ -1,15 +1,17 @@
1
1
 
2
- # Moved to lib/prick.rb to avoid having Gem depend on it
2
+ # "require 'semantic'" is moved to lib/prick.rb to avoid having Gem depend on it
3
3
  # require 'semantic' # https://github.com/jlindsey/semantic
4
4
 
5
5
  # Required by gem
6
6
  module Prick
7
- VERSION = "0.2.0"
7
+ VERSION = "0.7.0"
8
8
  end
9
9
 
10
10
  # Project related code starts here
11
11
  module Prick
12
12
  class Version
13
+ class FormatError < RuntimeError; end
14
+
13
15
  include Comparable
14
16
 
15
17
  PRE_LABEL = "pre"
@@ -18,11 +20,13 @@ module Prick
18
20
  def self.zero() Version.new("0.0.0") end
19
21
  def zero?() self == Version.zero end
20
22
 
21
- # Return true if `string` is a version. If true, it sets the Regex capture
22
- # groups 1-3. See also Constants::VERSION_RE
23
- def self.version?(string) (string =~ VERSION_RE).nil? ? false : true end
23
+ # Return true if `string` is a version
24
+ def self.version?(string)
25
+ string.is_a?(String) or raise Internal, "String expected"
26
+ !(string =~ VERSION_RE).nil?
27
+ end
24
28
 
25
- attr_accessor :custom
29
+ attr_accessor :fork
26
30
  attr_accessor :semver
27
31
  attr_accessor :feature
28
32
 
@@ -35,24 +39,27 @@ module Prick
35
39
  def patch() @semver.patch end
36
40
  def patch=(patch) @semver.patch = patch end
37
41
 
38
- # Return true if this is a custom release
39
- def custom?() !@custom.nil? end
42
+ # Return true if this is a fork release
43
+ def fork?() !@fork.nil? end
40
44
 
41
45
  # Return true if this is a feature release
42
46
  def feature?() !@feature.nil? end
43
47
 
44
- # Return true if this is a release branch
45
- def release?() !feature? end
48
+ # Return true if this is a release branch (and not a prerelease)
49
+ def release?() !feature? && !pre? end
46
50
 
47
51
  # Return true if this is a pre-release
48
52
  def pre?() !@semver.pre.nil? end
53
+ def prerelease?() pre? end
49
54
 
50
55
  # The releases is stored as a String (eg. 'pre.1') in the semantic version
51
56
  # but #pre returns only the Integer number
52
57
  def pre() @semver.pre =~ PRE_RE ? $1.to_i : nil end
58
+ def prerelease() pre end
53
59
 
54
60
  # #pre= expects an integer or nil argument
55
61
  def pre=(pre) @semver.pre = (pre ? "#{PRE_LABEL}.#{pre}" : nil) end
62
+ def prerelease=(pre) self.pre = pre end
56
63
 
57
64
  def dup() Version.new(self) end
58
65
  def clone() Version.new(self) end
@@ -60,37 +67,52 @@ module Prick
60
67
  def eql?(other) self == other end
61
68
  def hash() @semver.hash end
62
69
 
63
- def initialize(version, custom: nil, feature: nil)
70
+ def initialize(version, fork: nil, feature: nil)
64
71
  case version
65
72
  when String
66
- version =~ VERSION_RE
67
- self.class.version?(version) or raise "Expected a version, got #{version.inspect}"
68
- @custom = custom || $1
69
- @semver = Semantic::Version.new($2)
70
- @feature = feature || $3
73
+ version =~ VERSION_RE or raise Version::FormatError, "Expected a version, got #{version.inspect}"
74
+ @fork = fork || $1
75
+ @semver = Semantic::Version.new($3)
76
+ @feature = feature || $4
71
77
  when Semantic::Version
72
- @custom = custom
78
+ @fork = fork
73
79
  @semver = version.dup
74
80
  @feature = feature
75
81
  when Version
76
- @custom = custom || version.custom
82
+ @fork = fork || version.fork
77
83
  @semver = version.semver.dup
78
84
  @feature = feature || version.feature
79
85
  else
80
- raise "Expected a String, Version, or Semantic::Version, got #{version.class}"
86
+ raise Internal, "Expected a String, Version, or Semantic::Version, got #{version.class}"
81
87
  end
82
88
  end
83
89
 
90
+ # Try converting the string `version` into a Version object. Return nil if unsuccessful
91
+ def self.try(version)
92
+ version.is_a?(Version) ? version : (version?(version) ? new(version) : nil)
93
+ end
94
+
95
+ # Parse a branch or tag name into a Version object. Return a [version, tag]
96
+ # tuple where tag is true if name was a tag
97
+ def self.parse(name)
98
+ name =~ VERSION_RE or raise Version::FormatError, "Expected a version, got #{version.inspect}"
99
+ fork, tag, semver, feature = $1, $2, $3, $4
100
+ version = Version.new(semver, fork: fork, feature: feature)
101
+ [version, tag]
102
+ end
84
103
  # `part` can be one of :major, :minor, :patch, or :pre. If pre is undefined, it
85
104
  # is set to `pre_initial_value`
86
105
  def increment(part, pre_initial_value = 1)
106
+ self.dup.increment!(part, pre_initial_value)
107
+ end
108
+
109
+ def increment!(part, pre_initial_value = 1)
87
110
  if part == :pre
88
- v = self.dup
89
- v.pre = (v.pre ? v.pre+1 : pre_initial_value)
90
- v
111
+ self.pre = (self.pre ? self.pre + 1 : pre_initial_value)
91
112
  else
92
- Version.new(semver.increment!(part), custom: custom, feature: feature)
113
+ @semver = semver.increment!(part)
93
114
  end
115
+ self
94
116
  end
95
117
 
96
118
  def truncate(part)
@@ -109,8 +131,18 @@ module Prick
109
131
  end
110
132
  end
111
133
 
134
+ # def path
135
+ # parts = [FEATURE_DIR, truncate(:pre), feature].compact
136
+ # File.join(*parts)
137
+ # end
138
+
139
+ # def link
140
+ # !feature? or raise Internal, "Version #{to_s} is a feature, not a release"
141
+ # File.join(RELEASE_DIR, to_s)
142
+ # end
143
+
112
144
  def <=>(other)
113
- r = (custom || "") <=> (other.custom || "")
145
+ r = (fork || "") <=> (other.fork || "")
114
146
  return r if r != 0
115
147
  r = semver <=> other.semver
116
148
  return r if r != 0
@@ -118,16 +150,17 @@ module Prick
118
150
  return r
119
151
  end
120
152
 
121
- # Render as String
122
- def to_s
123
- (custom ? "#{custom}-" : "") + semver.to_s + (feature ? "_#{feature}" : "")
153
+ # Render as branch
154
+ def to_s(tag: false)
155
+ (fork ? "#{fork}-" : "") + (tag ? "v" : "") + semver.to_s + (feature ? "_#{feature}" : "")
124
156
  end
125
157
 
158
+ # Render as a tag
159
+ def to_tag() to_s(tag: true) end
160
+
126
161
  # Render as string
127
162
  def inspect() to_s end
128
163
  end
129
-
130
- # ZERO = Version.new("0.0.0")
131
164
  end
132
165
 
133
166