From 39006195d58b9199a0f261481a0bac031f738538 Mon Sep 17 00:00:00 2001 From: Alexey Yaremenko Date: Thu, 30 Jul 2020 11:02:25 +0300 Subject: [PATCH] feat: Semantic versioning added to Manifest modification API (#41) * dependencies & scope registries getter added * try get dependency/scope registry APIs * scope registry manipulation API update * set dependency API update * minor refactor changes * semantic version implementation * Version renamed to SemanticVersion & constructor is public * SemanticVersion public fields replaced with properties * redundant namespace qualifier removed * semantic version summary & minor improvements iteration * public interface methods & properties have summary now * additional SemanticVersion cunstructor added * minor description fixes for Dependency class Co-authored-by: Stanislav Osipov * minor description fixes for Manifest class Co-authored-by: Stanislav Osipov * minor description fixes for Manifest class Co-authored-by: Stanislav Osipov Co-authored-by: Alexey Yaremenko Co-authored-by: Stanislav Osipov --- Editor/Manifest/Dependency.cs | 55 +++++- Editor/Manifest/Manifest.cs | 157 +++++++++++++++-- Editor/Manifest/ScopeRegistry.cs | 29 +-- Editor/Manifest/SemanticVersion.cs | 223 ++++++++++++++++++++++++ Editor/Manifest/SemanticVersion.cs.meta | 3 + 5 files changed, 438 insertions(+), 29 deletions(-) create mode 100644 Editor/Manifest/SemanticVersion.cs create mode 100644 Editor/Manifest/SemanticVersion.cs.meta diff --git a/Editor/Manifest/Dependency.cs b/Editor/Manifest/Dependency.cs index 432f826..9d46ef9 100644 --- a/Editor/Manifest/Dependency.cs +++ b/Editor/Manifest/Dependency.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace StansAssets.Foundation.Editor { @@ -8,15 +9,41 @@ namespace StansAssets.Foundation.Editor public class Dependency { /// - /// The dependency name. + /// The name. /// public string Name { get; } /// - /// The dependency version. + /// The version. /// public string Version { get; private set; } + /// + /// `true` if the has ; otherwise, `false`. + /// + public bool HasSemanticVersion { get; private set; } + + /// + /// The semantic version. + /// + public SemanticVersion SemanticVersion { get; private set; } + + /// + /// Initializes a new instance of the class with provided properties. + /// + /// full name which contains name and version (e.g. 'com.company.package@1.0.0'). + public Dependency(string fullName) + { + if (TryGetNameAndVersion(fullName, out string name, out string version)) + { + Name = name; + Version = version; + TryAssignSemanticVersion(); + } + else + throw new ArgumentException("Dependency fullName has wrong format"); + } + /// /// Initializes a new instance of the class with provided properties. /// @@ -26,6 +53,27 @@ public Dependency(string name, string version) { Name = name; Version = version; + TryAssignSemanticVersion(); + } + + internal static bool TryGetNameAndVersion(string fullName, out string name, out string version) + { + name = version = null; + var dependencyData = fullName.Split('@'); + if (dependencyData.Length == 2) + { + name = dependencyData[0]; + version = dependencyData[1]; + return true; + } + return false; + } + + void TryAssignSemanticVersion() + { + HasSemanticVersion = SemanticVersion.TryCreateSemanticVersion(Version, out var semanticVersion); + if (HasSemanticVersion) + SemanticVersion = semanticVersion; } /// @@ -35,6 +83,7 @@ public Dependency(string name, string version) public void SetVersion(string version) { Version = version; + TryAssignSemanticVersion(); } /// diff --git a/Editor/Manifest/Manifest.cs b/Editor/Manifest/Manifest.cs index becf9af..b7c2b2d 100644 --- a/Editor/Manifest/Manifest.cs +++ b/Editor/Manifest/Manifest.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.IO; @@ -68,6 +69,48 @@ public void Fetch() } } + /// + /// Gets the associated with the specified name. + /// + /// The name of the to get. + /// When this method returns, contains the associated with the specified name, + /// if the name is found; otherwise, `null`. This parameter is passed uninitialized. + /// true if the contains a with the specified name; otherwise, false. + public bool TryGetDependency(string name, out Dependency dependency) + { + return m_Dependencies.TryGetValue(name, out dependency); + } + + /// + /// Gets the associated with the specified url. + /// + /// The url of the to get. + /// When this method returns, contains the associated with the specified url, + /// if the url is found; otherwise, `null`. This parameter is passed uninitialized. + /// true if the contains a with the specified url; otherwise, false. + public bool TryGetScopeRegistry(string url, out ScopeRegistry registry) + { + return m_ScopeRegistries.TryGetValue(url, out registry); + } + + /// + /// Returns dependencies of the manifest. + /// + /// Dependencies of the manifest. + public IEnumerable GetDependencies() + { + return m_Dependencies.Values; + } + + /// + /// Returns scope registries of the manifest. + /// + /// Scope registries of the manifest. + public IEnumerable GetScopeRegistries() + { + return m_ScopeRegistries.Values; + } + /// /// Returns dependency by a provided name. /// @@ -89,29 +132,119 @@ public ScopeRegistry GetScopeRegistry(string url) } /// - /// Adds scope registry. + /// Sets by given url. If manifest already contains with given url, + /// existing will be overwritten. /// - /// An entry to add. - public void AddScopeRegistry(ScopeRegistry registry) + /// Scope registry url. + /// to set. + public void SetScopeRegistry(string url, ScopeRegistry registry) { - if (!IsRegistryExists(registry.Url)) + m_ScopeRegistries[url] = registry; + } + + /// + /// Adds with the provided properties. If manifest already contains with given url, + /// provided scopes will be merged with existing scopes. + /// Name of existing won't be updated. + /// + /// Scope registry url. + /// Scope registry name. + /// Scope registry scopes. + /// New with provided properties or existing with updated scopes, if + /// already contains with given name. + public ScopeRegistry AddScopeRegistry(string url, string name, IEnumerable scopes) + { + ScopeRegistry registry; + if (!IsScopeRegistryExists(url)) { - m_ScopeRegistries.Add(registry.Url, registry); + registry = new ScopeRegistry(name, url, scopes); + SetScopeRegistry(url, registry); } + else + { + registry = GetScopeRegistry(url); + foreach (var scope in scopes) + { + if (!registry.HasScope(scope)) + { + registry.AddScope(scope); + } + } + } + return registry; } /// - /// Adds dependency. + /// Sets by given full name. If manifest already contains with given name, its' + /// s will be taken into account. with higher + /// will be placed into the . + /// + /// Dependency full name. + /// New or existing with given name. + /// Thrown when dependency fullName has wrong format + public Dependency SetOrUpdateDependency(string fullName) + { + if (Dependency.TryGetNameAndVersion(fullName, out string name, out string version)) + { + return SetOrUpdateDependency(name, version); + } + + throw new ArgumentException("Dependency fullName has wrong format"); + } + + /// + /// Sets by given name. If manifest already contains with given name, its' + /// s will be taken into account. with higher + /// will be placed into the . /// /// Dependency name. - /// Dependency version. - public void AddDependency(string name, string version) + /// Dependency name. + /// New or existing with given name. + public Dependency SetOrUpdateDependency(string name, string version) { - if (!IsDependencyExists(name)) + if (IsDependencyExists(name)) { - var dependency = new Dependency(name, version); - m_Dependencies.Add(dependency.Name, dependency); + var newDependency = new Dependency(name, version); + var existingDependency = GetDependency(name); + // We have to be sure that both Dependencies have Semantic Version + if (newDependency.HasSemanticVersion && existingDependency.HasSemanticVersion) + { + if (newDependency.SemanticVersion > existingDependency.SemanticVersion) + { + // Set new Dependency because it has higher Semantic Version + SetDependency(name, version); + } + } + else + SetDependency(name, version); } + else + SetDependency(name, version); + + return GetDependency(name); + } + + /// + /// Sets by given full name. If manifest already contains with given name, + /// existing will be overwritten. + /// + /// Dependency full name. + public void SetDependency(string fullName) + { + var dependency = new Dependency(fullName); + m_Dependencies[dependency.Name] = dependency; + } + + /// + /// Sets by given name. If manifest already contains with given name, + /// existing will be overwritten. + /// + /// Dependency name. + /// Dependency version. + public void SetDependency(string name, string version) + { + var dependency = new Dependency(name, version); + m_Dependencies[dependency.Name] = dependency; } /// @@ -147,7 +280,7 @@ public void ApplyChanges() /// /// ScopeRegistry url to search for. /// `true` if scoped registry found, `false` otherwise. - public bool IsRegistryExists(string url) + public bool IsScopeRegistryExists(string url) { return m_ScopeRegistries.ContainsKey(url); } diff --git a/Editor/Manifest/ScopeRegistry.cs b/Editor/Manifest/ScopeRegistry.cs index 15e522c..18a4bbb 100644 --- a/Editor/Manifest/ScopeRegistry.cs +++ b/Editor/Manifest/ScopeRegistry.cs @@ -25,7 +25,9 @@ public class ScopeRegistry /// /// Registry scopes. /// - public HashSet Scopes { get; } + public IEnumerable Scopes => m_Scopes; + + readonly HashSet m_Scopes; /// /// Initializes a new instance of class with the provided properties. @@ -33,27 +35,26 @@ public class ScopeRegistry /// Name of new scope registry. /// Url of new scope registry. /// Scopes of new scope registry. - public ScopeRegistry(string name, string url, HashSet scopes) + public ScopeRegistry(string name, string url, IEnumerable scopes) { Name = name; Url = url; - Scopes = scopes; + m_Scopes = new HashSet(scopes); } /// /// Initializes a new instance of class with the provided data. /// - /// Data to fill this object. Must contain name, - /// url and scopes. + /// Data to fill this object. Must contain name, url and scopes. public ScopeRegistry(Dictionary dictionary) { Name = (string) dictionary[k_KeyName]; Url = (string) dictionary[k_KeyUrl]; var scopes = (List) dictionary[k_KeyScopes]; - Scopes = new HashSet(); + m_Scopes = new HashSet(); foreach (var scope in scopes) { - Scopes.Add((string) scope); + m_Scopes.Add((string) scope); } } @@ -64,7 +65,7 @@ public ScopeRegistry(Dictionary dictionary) /// 'true' if this ScopeRegistry contains scope, `false` otherwise. public bool HasScope(string scope) { - return Scopes.Contains(scope); + return m_Scopes.Contains(scope); } /// @@ -74,7 +75,7 @@ public bool HasScope(string scope) public void AddScope(string scope) { if (!HasScope(scope)) - Scopes.Add(scope); + m_Scopes.Add(scope); } /// @@ -84,8 +85,8 @@ public void AddScope(string scope) public override int GetHashCode() { int hash = 0; if (!string.IsNullOrEmpty(Url)) hash ^= Url.GetHashCode(); - if (Scopes != null) { - foreach (var scope in Scopes) { + if (m_Scopes != null) { + foreach (var scope in m_Scopes) { hash ^= scope.GetHashCode(); } } @@ -101,8 +102,8 @@ public override bool Equals(object obj) { return obj is ScopeRegistry other && Url == other.Url && - Scopes != null && other.Scopes != null && - new HashSet(Scopes).SetEquals(other.Scopes); + m_Scopes != null && other.Scopes != null && + m_Scopes.SetEquals(other.Scopes); } /// @@ -114,7 +115,7 @@ public Dictionary ToDictionary() Dictionary result = new Dictionary(); result.Add(k_KeyName,Name); result.Add(k_KeyUrl,Url); - result.Add(k_KeyScopes,Scopes.ToList()); + result.Add(k_KeyScopes,m_Scopes.ToList()); return result; } } diff --git a/Editor/Manifest/SemanticVersion.cs b/Editor/Manifest/SemanticVersion.cs new file mode 100644 index 0000000..9ed8359 --- /dev/null +++ b/Editor/Manifest/SemanticVersion.cs @@ -0,0 +1,223 @@ +using System; +using System.Text; +using System.Text.RegularExpressions; + +namespace StansAssets.Foundation.Editor +{ + /// + /// Representation of the semantic version. + /// + public class SemanticVersion + { + // Regular Expression to check a Semantic Version string. With numbered capture groups + // (so cg1 = major, cg2 = minor, cg3 = patch, cg4 = prerelease and cg5 = buildmetadata) + // that is compatible with ECMA Script (JavaScript), + // PCRE (Perl Compatible Regular Expressions, i.e. Perl, PHP and R), Python and Go. + // See: https://regex101.com/r/vkijKf/1/ + static readonly Regex s_Matcher = new Regex(@"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"); + + /// + /// The major version number. + /// + public int Major { get; } + + /// + /// The minor version number. + /// + public int Minor { get; } + + /// + /// The patch version number. + /// + public int Patch { get; } + + /// + /// True if the has pre-release data string set; otherwise, false. + /// + public bool HasPreRelease { get; } + + /// + /// The pre-release data string. + /// + public string PreRelease { get; } + + /// + /// True if the has build metadata string set; otherwise, false. + /// + public bool HasBuildMetadata { get; } + + /// + /// The build metadata string. + /// + public string BuildMetadata { get; } + + string m_StringRepresentation; + + /// + /// Initializes a new instance of the class with provided data. + /// + /// String which contains data. + /// Thrown when versionString has incorrect format + public SemanticVersion(string versionString) + { + if (ValidateVersionFormat(versionString, out var regexMatch)) + { + Major = Int32.Parse(regexMatch.Groups[1].Value); + Minor = Int32.Parse(regexMatch.Groups[2].Value); + Patch = Int32.Parse(regexMatch.Groups[3].Value); + + PreRelease = regexMatch.Groups[4].Value; + HasPreRelease = !string.IsNullOrEmpty(PreRelease); + + BuildMetadata = regexMatch.Groups[5].Value; + HasBuildMetadata = !string.IsNullOrEmpty(BuildMetadata); + } + else + throw new ArgumentException("Version string has incorrect format"); + } + + /// + /// Initializes a new instance of the class with provided properties. + /// + /// The major version number. + /// The minor version number. + /// The patch version number. + /// The pre-release data string. + /// The build metadata string. + public SemanticVersion(int major, int minor, int patch, string preRelease, string buildMetadata) + { + Major = major; + Minor = minor; + Patch = patch; + + PreRelease = preRelease; + HasPreRelease = !string.IsNullOrEmpty(PreRelease); + + BuildMetadata = buildMetadata; + HasBuildMetadata = !string.IsNullOrEmpty(BuildMetadata); + } + + static bool ValidateVersionFormat(string versionString, out Match regexMatch) + { + regexMatch = s_Matcher.Match(versionString); + return regexMatch.Success; + } + + /// + /// Tries to create a new instance of the class with provided data. + /// + /// String which contains data. + /// When this method returns, contains the object with provided data, + /// if versionString has correct format; otherwise, null. This parameter is passed uninitialized. + /// 'true' if the has been successfully created; otherwise, 'false'. + public static bool TryCreateSemanticVersion(string versionString, out SemanticVersion semanticVersion) + { + semanticVersion = null; + if (ValidateVersionFormat(versionString, out var regexMatch)) + { + var major = Int32.Parse(regexMatch.Groups[1].Value); + var minor = Int32.Parse(regexMatch.Groups[2].Value); + var patch = Int32.Parse(regexMatch.Groups[3].Value); + var preRelease = regexMatch.Groups[4].Value; + var buildMetadata = regexMatch.Groups[5].Value; + + semanticVersion = new SemanticVersion(major, minor, patch, preRelease, buildMetadata); + return true; + } + + return false; + } + + /// + /// Compares two objects. Major, Minor and Patch version numbers will be compared one by one respectively. + /// + /// object which you want to compare. + /// object which you want to compare to. + public static bool operator >(SemanticVersion semanticVersion, SemanticVersion other) + { + if (semanticVersion.Major > other.Major) + return true; + if (semanticVersion.Minor > other.Minor) + return true; + if (semanticVersion.Patch > other.Patch) + return true; + + return false; + } + + /// + /// Compares two objects. Major, Minor and Patch version numbers will be compared one by one respectively. + /// + /// object which you want to compare. + /// object which you want to compare to. + public static bool operator <(SemanticVersion semanticVersion, SemanticVersion other) + { + return !(semanticVersion > other); + } + + /// + /// Generates a hash of this object data. + /// + /// Hash of this object. + public override int GetHashCode() { + int hash = 0; + hash ^= Major; + hash ^= Minor; + hash ^= Patch; + if (HasPreRelease) + hash ^= PreRelease.GetHashCode(); + if (HasBuildMetadata) + hash ^= BuildMetadata.GetHashCode(); + + return hash; + } + + /// + /// Determines whether two instances are equal. + /// + /// The object to compare with the current . + /// 'true' if the specified object is equal to the current ; otherwise, 'false'. + public override bool Equals(object obj) + { + if (obj is SemanticVersion other) + { + bool sameVersions = Major == other.Major && + Minor == other.Minor && + Patch == other.Patch; + if (sameVersions && HasPreRelease && other.HasPreRelease) + { + if (PreRelease.Equals(other.PreRelease) && HasBuildMetadata && other.HasBuildMetadata) + { + return BuildMetadata.Equals(other.BuildMetadata); + } + } + } + return false; + } + + /// + /// Returns a string that represents the current . + /// + /// A string that represents the current . + public override string ToString() + { + if (m_StringRepresentation == null) + { + StringBuilder builder = new StringBuilder(); + builder.Append($"{Major}."); + builder.Append($"{Minor}."); + builder.Append(Patch); + + if (HasPreRelease) + builder.Append($"-{PreRelease}"); + + if (HasBuildMetadata) + builder.Append($"+{BuildMetadata}"); + + m_StringRepresentation = builder.ToString(); + } + + return m_StringRepresentation; + } + } +} diff --git a/Editor/Manifest/SemanticVersion.cs.meta b/Editor/Manifest/SemanticVersion.cs.meta new file mode 100644 index 0000000..b586a01 --- /dev/null +++ b/Editor/Manifest/SemanticVersion.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b4c9b95234aa490daa0c3c403837385c +timeCreated: 1591974779 \ No newline at end of file