diff --git a/.gitignore b/.gitignore index 46d24b4..23438af 100644 --- a/.gitignore +++ b/.gitignore @@ -136,6 +136,7 @@ _TeamCity* # Visual Studio code coverage results *.coverage *.coveragexml +*.opencover.xml # NCrunch _NCrunch_* diff --git a/Dockerfile b/Dockerfile index 3698e1c..35ea302 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,6 @@ WORKDIR /app COPY src/BikeshareClient . RUN dotnet restore -RUN dotnet test TestBikeshareClient/TestBikeshareClient.csproj /p:CollectCoverage=true /p:Threshold=80 /p:ThresholdType=line /p:CoverletOutputFormat=opencover +RUN dotnet test TestBikeshareClient/TestBikeshareClient.csproj /p:CollectCoverage=true /p:Threshold=80 /p:ThresholdType=line /p:CoverletOutputFormat=opencover /p:ExcludeByAttribute=ExcludeFromCodeCoverageAttribute RUN dotnet pack -c Release -o output $VERSION_SUFFIX diff --git a/src/BikeshareClient/BikeshareClient/BikeshareClient.csproj b/src/BikeshareClient/BikeshareClient/BikeshareClient.csproj index 9e39eac..0991347 100644 --- a/src/BikeshareClient/BikeshareClient/BikeshareClient.csproj +++ b/src/BikeshareClient/BikeshareClient/BikeshareClient.csproj @@ -2,8 +2,8 @@ netstandard2.0 - 3.1.0 - 3.1.0 + 3.2.0 + 3.2.0 BikeshareClient andmos Dotnet client for bike share systems implementing the General Bikeshare Feed Specification (GBFS). diff --git a/src/BikeshareClient/BikeshareClient/DTO/BikeStatusDTO.cs b/src/BikeshareClient/BikeshareClient/DTO/BikeStatusDTO.cs index a98bc0f..e964370 100644 --- a/src/BikeshareClient/BikeshareClient/DTO/BikeStatusDTO.cs +++ b/src/BikeshareClient/BikeshareClient/DTO/BikeStatusDTO.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel; +using BikeshareClient.Helpers; using BikeshareClient.Models; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -9,18 +11,24 @@ namespace BikeshareClient.DTO internal readonly struct BikeStatusDTO { [JsonConstructor] - public BikeStatusDTO([JsonProperty("last_updated"), JsonConverter(typeof(UnixDateTimeConverter))]DateTime lastUpdate, - [JsonProperty("ttl")] int timeToLive, - [JsonProperty("data")] BikeStatusData bikeData) + public BikeStatusDTO( + [JsonProperty("last_updated"), JsonConverter(typeof(UnixDateTimeConverter))]DateTime lastUpdate, + [JsonProperty("ttl")] int timeToLive, + [JsonProperty("version"), JsonConverter(typeof(StringToSemanticVersionConverter))] SemanticVersion version, + [JsonProperty("data")] BikeStatusData bikeData) { LastUpdated = lastUpdate; TimeToLive = timeToLive; - BikeStatusData = bikeData; + Version = version ?? new SemanticVersion("1.0"); + BikeStatusData = bikeData; + } - public DateTime LastUpdated { get; } + public DateTime LastUpdated { get; } + + public int TimeToLive { get; } - public int TimeToLive { get; } + public SemanticVersion Version { get; } public BikeStatusData BikeStatusData { get; } } diff --git a/src/BikeshareClient/BikeshareClient/DTO/GbfsDTO.cs b/src/BikeshareClient/BikeshareClient/DTO/GbfsDTO.cs index 08a1077..9d2cfe2 100644 --- a/src/BikeshareClient/BikeshareClient/DTO/GbfsDTO.cs +++ b/src/BikeshareClient/BikeshareClient/DTO/GbfsDTO.cs @@ -11,18 +11,23 @@ namespace BikeshareClient.DTO internal readonly struct GbfsDTO { [JsonConstructor] - public GbfsDTO([JsonProperty("last_updated"), JsonConverter(typeof(UnixDateTimeConverter))] DateTime lastUpdated, - [JsonProperty("ttl")] int timeToLive, - [JsonProperty("data"), JsonConverter(typeof(FeedsConverter))] FeedsData[] feedsData) + public GbfsDTO( + [JsonProperty("last_updated"), JsonConverter(typeof(UnixDateTimeConverter))] DateTime lastUpdated, + [JsonProperty("ttl")] int timeToLive, + [JsonProperty("version"), JsonConverter(typeof(StringToSemanticVersionConverter))] SemanticVersion version, + [JsonProperty("data"), JsonConverter(typeof(FeedsConverter))] FeedsData[] feedsData) { LastUpdated = lastUpdated; TimeToLive = timeToLive; + Version = version ?? new SemanticVersion("1.0"); FeedsData = feedsData; } public DateTime LastUpdated { get; } public int TimeToLive { get; } + public SemanticVersion Version { get; } + public FeedsData[] FeedsData { get; } } diff --git a/src/BikeshareClient/BikeshareClient/DTO/StationDTO.cs b/src/BikeshareClient/BikeshareClient/DTO/StationDTO.cs index b72f2d6..3409191 100644 --- a/src/BikeshareClient/BikeshareClient/DTO/StationDTO.cs +++ b/src/BikeshareClient/BikeshareClient/DTO/StationDTO.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using BikeshareClient.Helpers; using BikeshareClient.Models; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -9,12 +10,15 @@ namespace BikeshareClient.DTO internal readonly struct StationDTO { [JsonConstructor] - public StationDTO([JsonProperty("last_updated"), JsonConverter(typeof(UnixDateTimeConverter))] DateTime lastUpdated, - [JsonProperty("ttl")] int timeToLive, - [JsonProperty("data")] StationData stationsData) + public StationDTO( + [JsonProperty("last_updated"), JsonConverter(typeof(UnixDateTimeConverter))] DateTime lastUpdated, + [JsonProperty("ttl")] int timeToLive, + [JsonProperty("version"), JsonConverter(typeof(StringToSemanticVersionConverter))] SemanticVersion version, + [JsonProperty("data")] StationData stationsData) { LastUpdated = lastUpdated; TimeToLive = timeToLive; + Version = version ?? new SemanticVersion("1.0"); StationsData = stationsData; } @@ -22,6 +26,8 @@ public StationDTO([JsonProperty("last_updated"), JsonConverter(typeof(UnixDateTi public int TimeToLive { get; } + public SemanticVersion Version { get; } + public StationData StationsData { get; } } diff --git a/src/BikeshareClient/BikeshareClient/DTO/StationStatusDTO.cs b/src/BikeshareClient/BikeshareClient/DTO/StationStatusDTO.cs index abbf23b..bb169e2 100644 --- a/src/BikeshareClient/BikeshareClient/DTO/StationStatusDTO.cs +++ b/src/BikeshareClient/BikeshareClient/DTO/StationStatusDTO.cs @@ -1,41 +1,45 @@ using System; using System.Collections.Generic; +using BikeshareClient.Helpers; using BikeshareClient.Models; using Newtonsoft.Json; using Newtonsoft.Json.Converters; namespace BikeshareClient.DTO { - internal readonly struct StationStatusDTO + internal readonly struct StationStatusDTO { - [JsonConstructor] - public StationStatusDTO([JsonProperty("last_updated"), JsonConverter(typeof(UnixDateTimeConverter))]DateTime lastUpdated, - [JsonProperty("ttl")] int timeToLive, - [JsonProperty("data")]StationStatusData stationStatusData) + [JsonConstructor] + public StationStatusDTO( + [JsonProperty("last_updated"), JsonConverter(typeof(UnixDateTimeConverter))]DateTime lastUpdated, + [JsonProperty("ttl")] int timeToLive, + [JsonProperty("version"), JsonConverter(typeof(StringToSemanticVersionConverter))] SemanticVersion version, + [JsonProperty("data")]StationStatusData stationStatusData) { LastUpdated = lastUpdated; TimeToLive = timeToLive; + Version = version ?? new SemanticVersion("1.0"); StationsStatusData = stationStatusData; } - public DateTime LastUpdated { get; } + public DateTime LastUpdated { get; } - public int TimeToLive { get; } - + + public SemanticVersion Version { get; } public StationStatusData StationsStatusData { get; } } - internal struct StationStatusData + internal struct StationStatusData { - [JsonConstructor] - public StationStatusData(IEnumerable stations) - { - StationsStatus = stations; - } + [JsonConstructor] + public StationStatusData(IEnumerable stations) + { + StationsStatus = stations; + } - public IEnumerable StationsStatus { get; } + public IEnumerable StationsStatus { get; } } } diff --git a/src/BikeshareClient/BikeshareClient/DTO/SystemInformationDTO.cs b/src/BikeshareClient/BikeshareClient/DTO/SystemInformationDTO.cs index 188384c..f56f025 100644 --- a/src/BikeshareClient/BikeshareClient/DTO/SystemInformationDTO.cs +++ b/src/BikeshareClient/BikeshareClient/DTO/SystemInformationDTO.cs @@ -1,26 +1,32 @@ using System; +using BikeshareClient.Helpers; using BikeshareClient.Models; using Newtonsoft.Json; using Newtonsoft.Json.Converters; namespace BikeshareClient.DTO { - internal readonly struct SystemInformationDTO + internal readonly struct SystemInformationDTO { - public SystemInformationDTO([JsonProperty("last_updated"), JsonConverter(typeof(UnixDateTimeConverter))] DateTime lastupdated, - [JsonProperty("ttl")] int timeToLive, - [JsonProperty("data")] SystemInformation systemInformation) + public SystemInformationDTO( + [JsonProperty("last_updated"), JsonConverter(typeof(UnixDateTimeConverter))] DateTime lastupdated, + [JsonProperty("ttl")] int timeToLive, + [JsonProperty("version"), JsonConverter(typeof(StringToSemanticVersionConverter))] SemanticVersion version, + [JsonProperty("data")] SystemInformation systemInformation) { - LastUpdated = lastupdated; - TimeToLive = timeToLive; - SystemInformation = systemInformation; - } + LastUpdated = lastupdated; + TimeToLive = timeToLive; + Version = version ?? new SemanticVersion("1.0"); + SystemInformation = systemInformation; + } - public DateTime LastUpdated { get; } + public DateTime LastUpdated { get; } - public int TimeToLive { get; } + public int TimeToLive { get; } - public SystemInformation SystemInformation { get; } + public SemanticVersion Version { get; } + + public SystemInformation SystemInformation { get; } } } diff --git a/src/BikeshareClient/BikeshareClient/Helpers/SemanticVersion.cs b/src/BikeshareClient/BikeshareClient/Helpers/SemanticVersion.cs new file mode 100644 index 0000000..74a02f2 --- /dev/null +++ b/src/BikeshareClient/BikeshareClient/Helpers/SemanticVersion.cs @@ -0,0 +1,353 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Text; +using System.Text.RegularExpressions; +using Newtonsoft.Json; + +namespace BikeshareClient.Helpers +{ + [ExcludeFromCodeCoverage] + public sealed class SemanticVersion : IComparable, IComparable, IEquatable + { + private const RegexOptions _flags = RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture; + private static readonly Regex _semanticVersionRegex = new Regex(@"^(?\d+(\s*\.\s*\d+){0,3})(?-[a-z][0-9a-z-]*)?$", _flags); + private static readonly Regex _strictSemanticVersionRegex = new Regex(@"^(?\d+(\.\d+){2})(?-[a-z][0-9a-z-]*)?$", _flags); + private readonly string _originalString; + private string _normalizedVersionString; + + public SemanticVersion(string version) + : this(Parse(version)) + { + // The constructor normalizes the version string so that it we do not need to normalize it every time we need to operate on it. + // The original string represents the original form in which the version is represented to be used when printing. + _originalString = version; + } + + public SemanticVersion(int major, int minor, int build, int revision) + : this(new Version(major, minor, build, revision)) + { + } + + public SemanticVersion(int major, int minor, int build, string specialVersion) + : this(new Version(major, minor, build), specialVersion) + { + } + [JsonConstructor] + public SemanticVersion(Version version) + : this(version, String.Empty) + { + } + + public SemanticVersion(Version version, string specialVersion) + : this(version, specialVersion, null) + { + } + + private SemanticVersion(Version version, string specialVersion, string originalString) + { + if (version == null) + { + var foo = "bar"; + throw new ArgumentNullException("version"); + + } + Version = NormalizeVersionValue(version); + SpecialVersion = specialVersion ?? String.Empty; + _originalString = String.IsNullOrEmpty(originalString) ? version.ToString() + (!String.IsNullOrEmpty(specialVersion) ? '-' + specialVersion : null) : originalString; + } + + internal SemanticVersion(SemanticVersion semVer) + { + _originalString = semVer.ToString(); + Version = semVer.Version; + SpecialVersion = semVer.SpecialVersion; + } + + /// + /// Gets the normalized version portion. + /// + public Version Version + { + get; + private set; + } + + /// + /// Gets the optional special version. + /// + public string SpecialVersion + { + get; + private set; + } + + public string[] GetOriginalVersionComponents() + { + if (!String.IsNullOrEmpty(_originalString)) + { + string original; + + // search the start of the SpecialVersion part, if any + int dashIndex = _originalString.IndexOf('-'); + if (dashIndex != -1) + { + // remove the SpecialVersion part + original = _originalString.Substring(0, dashIndex); + } + else + { + original = _originalString; + } + + return SplitAndPadVersionString(original); + } + else + { + return SplitAndPadVersionString(Version.ToString()); + } + } + + private static string[] SplitAndPadVersionString(string version) + { + string[] a = version.Split('.'); + if (a.Length == 4) + { + return a; + } + else + { + // if 'a' has less than 4 elements, we pad the '0' at the end + // to make it 4. + var b = new string[4] { "0", "0", "0", "0" }; + Array.Copy(a, 0, b, 0, a.Length); + return b; + } + } + + /// + /// Parses a version string using loose semantic versioning rules that allows 2-4 version components followed by an optional special version. + /// + public static SemanticVersion Parse(string version) + { + if (String.IsNullOrEmpty(version)) + { + throw new ArgumentException(version, "cannot be null or empty"); + } + + SemanticVersion semVer; + if (!TryParse(version, out semVer)) + { + throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, version), nameof(version)); + } + return semVer; + } + + /// + /// Parses a version string using loose semantic versioning rules that allows 2-4 version components followed by an optional special version. + /// + public static bool TryParse(string version, out SemanticVersion value) + { + return TryParseInternal(version, _semanticVersionRegex, out value); + } + + /// + /// Parses a version string using strict semantic versioning rules that allows exactly 3 components and an optional special version. + /// + public static bool TryParseStrict(string version, out SemanticVersion value) + { + return TryParseInternal(version, _strictSemanticVersionRegex, out value); + } + + private static bool TryParseInternal(string version, Regex regex, out SemanticVersion semVer) + { + semVer = null; + if (String.IsNullOrEmpty(version)) + { + return false; + } + + var match = regex.Match(version.Trim()); + Version versionValue; + if (!match.Success || !Version.TryParse(match.Groups["Version"].Value, out versionValue)) + { + return false; + } + + semVer = new SemanticVersion(NormalizeVersionValue(versionValue), match.Groups["Release"].Value.TrimStart('-'), version.Replace(" ", "")); + return true; + } + + /// + /// Attempts to parse the version token as a SemanticVersion. + /// + /// An instance of SemanticVersion if it parses correctly, null otherwise. + public static SemanticVersion ParseOptionalVersion(string version) + { + SemanticVersion semVer; + TryParse(version, out semVer); + return semVer; + } + + private static Version NormalizeVersionValue(Version version) + { + return new Version(version.Major, + version.Minor, + Math.Max(version.Build, 0), + Math.Max(version.Revision, 0)); + } + + public int CompareTo(object obj) + { + if (object.ReferenceEquals(obj, null)) + { + return 1; + } + SemanticVersion other = obj as SemanticVersion; + if (other == null) + { + throw new ArgumentException("obj"); + } + return CompareTo(other); + } + + public int CompareTo(SemanticVersion other) + { + if (Object.ReferenceEquals(other, null)) + { + return 1; + } + + int result = Version.CompareTo(other.Version); + + if (result != 0) + { + return result; + } + + bool empty = String.IsNullOrEmpty(SpecialVersion); + bool otherEmpty = String.IsNullOrEmpty(other.SpecialVersion); + if (empty && otherEmpty) + { + return 0; + } + else if (empty) + { + return 1; + } + else if (otherEmpty) + { + return -1; + } + return StringComparer.OrdinalIgnoreCase.Compare(SpecialVersion, other.SpecialVersion); + } + + public static bool operator ==(SemanticVersion version1, SemanticVersion version2) + { + if (Object.ReferenceEquals(version1, null)) + { + return Object.ReferenceEquals(version2, null); + } + return version1.Equals(version2); + } + + public static bool operator !=(SemanticVersion version1, SemanticVersion version2) + { + return !(version1 == version2); + } + + public static bool operator <(SemanticVersion version1, SemanticVersion version2) + { + if (version1 == null) + { + throw new ArgumentNullException(nameof(version1)); + } + return version1.CompareTo(version2) < 0; + } + + public static bool operator <=(SemanticVersion version1, SemanticVersion version2) + { + return (version1 == version2) || (version1 < version2); + } + + public static bool operator >(SemanticVersion version1, SemanticVersion version2) + { + if (version1 == null) + { + throw new ArgumentNullException(nameof(version1)); + } + return version2 < version1; + } + + public static bool operator >=(SemanticVersion version1, SemanticVersion version2) + { + return (version1 == version2) || (version1 > version2); + } + + //public override string ToString() + //{ + // return _originalString; + //} + + /// + /// Returns the normalized string representation of this instance of . + /// If the instance can be strictly parsed as a , the normalized version + /// string if of the format {major}.{minor}.{build}[-{special-version}]. If the instance has a non-zero + /// value for , the format is {major}.{minor}.{build}.{revision}[-{special-version}]. + /// + /// The normalized string representation. + public string ToNormalizedString() + { + if (_normalizedVersionString == null) + { + var builder = new StringBuilder(); + builder + .Append(Version.Major) + .Append('.') + .Append(Version.Minor) + .Append('.') + .Append(Math.Max(0, Version.Build)); + + if (Version.Revision > 0) + { + builder.Append('.') + .Append(Version.Revision); + } + + if (!string.IsNullOrEmpty(SpecialVersion)) + { + builder.Append('-') + .Append(SpecialVersion); + } + + _normalizedVersionString = builder.ToString(); + } + + return _normalizedVersionString; + } + + public bool Equals(SemanticVersion other) + { + return !Object.ReferenceEquals(null, other) && + Version.Equals(other.Version) && + SpecialVersion.Equals(other.SpecialVersion, StringComparison.OrdinalIgnoreCase); + } + + public override bool Equals(object obj) + { + SemanticVersion semVer = obj as SemanticVersion; + return !Object.ReferenceEquals(null, semVer) && Equals(semVer); + } + + public override int GetHashCode() + { + int hashCode = Version.GetHashCode(); + if (SpecialVersion != null) + { + hashCode = hashCode * 4567 + SpecialVersion.GetHashCode(); + } + + return hashCode; + } + } +} diff --git a/src/BikeshareClient/BikeshareClient/Helpers/StringToSemanticVersionConverter.cs b/src/BikeshareClient/BikeshareClient/Helpers/StringToSemanticVersionConverter.cs new file mode 100644 index 0000000..7442607 --- /dev/null +++ b/src/BikeshareClient/BikeshareClient/Helpers/StringToSemanticVersionConverter.cs @@ -0,0 +1,24 @@ +using System; +using Newtonsoft.Json; +namespace BikeshareClient.Helpers +{ + public class StringToSemanticVersionConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return objectType == typeof(string); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + SemanticVersion version; + var parsed = SemanticVersion.TryParse(reader.Value.ToString(), out version); + return parsed ? version : new SemanticVersion("1.0"); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/BikeshareClient/BikeshareClient/Models/BikeStatus.cs b/src/BikeshareClient/BikeshareClient/Models/BikeStatus.cs index 481de6d..d32f608 100644 --- a/src/BikeshareClient/BikeshareClient/Models/BikeStatus.cs +++ b/src/BikeshareClient/BikeshareClient/Models/BikeStatus.cs @@ -9,12 +9,13 @@ namespace BikeshareClient.Models public class BikeStatus { [JsonConstructor] - public BikeStatus([JsonProperty("bike_id")]string id, - [JsonProperty("name")]string name, - [JsonProperty("lat")]double latitude, - [JsonProperty("lon")]double longitude, - [JsonProperty("is_reserved"), JsonConverter(typeof(IntegerToBoolConverter))] bool reserved, - [JsonProperty("is_disabled"), JsonConverter(typeof(IntegerToBoolConverter))] bool disabled) + public BikeStatus( + [JsonProperty("bike_id")]string id, + [JsonProperty("name")]string name, + [JsonProperty("lat")]double latitude, + [JsonProperty("lon")]double longitude, + [JsonProperty("is_reserved"), JsonConverter(typeof(IntegerToBoolConverter))] bool reserved, + [JsonProperty("is_disabled"), JsonConverter(typeof(IntegerToBoolConverter))] bool disabled) { Id = id; Name = name; diff --git a/src/BikeshareClient/BikeshareClient/Models/Feed.cs b/src/BikeshareClient/BikeshareClient/Models/Feed.cs index 656f4af..54bdbde 100644 --- a/src/BikeshareClient/BikeshareClient/Models/Feed.cs +++ b/src/BikeshareClient/BikeshareClient/Models/Feed.cs @@ -6,8 +6,9 @@ namespace BikeshareClient.Models public class Feed { [JsonConstructor] - public Feed([JsonProperty("name")]string name, - [JsonProperty("url")]string url) + public Feed( + [JsonProperty("name")]string name, + [JsonProperty("url")]string url) { Name = name; Url = url; diff --git a/src/BikeshareClient/BikeshareClient/Models/Station.cs b/src/BikeshareClient/BikeshareClient/Models/Station.cs index cd02cee..7144679 100644 --- a/src/BikeshareClient/BikeshareClient/Models/Station.cs +++ b/src/BikeshareClient/BikeshareClient/Models/Station.cs @@ -8,12 +8,13 @@ namespace BikeshareClient.Models public class Station { [JsonConstructor] - public Station([JsonProperty("station_id")] string id, - [JsonProperty("name"), JsonConverter(typeof(TrimmingConverter))] string name, - [JsonProperty("address")] string address, - [JsonProperty("lat")] double latitude, - [JsonProperty("lon")] double longitude, - [JsonProperty("capacity")] int capacity) + public Station( + [JsonProperty("station_id")] string id, + [JsonProperty("name"), JsonConverter(typeof(TrimmingConverter))] string name, + [JsonProperty("address")] string address, + [JsonProperty("lat")] double latitude, + [JsonProperty("lon")] double longitude, + [JsonProperty("capacity")] int capacity) { Id = id; Name = name; diff --git a/src/BikeshareClient/BikeshareClient/Models/StationStatus.cs b/src/BikeshareClient/BikeshareClient/Models/StationStatus.cs index 9a374a0..f69b88e 100644 --- a/src/BikeshareClient/BikeshareClient/Models/StationStatus.cs +++ b/src/BikeshareClient/BikeshareClient/Models/StationStatus.cs @@ -10,15 +10,16 @@ namespace BikeshareClient.Models public class StationStatus { [JsonConstructor] - public StationStatus([JsonProperty("station_id")] string id, - [JsonProperty("num_bikes_available")] int bikesAvailable, - [JsonProperty("num_bikes_disabled")] int bikesDisabled, - [JsonProperty("num_docks_available")]int docksAvailable, - [JsonProperty("is_installed"),JsonConverter(typeof(IntegerToBoolConverter))]bool installed, - [JsonProperty("is_renting"), JsonConverter(typeof(IntegerToBoolConverter))]bool renting, - [JsonProperty("is_returning"), JsonConverter(typeof(IntegerToBoolConverter))]bool returning, - [JsonProperty("num_docks_disabled")]int docsDisabled, - [JsonProperty("last_reported"), JsonConverter(typeof(UnixDateTimeConverter))] DateTime lastReported) + public StationStatus( + [JsonProperty("station_id")] string id, + [JsonProperty("num_bikes_available")] int bikesAvailable, + [JsonProperty("num_bikes_disabled")] int bikesDisabled, + [JsonProperty("num_docks_available")]int docksAvailable, + [JsonProperty("is_installed"),JsonConverter(typeof(IntegerToBoolConverter))]bool installed, + [JsonProperty("is_renting"), JsonConverter(typeof(IntegerToBoolConverter))]bool renting, + [JsonProperty("is_returning"), JsonConverter(typeof(IntegerToBoolConverter))]bool returning, + [JsonProperty("num_docks_disabled")]int docsDisabled, + [JsonProperty("last_reported"), JsonConverter(typeof(UnixDateTimeConverter))] DateTime lastReported) { Id = id; BikesAvailable = bikesAvailable; diff --git a/src/BikeshareClient/BikeshareClient/Models/SystemInformation.cs b/src/BikeshareClient/BikeshareClient/Models/SystemInformation.cs index 3d6cfc5..4c2ff56 100644 --- a/src/BikeshareClient/BikeshareClient/Models/SystemInformation.cs +++ b/src/BikeshareClient/BikeshareClient/Models/SystemInformation.cs @@ -8,13 +8,14 @@ namespace BikeshareClient.Models public class SystemInformation { [JsonConstructor] - public SystemInformation([JsonProperty("system_id")] string id, - [JsonProperty("name")] string name, - [JsonProperty("language")]string language, - [JsonProperty("operator")]string operatorName, - [JsonProperty("timezone")]string timeZone, - [JsonProperty("phone_number")]string phoneNumber, - [JsonProperty("email")] string email) + public SystemInformation( + [JsonProperty("system_id")] string id, + [JsonProperty("name")] string name, + [JsonProperty("language")]string language, + [JsonProperty("operator")]string operatorName, + [JsonProperty("timezone")]string timeZone, + [JsonProperty("phone_number")]string phoneNumber, + [JsonProperty("email")] string email) { Id = id; Name = name; diff --git a/src/BikeshareClient/TestBikeshareClient/Helpers/TestGbfsFeedsConverter.cs b/src/BikeshareClient/TestBikeshareClient/Helpers/TestGbfsFeedsConverter.cs index 2d31a4e..eb9f7b4 100644 --- a/src/BikeshareClient/TestBikeshareClient/Helpers/TestGbfsFeedsConverter.cs +++ b/src/BikeshareClient/TestBikeshareClient/Helpers/TestGbfsFeedsConverter.cs @@ -12,7 +12,7 @@ public class TestGbfsFeedsConverter private readonly string TestFile = @"Helpers/TestableGbfsJson.json"; [Fact] - public void FeedsConverter_GivenValidGbfsJsonFile_ShouldReturnFeedsData() + public void ReadJson_GivenValidGbfsJsonFile_ShouldReturnFeedsData() { var gbfsObject = JsonConvert.DeserializeObject(File.ReadAllText(TestFile)); @@ -20,14 +20,13 @@ public void FeedsConverter_GivenValidGbfsJsonFile_ShouldReturnFeedsData() } [Fact] - public void FeedsConverter_GivenValidGbfsJsonFile_ShouldReturnFeedsDataWithLanguages() + public void ReadJson_GivenValidGbfsJsonFile_ShouldReturnFeedsDataWithLanguages() { var gbfsObject = JsonConvert.DeserializeObject(File.ReadAllText(TestFile)); var firstLanguage = gbfsObject.FeedsData.Select(l => l.Language).FirstOrDefault(); Assert.Equal("ar", firstLanguage.Name); - } } } diff --git a/src/BikeshareClient/TestBikeshareClient/Helpers/TestSemanticVersion.cs b/src/BikeshareClient/TestBikeshareClient/Helpers/TestSemanticVersion.cs new file mode 100644 index 0000000..033292a --- /dev/null +++ b/src/BikeshareClient/TestBikeshareClient/Helpers/TestSemanticVersion.cs @@ -0,0 +1,69 @@ +using BikeshareClient.Helpers; +using Newtonsoft.Json; +using Xunit; +using System.Collections.Generic; +using System.Linq; +using System; + +namespace TestBikeshareClient.Helpers +{ + public class TestSemanticVersion + { + [Fact] + public void SemanticVersion_GivenValidJSONString_ReturnsSemanticVersion() + { + var json = @"{ Version : ""1.2.3"" }"; + SemanticVersion parsedValue; + + var semanticModel = JsonConvert.DeserializeObject>(json).FirstOrDefault(); + var semantic = SemanticVersion.TryParse(semanticModel.Value, out parsedValue); + + Assert.True(semantic); + Assert.True(parsedValue.Version.Major == 1); + Assert.True(parsedValue.Version.Minor == 2); + Assert.True(parsedValue.Version.Build == 3); + } + + [Fact] + public void SemanticVersion_GivenValidSemanticVersionString_ReturnsCorrectObject() + { + var semantic = new SemanticVersion("1.2.3"); + + Assert.True(semantic.Version.Major == 1); + Assert.True(semantic.Version.Minor == 2); + Assert.True(semantic.Version.Build == 3); + } + + [Fact] + public void SemanticVersion_GivenValidSemanticVersionIntegers_ReturnsCorrectObject() + { + var semantic = new SemanticVersion(1,2,3,4); + + Assert.True(semantic.Version.Major == 1); + Assert.True(semantic.Version.Minor == 2); + Assert.True(semantic.Version.Build == 3); + Assert.True(semantic.Version.Revision == 4); + } + + [Fact] + public void SemanticVersion_GivenValidSemanticVersionSpecial_ReturnsCorrectObject() + { + var semantic = new SemanticVersion(1, 2, 3, "build-0001"); + + Assert.Equal(semantic.SpecialVersion, "build-0001"); + } + + [Fact] + public void SemanticVersion_GivenValidVersion_ReturnsCorrectObject() + { + var semantic = new SemanticVersion(new Version(1,2,3)); + + Assert.True(semantic.Version.Major == 1); + Assert.True(semantic.Version.Minor == 2); + Assert.True(semantic.Version.Build == 3); + } + + } + + +} diff --git a/src/BikeshareClient/TestBikeshareClient/Helpers/TestStringToSemanticVersionConverter.cs b/src/BikeshareClient/TestBikeshareClient/Helpers/TestStringToSemanticVersionConverter.cs new file mode 100644 index 0000000..9630101 --- /dev/null +++ b/src/BikeshareClient/TestBikeshareClient/Helpers/TestStringToSemanticVersionConverter.cs @@ -0,0 +1,48 @@ +using Xunit; +using Newtonsoft.Json; +using BikeshareClient.DTO; +using System.IO; +using BikeshareClient.Helpers; +using System; + +namespace TestBikeshareClient.Helpers +{ + public class TestStringToSemanticVersionConverter + { + private readonly string TestFile = @"Helpers/TestableGbfsJson.json"; + + [Fact] + public void ReadJson_GivenValidJSON_ReturnsSemanticVersion() + { + var gbfsObject = JsonConvert.DeserializeObject(File.ReadAllText(TestFile)); + + Assert.True(gbfsObject.Version.Version.Major == 1); + Assert.True(gbfsObject.Version.Version.Minor == 5); + } + + [Fact] + public void ReadJson_GivenEmptyString_ReturnsDefaultVersion() + { + var testString = @"{ ""version"": """" }"; + + var testObject = JsonConvert.DeserializeObject(testString); + + Assert.True(testObject.Version.Version.Major == 1); + Assert.True(testObject.Version.Version.Minor == 0); + } + + [Fact] + public void WriteJson_GivenSemanticVersion_ThrowsNotImplementedException() + { + var testObject = new TestObject { Version = new SemanticVersion("1.2.3") }; + + Assert.Throws(() => JsonConvert.SerializeObject(testObject)); + } + + private class TestObject + { + [JsonProperty("version"), JsonConverter(typeof(StringToSemanticVersionConverter))] + public SemanticVersion Version { get; set; } + } + } +} diff --git a/src/BikeshareClient/TestBikeshareClient/Helpers/TestTrimmingConverter.cs b/src/BikeshareClient/TestBikeshareClient/Helpers/TestTrimmingConverter.cs index 5f7d730..cf42458 100644 --- a/src/BikeshareClient/TestBikeshareClient/Helpers/TestTrimmingConverter.cs +++ b/src/BikeshareClient/TestBikeshareClient/Helpers/TestTrimmingConverter.cs @@ -10,7 +10,7 @@ public class TestTrimmingConverter { [Fact] - public void TrimmingConverter_GivenJsonWithWhiteSpace_TrimsTestProperty() + public void ReadJson_GivenJsonWithWhiteSpace_TrimsTestProperty() { string testJsonString = "{\"key\": \"Value \"}"; @@ -20,7 +20,7 @@ public void TrimmingConverter_GivenJsonWithWhiteSpace_TrimsTestProperty() } [Fact] - public void TrimmingConverter_GivenStationInformationJsonWithTrailingSpace_TrimsAddress() + public void ReadJson_GivenStationInformationJsonWithTrailingSpace_TrimsAddress() { string testStationInformationJson = "{\"station_id\": \"293\", \"name\": \"S. P. Andersens vei \", \"address\": \"S. P. Andersens vei\", \"lat\": 63.409888448864876, \"lon\": 10.405212902563903, \"capacity\": 18}"; diff --git a/src/BikeshareClient/TestBikeshareClient/Helpers/TestableGbfsJson.json b/src/BikeshareClient/TestBikeshareClient/Helpers/TestableGbfsJson.json index da6efb5..22dc3c1 100644 --- a/src/BikeshareClient/TestBikeshareClient/Helpers/TestableGbfsJson.json +++ b/src/BikeshareClient/TestBikeshareClient/Helpers/TestableGbfsJson.json @@ -1,6 +1,7 @@ { "last_updated": 1540822114, "ttl": 10, + "version": "1.5", "data": { "ar": { "feeds": [ diff --git a/src/BikeshareClient/TestBikeshareClient/ProvidersTests/TestBikeShareDataProvider.cs b/src/BikeshareClient/TestBikeshareClient/ProvidersTests/TestBikeShareDataProvider.cs index f7bd482..cb2198c 100644 --- a/src/BikeshareClient/TestBikeshareClient/ProvidersTests/TestBikeShareDataProvider.cs +++ b/src/BikeshareClient/TestBikeshareClient/ProvidersTests/TestBikeShareDataProvider.cs @@ -4,6 +4,8 @@ using BikeshareClient.DTO; using Xunit; using System.Net.Http; +using BikeshareClient.Helpers; + namespace TestBikeshareClient { public class TestBikeShareDataProvider @@ -97,6 +99,15 @@ public async Task GetBikeShareData_GivenWrongBaseUrl_ThrowsNotImplementetdExcept await Assert.ThrowsAsync(async () => await dataProvider.GetBikeShareData()); } + [Fact] + public async Task GetBikeShareData_GivenBaseUrlWithoutVersionAttribute_ReturnsDefaultVersion() + { + var defaultVersion = new SemanticVersion("1.0"); + var dataProvider = new BikeShareDataProvider("http://gbfs.urbansharing.com/trondheim/"); + + var gbfsDTO = await dataProvider.GetBikeShareData(); + Assert.True(Equals(gbfsDTO.Version, defaultVersion)); + } } } diff --git a/src/BikeshareClient/TestBikeshareClient/TestBikeShareClient.cs b/src/BikeshareClient/TestBikeshareClient/TestBikeShareClient.cs index b264cd4..f3fe094 100644 --- a/src/BikeshareClient/TestBikeshareClient/TestBikeShareClient.cs +++ b/src/BikeshareClient/TestBikeshareClient/TestBikeShareClient.cs @@ -142,7 +142,6 @@ public async Task GetSystemInformationAsync_GivenValidBaseUrl_ReturnsInformation [Theory] [InlineData(@"http://hamilton.socialbicycles.com/opendata/")] [InlineData(@"http://coast.socialbicycles.com/opendata/")] - [InlineData(@"https://reddybikeshare.socialbicycles.com/opendata/gbfs.json")] public async Task GetBikeStatusAsync_GivenCorrectBaseUrl_ReturnsBikesStatus(string endpoint) { var client = new Client(endpoint); @@ -167,7 +166,6 @@ public async Task GetBikeStatusAsync_GivenCorrectBaseUrlWithGbfsDiscoveryFile_Re [Theory] [InlineData(@"http://hamilton.socialbicycles.com/opendata/gbfs.json")] [InlineData(@"http://coast.socialbicycles.com/opendata/gbfs.json")] - [InlineData(@"https://reddybikeshare.socialbicycles.com/opendata/gbfs.json")] public async Task GetBikeStatusAsync_GivenCorrectBaseUrlWithGbfsDiscoveryFile_ReturnsValidPropertyValues(string endpoint) { var client = new Client(endpoint);