Skip to content

Dependency cli parsing #673

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
43 changes: 43 additions & 0 deletions spec/unit/dependency_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,49 @@ module Shards
parse_dependency({foo: {git: "", tag: "rc-1.0"}}).to_s.should eq("foo (tag rc-1.0)")
parse_dependency({foo: {git: "", commit: "4478d8afe8c728f44b47d3582a270423cd7fc07d"}}).to_s.should eq("foo (commit 4478d8a)")
end

it ".parts_from_cli" do
# GitHub short syntax
Dependency.parts_from_cli("github:foo/bar").should eq({resolver_key: "github", source: "foo/bar", requirement: Any})
Dependency.parts_from_cli("github:Foo/[email protected]").should eq({resolver_key: "github", source: "Foo/Bar", requirement: VersionReq.new("~> 1.2.3")})

# GitHub urls
Dependency.parts_from_cli("https://github.com/foo/bar").should eq({resolver_key: "github", source: "foo/bar", requirement: Any})
Dependency.parts_from_cli("https://github.com/Foo/Bar/commit/000000").should eq({resolver_key: "github", source: "Foo/Bar", requirement: GitCommitRef.new("000000")})
Dependency.parts_from_cli("https://github.com/Foo/Bar/tree/v1.2.3").should eq({resolver_key: "github", source: "Foo/Bar", requirement: GitTagRef.new("v1.2.3")})
Dependency.parts_from_cli("https://github.com/Foo/Bar/tree/some/branch").should eq({resolver_key: "github", source: "Foo/Bar", requirement: GitBranchRef.new("some/branch")})

# GitLab short syntax
Dependency.parts_from_cli("gitlab:foo/bar").should eq({resolver_key: "gitlab", source: "foo/bar", requirement: Any})

# GitLab urls
Dependency.parts_from_cli("https://gitlab.com/foo/bar").should eq({resolver_key: "gitlab", source: "foo/bar", requirement: Any})

# Bitbucket short syntax
Dependency.parts_from_cli("bitbucket:foo/bar").should eq({resolver_key: "bitbucket", source: "foo/bar", requirement: Any})

# bitbucket urls
Dependency.parts_from_cli("https://bitbucket.com/foo/bar").should eq({resolver_key: "bitbucket", source: "foo/bar", requirement: Any})

# Git convenient syntax since resolver matches scheme
Dependency.parts_from_cli("git://git.example.org/crystal-library.git").should eq({resolver_key: "git", source: "git://git.example.org/crystal-library.git", requirement: Any})

# Local paths
local_absolute = File.join(tmp_path, "local")
local_relative = File.join("spec", ".repositories", "local") # rel_path is relative to integration spec
Dir.mkdir_p(local_absolute)

# Path short syntax
Dependency.parts_from_cli(local_absolute).should eq({resolver_key: "path", source: local_absolute, requirement: Any})
Dependency.parts_from_cli(local_relative).should eq({resolver_key: "path", source: local_relative, requirement: Any})

# Path resolver syntax
Dependency.parts_from_cli("path:#{local_absolute}").should eq({resolver_key: "path", source: local_absolute, requirement: Any})
Dependency.parts_from_cli("path:#{local_relative}").should eq({resolver_key: "path", source: local_relative, requirement: Any})

# Other resolvers short
Dependency.parts_from_cli("git:git://git.example.org/crystal-library.git").should eq({resolver_key: "git", source: "git://git.example.org/crystal-library.git", requirement: Any})
end
end
end

Expand Down
110 changes: 109 additions & 1 deletion src/dependency.cr
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,105 @@ module Shards
property name : String
property resolver : Resolver
property requirement : Requirement
# resolver's key and source are normalized. We preserve the key and source to be used
# in the shard.yml file in these field. This is used to generate the shard.yml file
# in a more human-readable way.
# A Dependency can still be created without them, but it will not be possible to
# generate the shard.yml file.
property! resolver_key : String
property! source : String

def initialize(@name : String, @resolver : Resolver, @requirement : Requirement = Any, @resolver_key : String? = nil, @source : String? = nil)
end

# Parse a dependency from a CLI argument
def self.from_cli(value : String) : Dependency
parts = parts_from_cli(value)

def initialize(@name : String, @resolver : Resolver, @requirement : Requirement = Any)
# We need to check the actual shard name to create a dependency.
# This requires getting the actual spec file from some matching version.
resolver = Resolver.find_resolver(parts[:resolver_key], "unknown", parts[:source])
version = resolver.versions_for(parts[:requirement]).first || raise Shards::Error.new("No versions found for dependency: #{value}")
spec = resolver.spec(version)
name = spec.name || raise Shards::Error.new("No name found for dependency: #{value}")

Dependency.new(name, resolver, parts[:requirement], parts[:resolver_key], parts[:source])
end

# :nodoc:
#
# Parse the dependency from a CLI argument
# and return the parts needed to create the proper dependency.
#
# Split to allow better unit testing.
def self.parts_from_cli(value : String) : {resolver_key: String, source: String, requirement: Requirement}
resolver_key = nil
source = ""
requirement = Any

if File.directory?(value)
resolver_key = "path"
source = value
end

if value.starts_with?("https://github.com")
resolver_key = "github"
uri = URI.parse(value)
source = uri.path[1..-1] # drop first "/""

components = source.split("/")
case components[2]?
when "commit"
source = "#{components[0]}/#{components[1]}"
requirement = GitCommitRef.new(components[3])
when "tree"
source = "#{components[0]}/#{components[1]}"
requirement = if components[3].starts_with?("v")
GitTagRef.new(components[3])
else
GitBranchRef.new(components[3..-1].join("/"))
end
end
end

if value.starts_with?("https://gitlab.com")
resolver_key = "gitlab"
uri = URI.parse(value)
source = uri.path[1..-1] # drop first "/""
end

if value.starts_with?("https://bitbucket.com")
resolver_key = "bitbucket"
uri = URI.parse(value)
source = uri.path[1..-1] # drop first "/""
end

if value.starts_with?("git://")
resolver_key = "git"
source = value
end

unless resolver_key
Resolver.resolver_keys.each do |key|
key_schema = "#{key}:"
if value.starts_with?(key_schema)
resolver_key = key
source = value.sub(key_schema, "")

# narrow down requirement
if source.includes?("@")
source, version = source.split("@")
requirement = VersionReq.new("~> #{version}")
end

break
end
end
end

raise Shards::Error.new("Invalid dependency format: #{value}") unless resolver_key

{resolver_key: resolver_key, source: source, requirement: requirement}
end

def self.from_yaml(pull : YAML::PullParser)
Expand Down Expand Up @@ -44,6 +141,7 @@ module Shards
end
end

# Used to generate the shard.lock file.
def to_yaml(yaml : YAML::Builder)
yaml.scalar name
yaml.mapping do
Expand All @@ -53,6 +151,16 @@ module Shards
end
end

# Used to generate the shard.yml file.
def to_shard_yaml(yaml : YAML::Builder)
yaml.scalar name
yaml.mapping do
yaml.scalar resolver_key
yaml.scalar source
requirement.to_yaml(yaml)
end
end

def as_package?
version =
case req = @requirement
Expand Down
4 changes: 4 additions & 0 deletions src/resolvers/resolver.cr
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ module Shards
private RESOLVER_CLASSES = {} of String => Resolver.class
private RESOLVER_CACHE = {} of ResolverCacheKey => Resolver

def self.resolver_keys
RESOLVER_CLASSES.keys
end

def self.register_resolver(key, resolver)
RESOLVER_CLASSES[key] = resolver
end
Expand Down