shields-badge 1.0.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.
data/SECURITY.md ADDED
@@ -0,0 +1,24 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ | Version | Supported | Enterprise Support |
6
+ |----------|-----------|---------------------------------------|
7
+ | 1.latest | ✅ | [Tidelift Subscription][tidelift-ref] |
8
+
9
+ ### EOL Policy
10
+
11
+ Non-commercial support for the oldest version of Ruby (which itself is going EOL) may be dropped each year in April.
12
+
13
+ ## Reporting a Vulnerability
14
+
15
+ To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security).
16
+ Tidelift will coordinate the fix and disclosure.
17
+
18
+ ## Shields Badge for Enterprise
19
+
20
+ Available as part of the Tidelift Subscription.
21
+
22
+ The maintainers of shields-badge and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source packages you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact packages you use. [Learn more.][tidelift-ref]
23
+
24
+ [tidelift-ref]: https://tidelift.com/subscription/pkg/rubygems-shields-badge?utm_source=rubygems-shields-badge&utm_medium=referral&utm_campaign=enterprise&utm_term=repo
@@ -0,0 +1,11 @@
1
+ module Shields
2
+ class AnchorHrefTemplate
3
+ def initialize(path)
4
+ @path = path
5
+ end
6
+
7
+ def call(**args)
8
+ @path % args
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Shields
2
+ class ApiTemplate
3
+ def initialize(path)
4
+ @path = path
5
+ end
6
+
7
+ def call(**args)
8
+ @path % args
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shields
4
+ module Badge
5
+ module Activity
6
+ # Shield for the number of GitHub commits since the last release
7
+ # See: https://shields.io/badges/git-hub-commits-since-latest-release
8
+ class GithubCommitsSinceLatestRelease < Base
9
+ class PathDto < ::Castkit::DataObject
10
+ enable_plugins :path_plugin
11
+ enable_plugins :github_repo_plugin
12
+ end
13
+
14
+ class QueryDto < ::Castkit::DataObject
15
+ OPTIONS = {
16
+ sort: %w[date semver],
17
+ include_prereleases: %w[true false],
18
+ }
19
+ FILTER_MATCHER = /[*!]/
20
+
21
+ enable_plugins :query_plugin
22
+
23
+ optional do
24
+ string :include_prereleases, ignore_nil: true, validator: ->(v, _options) {
25
+ raise Errors::ValidationError, "invalid option for include_prereleases, must be one of #{OPTIONS[:include_prereleases]}" if v && !OPTIONS[:include_prereleases].include?(v)
26
+ true
27
+ }
28
+ string :sort, ignore_nil: true, validator: ->(v, _options) {
29
+ raise Errors::ValidationError, "invalid option for sort, must be one of #{OPTIONS[:sort]}" if v && !OPTIONS[:sort].include?(v)
30
+ true
31
+ }
32
+ string :filter, ignore_nil: true, validator: ->(v, _options) {
33
+ raise Errors::ValidationError, "invalid option for filter, must use * or ! but was #{v}" if v && !v[FILTER_MATCHER]
34
+ true
35
+ }
36
+ end
37
+ end
38
+
39
+ class << self
40
+ # Formatted string specification template
41
+ # See: https://ruby-doc.org/3.4.1/String.html#method-i-25
42
+ def api_template = "/github/commits-since/%{user}/%{repo}/latest"
43
+
44
+ def anchor_href_template = "https://github.com/%{user}/%{repo}/releases"
45
+
46
+ def label_text_template = "%{user}/%{repo} commits since latest release"
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,90 @@
1
+ require "uri"
2
+
3
+ module Shields
4
+ module Badge
5
+ class Base
6
+ HTTP_METHOD = :get
7
+ BASE_URL = {
8
+ "svg" => "https://img.shields.io",
9
+ "png" => "https://raster.shields.io",
10
+ }
11
+
12
+ class << self
13
+ def label_text
14
+ raise Errors::NotImplemented, "Subclasses must implement #label_text"
15
+ end
16
+
17
+ def img_src_url
18
+ raise Errors::NotImplemented, "Subclasses must implement #img_src_url"
19
+ end
20
+
21
+ def anchor_href_url
22
+ raise Errors::NotImplemented, "Subclasses must implement #anchor_href_url"
23
+ end
24
+ end
25
+
26
+ attr_accessor :path_parameters,
27
+ :query_parameters,
28
+ :image_type,
29
+ :anchor_href,
30
+ :anchor_params,
31
+ :label
32
+
33
+ def initialize(path_parameters: {}, query_parameters: {}, image_type: "svg", **options)
34
+ if path_parameters.nil? || path_parameters.empty?
35
+ path_parameters = extract_top_level_options(options, self.class::PathDto)
36
+ end
37
+ if query_parameters.nil? || query_parameters.empty?
38
+ query_parameters = extract_top_level_options(options, self.class::QueryDto)
39
+ end
40
+ self.path_parameters = self.class::PathDto.new(**path_parameters)
41
+ self.query_parameters = self.class::QueryDto.new(**query_parameters)
42
+ self.image_type = image_type
43
+ self.anchor_href = options.fetch(:anchor_href, nil)
44
+ self.anchor_params = options.fetch(:anchor_params, {})
45
+ self.label = options.fetch(:label, nil)
46
+ end
47
+
48
+ def format(formatter)
49
+ args = formatter.signature.each_with_object({}) { |key, memo| memo[key] = send(key) }
50
+ formatter.call(
51
+ **args,
52
+ )
53
+ end
54
+
55
+ protected
56
+
57
+ def extract_top_level_options(options, dto_klass)
58
+ path_dto_klass = dto_klass
59
+ main_args = path_dto_klass.attributes.keys
60
+ args_for_dto = path_dto_klass.attributes.values.each_with_object(main_args) { |at, arr| arr.concat(at.options[:aliases].map(&:to_sym)) }
61
+ options.select { |k, _v| args_for_dto.include?(k) }
62
+ end
63
+
64
+ # Formatted string specification template
65
+ # See: https://ruby-doc.org/3.4.1/String.html#method-i-25
66
+ def label_text
67
+ label || LabelTextTemplate.new(self.class.label_text_template)
68
+ .call(**path_parameters.to_h)
69
+ end
70
+
71
+ def img_src_url
72
+ URI("#{base_url}#{ApiTemplate.new(self.class.api_template)
73
+ .call(**path_parameters.to_h)}")
74
+ .tap { |uri| uri.query = URI.encode_www_form(query_parameters.to_h) }
75
+ .to_s
76
+ end
77
+
78
+ def anchor_href_url
79
+ anchor_href || AnchorHrefTemplate.new(self.class.anchor_href_template)
80
+ .call(**path_parameters.to_h.merge(anchor_params))
81
+ end
82
+
83
+ def base_url
84
+ raise Errors::Error, "Unknown image type: #{image_type} must be one of #{BASE_URL.keys}" unless BASE_URL.key?(image_type)
85
+
86
+ BASE_URL[image_type]
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shields
4
+ module Badge
5
+ module Build
6
+ # Shield for GitHub Actions CI status
7
+ # See: https://shields.io/badges/git-hub-branch-check-runs
8
+ class GithubBranchCheckRuns < Base
9
+ class PathDto < ::Castkit::DataObject
10
+ enable_plugins :path_plugin
11
+ enable_plugins :github_repo_plugin
12
+ enable_plugins :git_branch_plugin
13
+ end
14
+
15
+ class QueryDto < ::Castkit::DataObject
16
+ enable_plugins :query_plugin
17
+
18
+ optional do
19
+ string :name_filter, ignore_nil: true, aliases: %w(nameFilter)
20
+ end
21
+
22
+ # Calls up to QueryPlugin#camel_case_keys
23
+ def camel_case_keys
24
+ super + [:name_filter]
25
+ end
26
+ end
27
+
28
+ class << self
29
+ # Formatted string specification template
30
+ # See: https://ruby-doc.org/3.4.1/String.html#method-i-25
31
+ def api_template = "/github/check-runs/%{user}/%{repo}/%{branch}"
32
+
33
+ def anchor_href_template = "https://github.com/%{user}/%{repo}/actions"
34
+
35
+ def label_text_template = "%{user}/%{repo} check runs (branch: %{branch})"
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shields
4
+ module Badge
5
+ module CodeCoverage
6
+ # Shield for Coveralls test coverage
7
+ # See: https://shields.io/badges/coveralls
8
+ class Coveralls < Base
9
+ class PathDto < ::Castkit::DataObject
10
+ VCS_TYPES = %w[github bitbucket gitlab].freeze
11
+ enable_plugins :path_plugin
12
+ enable_plugins :github_repo_plugin
13
+
14
+ required do
15
+ string :vcs_type, ignore_nil: true, aliases: %w(vcsType), validator: ->(v, _options) {
16
+ raise Errors::ValidationError, "invalid option for vcs_type, must be one of #{VCS_TYPES}" if v && !VCS_TYPES.include?(v)
17
+ true
18
+ }
19
+ end
20
+ end
21
+
22
+ class QueryDto < ::Castkit::DataObject
23
+ enable_plugins :query_plugin
24
+
25
+ optional do
26
+ string :branch, ignore_nil: true
27
+ end
28
+ end
29
+
30
+ class << self
31
+ # Formatted string specification template
32
+ # See: https://ruby-doc.org/3.4.1/String.html#method-i-25
33
+ def api_template = "/coverallsCoverage/%{vcs_type}/%{user}/%{repo}"
34
+
35
+ def anchor_href_template = "https://github.com/%{user}/%{repo}/actions"
36
+
37
+ def label_text_template = "%{user}/%{repo} test coverage"
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shields
4
+ module Badge
5
+ module Downloads
6
+ # Shield for RubyGem total downloads
7
+ # # See: https://shields.io/badges/gem-download-rank
8
+ class GemDownloadRank < Base
9
+ class PathDto < ::Castkit::DataObject
10
+ PERIODS = %w[rt rd].freeze
11
+ enable_plugins :path_plugin
12
+ required do
13
+ string :gem, ignore_nil: true
14
+ string :period, ignore_nil: true, validator: ->(v, _options) {
15
+ raise Errors::ValidationError, "invalid option for period, must be one of #{PERIODS}" if v && !PERIODS.include?(v)
16
+ true
17
+ }
18
+ end
19
+ end
20
+
21
+ class QueryDto < ::Castkit::DataObject
22
+ enable_plugins :query_plugin
23
+ end
24
+
25
+ class << self
26
+ # Formatted string specification template
27
+ # See: https://ruby-doc.org/3.4.1/String.html#method-i-25
28
+ def api_template = "/gem/%{period}/%{gem}"
29
+
30
+ def anchor_href_template = "https://rubygems.org/gems/%{gem}"
31
+
32
+ def label_text_template = "RubyGems Download Rank"
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shields
4
+ module Badge
5
+ module Downloads
6
+ # Shield for RubyGem daily downloads
7
+ # # See: https://shields.io/badges/gem-total-downloads
8
+ class GemTotalDownloads < Base
9
+ class PathDto < ::Castkit::DataObject
10
+ enable_plugins :path_plugin
11
+ required do
12
+ string :gem, ignore_nil: true
13
+ end
14
+ end
15
+
16
+ class QueryDto < ::Castkit::DataObject
17
+ enable_plugins :query_plugin
18
+ end
19
+
20
+ class << self
21
+ # Formatted string specification template
22
+ # See: https://ruby-doc.org/3.4.1/String.html#method-i-25
23
+ def api_template = "/gem/dt/%{gem}"
24
+
25
+ def anchor_href_template = "https://rubygems.org/gems/%{gem}"
26
+
27
+ def label_text_template = "RubyGems Total Downloads"
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shields
4
+ module Badge
5
+ module Social
6
+ # Shield for GitHub Repo Stars count
7
+ # See: https://shields.io/badges/git-hub-repo-stars
8
+ class GithubRepoStars < Base
9
+ class PathDto < ::Castkit::DataObject
10
+ enable_plugins :path_plugin
11
+ enable_plugins :github_repo_plugin
12
+ end
13
+
14
+ class QueryDto < ::Castkit::DataObject
15
+ enable_plugins :query_plugin
16
+ end
17
+
18
+ class << self
19
+ # Formatted string specification template
20
+ # See: https://ruby-doc.org/3.4.1/String.html#method-i-25
21
+ def api_template = "/github/stars/%{user}/%{repo}"
22
+
23
+ def anchor_href_template = "https:///github.com/%{user}/%{repo}/stargazers"
24
+
25
+ def label_text_template = "GitHub Stars"
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shields
4
+ module Badge
5
+ module Version
6
+ VERSION = "1.0.0"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ # third party gems
4
+ require "castkit"
5
+ require "castkit/plugins"
6
+ require "version_gem"
7
+
8
+ # this gem
9
+ # Template classes for shields.io badge generation
10
+ require_relative "api_template"
11
+ require_relative "anchor_href_template"
12
+ require_relative "label_text_template"
13
+
14
+ # Constants
15
+ require_relative "categories"
16
+
17
+ # Formatters
18
+ require_relative "formatters/markdown"
19
+ require_relative "formatters/image_src_url"
20
+
21
+ # Serializers
22
+ require_relative "serializers/camel_caser"
23
+
24
+ # Plugins
25
+ require_relative "plugins/git_branch_plugin"
26
+ require_relative "plugins/github_repo_plugin"
27
+ require_relative "plugins/query_plugin"
28
+ require_relative "plugins/path_plugin"
29
+
30
+ # Errors
31
+ require_relative "errors/error"
32
+ require_relative "errors/not_implemented"
33
+ require_relative "errors/validation_error"
34
+
35
+ # Core
36
+ require_relative "badge/base"
37
+
38
+ # Version
39
+ require_relative "badge/version"
40
+
41
+ # Badges
42
+ # Individually required as they are registered
43
+ # Or by requiring "shields/register_all"
44
+ module Shields
45
+ module Badge
46
+ SAFE_TO_CLASSIFY = /\A[\p{LOWERCASE-LETTER}\p{DECIMAL-NUMBER}_]{1,511}\Z/
47
+ SAFE_TO_UNDERSCORE = /\A[\p{UPPERCASE-LETTER}\p{LOWERCASE-LETTER}\p{DECIMAL-NUMBER}]{1,256}\Z/
48
+ SUBBER_UNDER = /(\p{UPPERCASE-LETTER})/
49
+ INITIAL_UNDERSCORE = /^_/
50
+
51
+ def register_all
52
+ require_relative "register_all"
53
+ end
54
+ module_function :register_all
55
+
56
+ # Badges will be registered lazily, so that they can be used without loading the entire set.
57
+ def register(klass:)
58
+ raise Errors::Error, "Badge class must be a class, but is #{klass.class}" unless klass.is_a?(Class)
59
+
60
+ klass_name = klass.name.split("::").last
61
+ method_name = classy_to_underscore(klass_name)
62
+ define_singleton_method(method_name) do |**kwargs|
63
+ as = kwargs.delete(:as) || :markdown
64
+ formatter_module = case as.to_sym
65
+ # Please write an rSt formatter and submit a PR!
66
+ # Please write an AsciiDoc formatter and submit a PR!
67
+ # Please write an HTML formatter and submit a PR!
68
+ when :image_src_url, :markdown
69
+ Shields::Formatters.const_get(underscore_to_classy(as))
70
+ else
71
+ raise Errors::Error, "Unknown formatter: #{as}"
72
+ end
73
+ badge = klass.new(**kwargs)
74
+ badge.format(formatter_module)
75
+ end
76
+ end
77
+ module_function :register
78
+
79
+ def register_by_method_name(method_name:, category: nil)
80
+ # send it through the conversion to ensure it is clean
81
+ # IMPORTANT: use the same logic as respond_to_missing?
82
+ klass_name, clean_method = clean_method_name(method_name)
83
+ # This code should be unreachable, but it is here for completeness
84
+ # :nocov:
85
+ return false unless klass_name && (clean_method == method_name.to_s)
86
+ # :nocov:
87
+
88
+ category ||= Categories[clean_method]
89
+ # method_name must also match the file name
90
+ file_parts = ["badge", category, clean_method].compact
91
+ file_extension = File.join(*file_parts)
92
+ begin
93
+ require_relative(file_extension)
94
+
95
+ klass = Kernel.const_get("Shields::Badge::#{underscore_to_classy(category)}::#{klass_name}")
96
+
97
+ return register(klass:)
98
+ rescue LoadError => error
99
+ if error.message.include?(file_extension)
100
+ # rescue is so method_missing, which likely called this method, can do its thing.
101
+ warn("Could not load #{file_extension} for #{klass_name} in #{__FILE__}:#{__LINE__}.\n")
102
+ else
103
+ # This code should be unreachable, but it is here for completeness
104
+ # :nocov:
105
+ raise error
106
+ # :nocov:
107
+ end
108
+ end
109
+ false
110
+ end
111
+ module_function :register_by_method_name
112
+
113
+ def method_missing(method_name, **kwargs, &block)
114
+ super unless register_by_method_name(method_name:)
115
+
116
+ send(method_name, **kwargs)
117
+ end
118
+ module_function :method_missing
119
+
120
+ def respond_to_missing?(method_name, include_private = false)
121
+ klass_name, clean_method = clean_method_name(method_name)
122
+ klass_name && (clean_method == method_name.to_s) || super
123
+ end
124
+ module_function :respond_to_missing?
125
+
126
+ private
127
+
128
+ def clean_method_name(method_name)
129
+ safe_classy = underscore_to_classy(method_name)
130
+ [safe_classy, classy_to_underscore(safe_classy)]
131
+ end
132
+ module_function :clean_method_name
133
+
134
+ def underscore_to_classy(underscored)
135
+ safe = underscored[SAFE_TO_CLASSIFY]
136
+ raise Errors::Error, "Invalid badge name must match #{SAFE_TO_CLASSIFY}: #{safe} (#{safe.class}) != #{underscored[0..510]} (#{underscored.class})" unless safe == underscored.to_s
137
+
138
+ safe.to_s.split("_").map(&:capitalize).join("")
139
+ end
140
+ module_function :underscore_to_classy
141
+
142
+ def classy_to_underscore(string)
143
+ safe = string[SAFE_TO_UNDERSCORE]
144
+ raise Errors::Error, "Invalid badge class must match #{SAFE_TO_UNDERSCORE}: #{safe} (#{safe.class}) != #{string[0..255]} (#{string.class})" unless safe == string.to_s
145
+
146
+ underscored = safe.gsub(SUBBER_UNDER) { "_#{$1}" }
147
+ shifted_leading_underscore = underscored.sub(INITIAL_UNDERSCORE, "")
148
+ shifted_leading_underscore.downcase
149
+ end
150
+ module_function :classy_to_underscore
151
+ end
152
+ end
153
+
154
+ Shields::Badge::Version.class_eval do
155
+ extend VersionGem::Basic
156
+ end
@@ -0,0 +1,17 @@
1
+ module Shields
2
+ module Categories
3
+ CATEGORIES = {
4
+ "github_commits_since_latest_release" => "activity",
5
+ "github_branch_check_runs" => "build",
6
+ "coveralls" => "code_coverage",
7
+ "gem_download_rank" => "downloads",
8
+ "gem_total_downloads" => "downloads",
9
+ "github_repo_stars" => "social",
10
+ }
11
+
12
+ def [](name)
13
+ CATEGORIES[name]
14
+ end
15
+ module_function :[]
16
+ end
17
+ end
@@ -0,0 +1,5 @@
1
+ module Shields
2
+ module Errors
3
+ class Error < StandardError; end
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Shields
2
+ module Errors
3
+ class NotImplemented < Error; end
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Shields
2
+ module Errors
3
+ class ValidationError < Error; end
4
+ end
5
+ end
@@ -0,0 +1,17 @@
1
+ module Shields
2
+ module Formatters
3
+ module ImageSrcUrl
4
+ SIGNATURE = %i(img_src_url)
5
+
6
+ def call(img_src_url:)
7
+ img_src_url
8
+ end
9
+ module_function :call
10
+
11
+ def signature
12
+ SIGNATURE
13
+ end
14
+ module_function :signature
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ module Shields
2
+ module Formatters
3
+ module Markdown
4
+ SIGNATURE = %i(label_text img_src_url anchor_href_url)
5
+
6
+ def call(label_text:, img_src_url:, anchor_href_url:)
7
+ "[![%{label_text}](%{img_src_url})](%{anchor_href_url})" %
8
+ {label_text:, img_src_url:, anchor_href_url:}
9
+ end
10
+ module_function :call
11
+
12
+ def signature
13
+ SIGNATURE
14
+ end
15
+ module_function :signature
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,11 @@
1
+ module Shields
2
+ class LabelTextTemplate
3
+ def initialize(msg)
4
+ @msg = msg
5
+ end
6
+
7
+ def call(**args)
8
+ @msg % args
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ module Shields
2
+ module Plugins
3
+ module GitBranchPlugin
4
+ class << self
5
+ def setup!(klass)
6
+ klass.required do
7
+ klass.string(:branch)
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
14
+
15
+ Castkit.configure do |config|
16
+ config.register_plugin(:git_branch_plugin, Shields::Plugins::GitBranchPlugin)
17
+ end
@@ -0,0 +1,18 @@
1
+ module Shields
2
+ module Plugins
3
+ module GithubRepoPlugin
4
+ class << self
5
+ def setup!(klass)
6
+ klass.required do
7
+ klass.string(:user)
8
+ klass.string(:repo)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ Castkit.configure do |config|
17
+ config.register_plugin(:github_repo_plugin, Shields::Plugins::GithubRepoPlugin)
18
+ end
@@ -0,0 +1,11 @@
1
+ module Shields
2
+ module Plugins
3
+ module PathPlugin
4
+ # Currently, there are no shared behaviors of Path Parameters that would make sense here.
5
+ end
6
+ end
7
+ end
8
+
9
+ Castkit.configure do |config|
10
+ config.register_plugin(:path_plugin, Shields::Plugins::PathPlugin)
11
+ end