semverve 0.1.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.
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ require_relative "error"
6
+ require_relative "formats"
7
+ require_relative "semantic_version"
8
+
9
+ module Semverve
10
+ ##
11
+ # Generates new Ruby version files from Semverve configuration.
12
+ class Generator
13
+ ##
14
+ # Default version used when +VERSION+ is not provided.
15
+ #
16
+ # @return [String]
17
+ DEFAULT_VERSION = "0.1.0"
18
+
19
+ ##
20
+ # Initializes a version-file generator.
21
+ #
22
+ # @param [Semverve::ResolvedConfiguration] configuration
23
+ # @param [#fetch] env
24
+ #
25
+ # @return [Semverve::Generator]
26
+ def initialize(configuration, env = ENV)
27
+ @configuration = configuration
28
+ @env = env
29
+ end
30
+
31
+ ##
32
+ # Generates the configured version file.
33
+ #
34
+ # @return [String]
35
+ def generate
36
+ version = SemanticVersion.parse(env.fetch("VERSION", DEFAULT_VERSION))
37
+ format = Formats.fetch(env.fetch("FORMAT", configuration.format))
38
+ path = configuration.absolute_version_file
39
+
40
+ if File.exist?(path) && !force?
41
+ raise Error, "Version file already exists at #{path}. Set FORCE=true to overwrite it."
42
+ end
43
+
44
+ FileUtils.mkdir_p(File.dirname(path))
45
+ File.write(path, format.generate(version, module_name: configuration.module_name))
46
+
47
+ path
48
+ end
49
+
50
+ private
51
+
52
+ ##
53
+ # Resolved configuration used for generation.
54
+ #
55
+ # @return [Semverve::ResolvedConfiguration]
56
+ attr_reader :configuration, :env
57
+
58
+ ##
59
+ # Whether generation should overwrite an existing version file.
60
+ #
61
+ # @return [Boolean]
62
+ def force?
63
+ env.fetch("FORCE", "false").match?(/\A(true|1|yes)\z/i)
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "error"
4
+
5
+ module Semverve
6
+ ##
7
+ # Framework-specific default configuration adapters.
8
+ module Presets
9
+ ##
10
+ # Returns defaults for a configured preset.
11
+ #
12
+ # @param [Symbol, String, nil] name
13
+ # @param [Semverve::Configuration] configuration
14
+ #
15
+ # @return [Hash]
16
+ def self.defaults_for(name, configuration)
17
+ return {} unless name
18
+
19
+ fetch(name).defaults(configuration)
20
+ end
21
+
22
+ ##
23
+ # Returns a preset adapter.
24
+ #
25
+ # @param [Symbol, String] name
26
+ #
27
+ # @return [#name, #defaults]
28
+ def self.fetch(name)
29
+ case name.to_sym
30
+ when :rails
31
+ Rails.new
32
+ else
33
+ raise Error, "Unknown preset #{name.inspect}. Use :rails."
34
+ end
35
+ end
36
+
37
+ ##
38
+ # Rails application defaults.
39
+ class Rails
40
+ ##
41
+ # Preset name.
42
+ #
43
+ # @return [Symbol]
44
+ def name
45
+ :rails
46
+ end
47
+
48
+ ##
49
+ # Rails-specific Semverve defaults.
50
+ #
51
+ # @param [Semverve::Configuration] _configuration
52
+ #
53
+ # @return [Hash]
54
+ def defaults(_configuration)
55
+ root = rails_root || Dir.pwd
56
+
57
+ {
58
+ format: :simple,
59
+ module_name: rails_module_name || project_module_name(root),
60
+ root: root,
61
+ version_file: File.join("config", "version.rb")
62
+ }
63
+ end
64
+
65
+ private
66
+
67
+ ##
68
+ # Rails.root when Rails is available.
69
+ #
70
+ # @return [String, nil]
71
+ def rails_root
72
+ return unless rails_defined?
73
+ return unless ::Rails.respond_to?(:root)
74
+ return unless ::Rails.root
75
+
76
+ ::Rails.root.to_s
77
+ end
78
+
79
+ ##
80
+ # Application module name inferred from Rails.application.
81
+ #
82
+ # @return [String, nil]
83
+ def rails_module_name
84
+ return unless rails_defined?
85
+ return unless ::Rails.respond_to?(:application)
86
+ return unless ::Rails.application
87
+
88
+ application_class = ::Rails.application.class
89
+ return application_class.module_parent_name if application_class.respond_to?(:module_parent_name)
90
+
91
+ class_name = application_class.name
92
+ return unless class_name&.include?("::")
93
+
94
+ class_name.split("::").first
95
+ end
96
+
97
+ ##
98
+ # Module name fallback based on the project directory.
99
+ #
100
+ # @param [String] root
101
+ #
102
+ # @return [String]
103
+ def project_module_name(root)
104
+ camelize(File.basename(File.expand_path(root)))
105
+ end
106
+
107
+ ##
108
+ # Whether Rails is loaded.
109
+ #
110
+ # @return [Boolean]
111
+ def rails_defined?
112
+ Object.const_defined?(:Rails)
113
+ end
114
+
115
+ ##
116
+ # Converts a project directory into a Ruby module name.
117
+ #
118
+ # @param [String] value
119
+ #
120
+ # @return [String]
121
+ def camelize(value)
122
+ value.split(/[_-]/).map(&:capitalize).join
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "error"
4
+
5
+ module Semverve
6
+ ##
7
+ # Infers gem metadata from explicit configuration or project files.
8
+ class ProjectMetadata
9
+ ##
10
+ # Initializes project metadata inference.
11
+ #
12
+ # @param [Semverve::Configuration] configuration
13
+ #
14
+ # @return [Semverve::ProjectMetadata]
15
+ def initialize(configuration)
16
+ @configuration = configuration
17
+ end
18
+
19
+ ##
20
+ # Gem name from explicit config, the version-file path, or the gemspec.
21
+ #
22
+ # @return [String]
23
+ def gem_name
24
+ explicit_gem_name || configured_version_file_gem_name || gemspec_name
25
+ end
26
+
27
+ ##
28
+ # Ruby module name from explicit config or the resolved gem name.
29
+ #
30
+ # @return [String]
31
+ def module_name
32
+ explicit_module_name || configured_module_name || camelize(gem_name)
33
+ end
34
+
35
+ ##
36
+ # Version-file path from explicit config or the resolved gem name.
37
+ #
38
+ # @return [String]
39
+ def version_file
40
+ configured_version_file || File.join("lib", gem_name, "version.rb")
41
+ end
42
+
43
+ private
44
+
45
+ ##
46
+ # Configuration used for metadata inference.
47
+ #
48
+ # @return [Semverve::Configuration]
49
+ attr_reader :configuration
50
+
51
+ ##
52
+ # Explicitly configured gem name.
53
+ #
54
+ # @return [String, nil]
55
+ def explicit_gem_name
56
+ configuration.gem_name
57
+ end
58
+
59
+ ##
60
+ # Configured gem name after applying presets.
61
+ #
62
+ # @return [String, nil]
63
+ def configured_gem_name
64
+ configuration.resolved_value(:gem_name)
65
+ end
66
+
67
+ ##
68
+ # Explicitly configured Ruby module name.
69
+ #
70
+ # @return [String, nil]
71
+ def explicit_module_name
72
+ configuration.module_name
73
+ end
74
+
75
+ ##
76
+ # Configured Ruby module name after applying presets.
77
+ #
78
+ # @return [String, nil]
79
+ def configured_module_name
80
+ configuration.resolved_value(:module_name)
81
+ end
82
+
83
+ ##
84
+ # Configured version-file path after applying presets.
85
+ #
86
+ # @return [String, nil]
87
+ def configured_version_file
88
+ configuration.resolved_value(:version_file)
89
+ end
90
+
91
+ ##
92
+ # Gem name inferred from the parent directory of the configured version file.
93
+ #
94
+ # @return [String, nil]
95
+ def configured_version_file_gem_name
96
+ return configured_gem_name if configured_gem_name
97
+ return unless configured_version_file
98
+
99
+ File.basename(File.dirname(configured_version_file))
100
+ end
101
+
102
+ ##
103
+ # Gem name extracted from the single gemspec in the project root.
104
+ #
105
+ # @return [String]
106
+ def gemspec_name
107
+ gemspec_file.then { |file| extract_name(file) }
108
+ end
109
+
110
+ ##
111
+ # Single gemspec file in the configured project root.
112
+ #
113
+ # @return [String]
114
+ def gemspec_file
115
+ files = Dir.glob(File.join(configuration.expanded_root, "*.gemspec"))
116
+
117
+ return files.first if files.one?
118
+
119
+ if files.empty?
120
+ raise Error, "Could not infer gem name because no .gemspec file was found. " \
121
+ "Set config.gem_name or config.version_file."
122
+ end
123
+
124
+ raise Error, "Could not infer gem name because multiple .gemspec files were found. " \
125
+ "Set config.gem_name or config.version_file."
126
+ end
127
+
128
+ ##
129
+ # Reads a gemspec and extracts its +spec.name+ assignment.
130
+ #
131
+ # @param [String] file
132
+ #
133
+ # @return [String]
134
+ def extract_name(file)
135
+ content = File.read(file)
136
+ match = content.match(/^\s*\w+\.name\s*=\s*["']([^"']+)["']/)
137
+
138
+ return match[1] if match
139
+
140
+ raise Error, "Could not infer gem name from #{file}. Set config.gem_name."
141
+ end
142
+
143
+ ##
144
+ # Converts a snake-case or kebab-case gem name into a Ruby module name.
145
+ #
146
+ # @param [String] value
147
+ #
148
+ # @return [String]
149
+ def camelize(value)
150
+ value.split(/[_-]/).map(&:capitalize).join
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../semverve"
4
+
5
+ module Semverve
6
+ ##
7
+ # Rails integration for installing Semverve's Rake tasks.
8
+ class Railtie < ::Rails::Railtie
9
+ rake_tasks do
10
+ Semverve.configuration.preset = :rails unless Semverve.configuration.preset
11
+
12
+ require_relative "task"
13
+ Semverve::Task.install
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "error"
4
+
5
+ module Semverve
6
+ ##
7
+ # Value object for a MAJOR.MINOR.PATCH semantic version.
8
+ class SemanticVersion
9
+ include Comparable
10
+
11
+ ##
12
+ # Regular expression for Semverve's supported version format.
13
+ #
14
+ # @return [Regexp]
15
+ PATTERN = /\A(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)\z/
16
+
17
+ ##
18
+ # Major version number.
19
+ #
20
+ # @return [Integer]
21
+ attr_reader :major
22
+
23
+ ##
24
+ # Minor version number.
25
+ #
26
+ # @return [Integer]
27
+ attr_reader :minor
28
+
29
+ ##
30
+ # Patch version number.
31
+ #
32
+ # @return [Integer]
33
+ attr_reader :patch
34
+
35
+ ##
36
+ # Parses a value into a semantic version.
37
+ #
38
+ # @param [#to_s] value
39
+ #
40
+ # @return [Semverve::SemanticVersion]
41
+ def self.parse(value)
42
+ match = value.to_s.match(PATTERN)
43
+
44
+ unless match
45
+ raise Error, "Expected a semantic version in MAJOR.MINOR.PATCH format, got #{value.inspect}."
46
+ end
47
+
48
+ new(
49
+ major: match[:major].to_i,
50
+ minor: match[:minor].to_i,
51
+ patch: match[:patch].to_i
52
+ )
53
+ end
54
+
55
+ ##
56
+ # Initializes a semantic version.
57
+ #
58
+ # @param [Integer] major
59
+ # @param [Integer] minor
60
+ # @param [Integer] patch
61
+ #
62
+ # @return [Semverve::SemanticVersion]
63
+ def initialize(major:, minor:, patch:)
64
+ @major = major
65
+ @minor = minor
66
+ @patch = patch
67
+ end
68
+
69
+ ##
70
+ # Returns a new semantic version with the requested level incremented.
71
+ #
72
+ # @param [Symbol, String] level
73
+ #
74
+ # @return [Semverve::SemanticVersion]
75
+ def increment(level)
76
+ case level.to_sym
77
+ when :major
78
+ self.class.new(major: major + 1, minor: 0, patch: 0)
79
+ when :minor
80
+ self.class.new(major: major, minor: minor + 1, patch: 0)
81
+ when :patch
82
+ self.class.new(major: major, minor: minor, patch: patch + 1)
83
+ else
84
+ raise Error, "Unknown version increment level: #{level.inspect}."
85
+ end
86
+ end
87
+
88
+ ##
89
+ # Compares semantic versions by major, minor, then patch.
90
+ #
91
+ # @param [Semverve::SemanticVersion] other
92
+ #
93
+ # @return [Integer, nil]
94
+ def <=>(other)
95
+ return unless other.is_a?(self.class)
96
+
97
+ to_a <=> other.to_a
98
+ end
99
+
100
+ ##
101
+ # Version as +MAJOR.MINOR.PATCH+.
102
+ #
103
+ # @return [String]
104
+ def to_s
105
+ to_a.join(".")
106
+ end
107
+
108
+ ##
109
+ # Version as +[major, minor, patch]+.
110
+ #
111
+ # @return [Array<Integer>]
112
+ def to_a
113
+ [major, minor, patch]
114
+ end
115
+ end
116
+ end