prick 0.2.0 → 0.7.0

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