Skip to content

Commit

Permalink
Add organization license decoder
Browse files Browse the repository at this point in the history
  • Loading branch information
mbj committed Nov 18, 2023
1 parent 1d3fda7 commit c4b01bd
Show file tree
Hide file tree
Showing 12 changed files with 642 additions and 340 deletions.
6 changes: 6 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# v0.11.25 2023-10-18

* Add support for commercial organization wide licenses

[#1404](https://github.com/mbj/mutant/pull/1404)

# v0.11.24 2023-10-10

* Fix restarg and kwrestarg nested mutations.
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
mutant (0.11.24)
mutant (0.11.25)
diff-lcs (~> 1.3)
parser (~> 3.2.2, >= 3.2.2.4)
regexp_parser (~> 2.8.2)
Expand Down
3 changes: 2 additions & 1 deletion lib/mutant.rb
Original file line number Diff line number Diff line change
Expand Up @@ -251,8 +251,9 @@ module Mutant
require 'mutant/range'
require 'mutant/license'
require 'mutant/license/subscription'
require 'mutant/license/subscription/opensource'
require 'mutant/license/subscription/commercial'
require 'mutant/license/subscription/opensource'
require 'mutant/license/subscription/repository'
require 'mutant/segment'
require 'mutant/segment/recorder'
end
Expand Down
2 changes: 1 addition & 1 deletion lib/mutant/license/subscription.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def success
end

def subscription_name
self.class.name.split('::').last.downcase
self.class::SUBSCRIPTION_NAME
end

def failure_message(expected, actual)
Expand Down
96 changes: 65 additions & 31 deletions lib/mutant/license/subscription/commercial.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,50 +4,84 @@ module Mutant
module License
class Subscription
class Commercial < self
class Author
include Anima.new(:email)

alias_method :to_s, :email
public :to_s
end
include AbstractType

def self.from_json(value)
new(licensed: value.fetch('authors').to_set { |email| Author.new(email: email) })
{
'individual' => Individual,
'organization' => Organization
}.fetch(value.fetch('type', 'individual')).from_json(value)
end

def call(world)
candidates = candidates(world)
class Organization < self
SUBSCRIPTION_NAME = 'commercial organization'

if (licensed & candidates).any?
success
else
failure(licensed, candidates)
def self.from_json(value)
new(licensed: value.fetch('repositories').map(&Repository.public_method(:parse)).to_set)
end
end

private
def call(world)
Repository.load_from_git(world).bind(&method(:check_subscription))
end

def candidates(world)
git_author(world).merge(commit_author(world))
end
private

def git_author(world)
capture(world, %w[git config --get user.email])
def check_subscription(actual)
if licensed.any? { |repository| actual.any? { |other| repository.allow?(other) } }
success
else
failure(licensed, actual)
end
end
end

def commit_author(world)
capture(world, %w[git show --quiet --pretty=format:%ae])
end
class Individual < self
SUBSCRIPTION_NAME = 'commercial individual'

def capture(world, command)
world
.capture_stdout(command)
.fmap(&:chomp)
.fmap { |email| Author.new(email: email) }
.fmap { |value| Set.new([value]) }
.from_right { Set.new }
end
class Author
include Anima.new(:email)

alias_method :to_s, :email
public :to_s
end

def self.from_json(value)
new(licensed: value.fetch('authors').to_set { |email| Author.new(email: email) })
end

def call(world)
candidates = candidates(world)

if (licensed & candidates).any?
success
else
failure(licensed, candidates)
end
end

private

def candidates(world)
git_author(world).merge(commit_author(world))
end

def git_author(world)
capture(world, %w[git config --get user.email])
end

def commit_author(world)
capture(world, %w[git show --quiet --pretty=format:%ae])
end

def capture(world, command)
world
.capture_stdout(command)
.fmap(&:chomp)
.fmap { |email| Author.new(email: email) }
.fmap { |value| Set.new([value]) }
.from_right { Set.new }
end
end # Individual
end # Commercial
end # Subscription
end # License
Expand Down
57 changes: 2 additions & 55 deletions lib/mutant/license/subscription/opensource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,63 +4,14 @@ module Mutant
module License
class Subscription
class Opensource < self
class Repository
include Anima.new(:host, :path)

REMOTE_REGEXP = /\A[^\t]+\t(?<url>[^ ]+) \((?:fetch|push)\)\n\z/.freeze
GIT_SSH_REGEXP = %r{\A[^@]+@(?<host>[^:/]+)[:/](?<path>.+?)(?:\.git)?\z}.freeze
GIT_HTTPS_REGEXP = %r{\Ahttps://(?<host>[^/]+)/(?<path>.+?)(?:\.git)?\z}.freeze

private_constant(*constants(false))

def to_s
[host, path].join('/')
end

def self.parse(input)
host, path = *input.split('/', 2).map(&:downcase)
new(host: host, path: path)
end

def self.parse_remote(input)
match = REMOTE_REGEXP.match(input) or
fail "Unmatched remote line: #{input.inspect}"

parse_url(match[:url])
end
private_class_method :parse_remote

def self.parse_url(input)
match = GIT_SSH_REGEXP.match(input) || GIT_HTTPS_REGEXP.match(input)

unless match
fail "Unmatched git remote URL: #{input.inspect}"
end

new(host: match[:host], path: match[:path].downcase)
end
private_class_method :parse_url

def allow?(other)
other.host.eql?(host) && path_match?(other.path)
end

private

def path_match?(other_path)
path.eql?(other_path) || (path.end_with?('/*') && other_path.start_with?(path[..-2]))
end
end # Opensource
SUBSCRIPTION_NAME = 'opensource repository'

def self.from_json(value)
new(licensed: value.fetch('repositories').map(&Repository.public_method(:parse)).to_set)
end

def call(world)
world
.capture_stdout(%w[git remote --verbose])
.fmap(&method(:parse_remotes))
.bind(&method(:check_subscription))
Repository.load_from_git(world).bind(&method(:check_subscription))
end

private
Expand All @@ -73,10 +24,6 @@ def check_subscription(actual)
end
end

def parse_remotes(input)
input.lines.map(&Repository.method(:parse_remote)).to_set
end

end # Opensource
end # Subscription
end # License
Expand Down
72 changes: 72 additions & 0 deletions lib/mutant/license/subscription/repository.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# frozen_string_literal: true

module Mutant
module License
class Subscription
class Repository
include Anima.new(:host, :path)

REMOTE_REGEXP = /\A[^\t]+\t(?<url>[^ ]+) \((?:fetch|push)\)\n\z/.freeze
GIT_SSH_REGEXP = %r{\A[^@]+@(?<host>[^:/]+)[:/](?<path>.+?)(?:\.git)?\z}.freeze
GIT_HTTPS_REGEXP = %r{\Ahttps://(?<host>[^/]+)/(?<path>.+?)(?:\.git)?\z}.freeze
WILDCARD = '/*'
WILDCARD_RANGE = (..-WILDCARD.length).freeze

private_constant(*constants(false))

def to_s
[host, path].join('/')
end

def self.load_from_git(world)
world
.capture_stdout(%w[git remote --verbose])
.fmap(&method(:parse_remotes))
end

def self.parse_remotes(input)
input.lines.map(&method(:parse_remote)).to_set
end
private_class_method :parse_remotes

def self.parse(input)
host, path = *input.split('/', 2).map(&:downcase)
new(host: host, path: path)
end

def self.parse_remote(input)
match = REMOTE_REGEXP.match(input) or
fail "Unmatched remote line: #{input.inspect}"

parse_url(match[:url])
end
private_class_method :parse_remote

def self.parse_url(input)
match = GIT_SSH_REGEXP.match(input) || GIT_HTTPS_REGEXP.match(input)

unless match
fail "Unmatched git remote URL: #{input.inspect}"
end

new(host: match[:host], path: match[:path].downcase)
end
private_class_method :parse_url

def allow?(other)
other.host.eql?(host) && path_match?(other.path)
end

private

def path_match?(other_path)
path.eql?(other_path) || wildcard_match?(path, other_path) || wildcard_match?(other_path, path)
end

def wildcard_match?(left, right)
left.end_with?(WILDCARD) && right.start_with?(left[WILDCARD_RANGE])
end
end # Repository
end # Subscription
end # License
end # Mutant
2 changes: 1 addition & 1 deletion lib/mutant/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@

module Mutant
# Current mutant version
VERSION = '0.11.24'
VERSION = '0.11.25'
end # Mutant
Loading

0 comments on commit c4b01bd

Please sign in to comment.