Skip to content
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/Bar@1.2.3").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