From de0e118c9846a5040d621b253976d802b206b7c1 Mon Sep 17 00:00:00 2001 From: zelodyc Date: Tue, 2 Mar 2021 00:42:52 -0800 Subject: [PATCH 01/14] Create Agiles model --- src/YouTrackSharp/Agiles/Agile.cs | 126 ++++++++++++++++++ src/YouTrackSharp/Agiles/AgileColumn.cs | 45 +++++++ .../Agiles/AgileColumnFieldValue.cs | 20 +++ src/YouTrackSharp/Agiles/AgileStatus.cs | 40 ++++++ .../Agiles/AttributeBasedSwimlaneSettings.cs | 22 +++ src/YouTrackSharp/Agiles/ColorCoding.cs | 15 +++ src/YouTrackSharp/Agiles/ColumnSettings.cs | 28 ++++ src/YouTrackSharp/Agiles/CustomFilterField.cs | 15 +++ .../Agiles/DatabaseAttributeValue.cs | 14 ++ .../Agiles/FieldBasedColorCoding.cs | 15 +++ src/YouTrackSharp/Agiles/FieldStyle.cs | 26 ++++ src/YouTrackSharp/Agiles/FilterField.cs | 26 ++++ .../Agiles/IssueBasedSwimlaneSettings.cs | 27 ++++ .../Agiles/PredefinedFilterField.cs | 8 ++ src/YouTrackSharp/Agiles/Project.cs | 27 ++++ .../Agiles/ProjectBasedColorCoding.cs | 15 +++ src/YouTrackSharp/Agiles/ProjectColor.cs | 26 ++++ src/YouTrackSharp/Agiles/Sprint.cs | 21 +++ src/YouTrackSharp/Agiles/SprintsSettings.cs | 62 +++++++++ .../Agiles/SwimlaneEntityAttributeValue.cs | 21 +++ src/YouTrackSharp/Agiles/SwimlaneSettings.cs | 20 +++ src/YouTrackSharp/Agiles/SwimlaneValue.cs | 20 +++ src/YouTrackSharp/Agiles/User.cs | 28 ++++ src/YouTrackSharp/Agiles/WIPLimit.cs | 26 ++++ 24 files changed, 693 insertions(+) create mode 100644 src/YouTrackSharp/Agiles/Agile.cs create mode 100644 src/YouTrackSharp/Agiles/AgileColumn.cs create mode 100644 src/YouTrackSharp/Agiles/AgileColumnFieldValue.cs create mode 100644 src/YouTrackSharp/Agiles/AgileStatus.cs create mode 100644 src/YouTrackSharp/Agiles/AttributeBasedSwimlaneSettings.cs create mode 100644 src/YouTrackSharp/Agiles/ColorCoding.cs create mode 100644 src/YouTrackSharp/Agiles/ColumnSettings.cs create mode 100644 src/YouTrackSharp/Agiles/CustomFilterField.cs create mode 100644 src/YouTrackSharp/Agiles/DatabaseAttributeValue.cs create mode 100644 src/YouTrackSharp/Agiles/FieldBasedColorCoding.cs create mode 100644 src/YouTrackSharp/Agiles/FieldStyle.cs create mode 100644 src/YouTrackSharp/Agiles/FilterField.cs create mode 100644 src/YouTrackSharp/Agiles/IssueBasedSwimlaneSettings.cs create mode 100644 src/YouTrackSharp/Agiles/PredefinedFilterField.cs create mode 100644 src/YouTrackSharp/Agiles/Project.cs create mode 100644 src/YouTrackSharp/Agiles/ProjectBasedColorCoding.cs create mode 100644 src/YouTrackSharp/Agiles/ProjectColor.cs create mode 100644 src/YouTrackSharp/Agiles/Sprint.cs create mode 100644 src/YouTrackSharp/Agiles/SprintsSettings.cs create mode 100644 src/YouTrackSharp/Agiles/SwimlaneEntityAttributeValue.cs create mode 100644 src/YouTrackSharp/Agiles/SwimlaneSettings.cs create mode 100644 src/YouTrackSharp/Agiles/SwimlaneValue.cs create mode 100644 src/YouTrackSharp/Agiles/User.cs create mode 100644 src/YouTrackSharp/Agiles/WIPLimit.cs diff --git a/src/YouTrackSharp/Agiles/Agile.cs b/src/YouTrackSharp/Agiles/Agile.cs new file mode 100644 index 0000000..b03955a --- /dev/null +++ b/src/YouTrackSharp/Agiles/Agile.cs @@ -0,0 +1,126 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using YouTrackSharp.Management; +using YouTrackSharp.Projects; + +namespace YouTrackSharp.Agiles { + /// + /// Represents an agile board configuration. + /// + public class Agile { + /// + /// Id of the Agile. + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// The name of the agile board. Can be null. + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// Owner of the agile board. Can be null. + /// + [JsonProperty("owner")] + public User Owner { get; set; } + + /// + /// The user group that can view this board. Can be null. + /// + [JsonProperty("visibleFor")] + public Group VisibleFor { get; set; } + + /// + /// When true, the board is visible to everyone who can view all projects that are associated with the board. + /// + [JsonProperty("visibleForProjectBased")] + public bool VisibleForProjectBased { get; set; } + + /// + /// Group of users who can update board settings. Can be null. + /// + [JsonProperty("updateableBy")] + public Group UpdateableBy { get; set; } + + /// + /// When true, anyone who can update the associated projects can update the board. + /// + [JsonProperty("updateableByProjectBased")] + public bool UpdateableByProjectBased { get; set; } + + /// + /// When true, the orphan swimlane is placed at the top of the board. Otherwise, the orphans swimlane is located + /// below all other swimlanes. + /// + [JsonProperty("orphansAtTheTop")] + public bool OrphansAtTheTop { get; set; } + + /// + /// When true, the orphans swimlane is not displayed on the board. + /// + [JsonProperty("hideOrphansSwimlane")] + public bool HideOrphansSwimlane { get; set; } + + /// + /// A custom field that is used as the estimation field for the board. Can be null. + /// + [JsonProperty("estimationField")] + public CustomField EstimationField { get; set; } + + /// + /// A custom field that is used as the original estimation field for the board. Can be null. + /// + [JsonProperty("originalEstimationField")] + public CustomField OriginalEstimationField { get; set; } + + /// + /// A collection of projects associated with the board. + /// + [JsonProperty("projects")] + public List Projects { get; set; } + + /// + /// The set of sprints that are associated with the board. + /// + [JsonProperty("sprints")] + public List Sprints { get; set; } + + /// + /// A sprint that is actual for the current date. Read-only. Can be null. + /// + [JsonProperty("currentSprint")] + public Sprint CurrentSprint { get; set; } + + /// + /// Column settings of the board. Read-only. + /// + [JsonProperty("columnSettings")] + public ColumnSettings ColumnSettings { get; set; } + + /// + /// Settings of the board swimlanes. Can be null. + /// + [JsonProperty("swimlaneSettings")] + public SwimlaneSettings SwimlaneSettings { get; set; } + + /// + /// Settings of the board sprints. Read-only. + /// + [JsonProperty("sprintsSettings")] + public SprintsSettings SprintsSettings { get; set; } + + /// + /// Color coding settings for the board. Can be null. + /// + [JsonProperty("colorCoding")] + public ColorCoding ColorCoding { get; set; } + + /// + /// Status of the board. Read-only. + /// + [JsonProperty("status")] + public AgileStatus Status { get; set; } + } +} diff --git a/src/YouTrackSharp/Agiles/AgileColumn.cs b/src/YouTrackSharp/Agiles/AgileColumn.cs new file mode 100644 index 0000000..07b25d6 --- /dev/null +++ b/src/YouTrackSharp/Agiles/AgileColumn.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace YouTrackSharp.Agiles { + /// + /// Represents settings for a single board column + /// + public class AgileColumn { + /// + /// Id of the AgileColumn. + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// Text presentation of values stored in a column. Read-only. Can be null. + /// + [JsonProperty("presentation")] + public string Presentation { get; set; } + + /// + /// true if a column represents resolved state of an issue. Can be updated only for newly created value. Read-only. + /// + [JsonProperty("isResolved")] + public bool IsResolved { get; set; } + + /// + /// Order of this column on board, counting from left to right. + /// + [JsonProperty("ordinal")] + public int Ordinal { get; set; } + + /// + /// WIP limit for this column. Can be null. + /// + [JsonProperty("wipLimit")] + public WIPLimit WipLimit { get; set; } + + /// + /// Field values represented by this column. + /// + [JsonProperty("fieldValues")] + public List FieldValues { get; set; } + } +} diff --git a/src/YouTrackSharp/Agiles/AgileColumnFieldValue.cs b/src/YouTrackSharp/Agiles/AgileColumnFieldValue.cs new file mode 100644 index 0000000..44cb647 --- /dev/null +++ b/src/YouTrackSharp/Agiles/AgileColumnFieldValue.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; + +namespace YouTrackSharp.Agiles { + /// + /// Represents a field value or values, parameterizing agile column. + /// + public class AgileColumnFieldValue : DatabaseAttributeValue { + /// + /// Presentation of a field value or values. Can be null. + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// True, if field has type State and teh value is resolved or all values are resolved. Read-only. + /// + [JsonProperty("isResolved")] + public bool IsResolved { get; set; } + } +} diff --git a/src/YouTrackSharp/Agiles/AgileStatus.cs b/src/YouTrackSharp/Agiles/AgileStatus.cs new file mode 100644 index 0000000..fe13538 --- /dev/null +++ b/src/YouTrackSharp/Agiles/AgileStatus.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace YouTrackSharp.Agiles { + /// + /// Shows if the board has any configuration problems. + /// + public class AgileStatus { + /// + /// Id of the AgileStatus. + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// true if the board is in valid state and can be used. Read-only. + /// + [JsonProperty("valid")] + public bool Valid { get; set; } + + /// + /// If `true`, then a background job is currently being executed for the board. In this case, while a background + /// job is running, the board cannot be updated. Read-only. + /// + [JsonProperty("hasJobs")] + public bool HasJobs { get; set; } + + /// + /// List of configuration errors found for this board. Read-only. + /// + [JsonProperty("errors")] + public List Errors { get; set; } + + /// + /// List of configuration-related warnings found for this board. Read-only. + /// + [JsonProperty("warnings")] + public List Warnings { get; set; } + } +} diff --git a/src/YouTrackSharp/Agiles/AttributeBasedSwimlaneSettings.cs b/src/YouTrackSharp/Agiles/AttributeBasedSwimlaneSettings.cs new file mode 100644 index 0000000..ef03972 --- /dev/null +++ b/src/YouTrackSharp/Agiles/AttributeBasedSwimlaneSettings.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace YouTrackSharp.Agiles { + /// + /// Settings of swimlanes that are identified by the set of values in the selected field. For example, you can set + /// swimlanes to represent issues for each Assignee. + /// + public class AttributeBasedSwimlaneSettings : SwimlaneSettings { + /// + /// CustomField which values are used to identify swimlane. + /// + [JsonProperty("field")] + public FilterField Field { get; set; } + + /// + /// Swimlanes that are visible on the Board. + /// + [JsonProperty("values")] + public List Values { get; set; } + } +} diff --git a/src/YouTrackSharp/Agiles/ColorCoding.cs b/src/YouTrackSharp/Agiles/ColorCoding.cs new file mode 100644 index 0000000..a235c76 --- /dev/null +++ b/src/YouTrackSharp/Agiles/ColorCoding.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; + +namespace YouTrackSharp.Agiles { + + /// + /// Describe rules according to which different colors are used for cards on agile board. + /// + public class ColorCoding { + /// + /// Id of the ColorCoding. + /// + [JsonProperty("id")] + public string Id { get; set; } + } +} diff --git a/src/YouTrackSharp/Agiles/ColumnSettings.cs b/src/YouTrackSharp/Agiles/ColumnSettings.cs new file mode 100644 index 0000000..69f8514 --- /dev/null +++ b/src/YouTrackSharp/Agiles/ColumnSettings.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using YouTrackSharp.Projects; + +namespace YouTrackSharp.Agiles { + /// + /// Agile board columns settings. + /// + public class ColumnSettings { + /// + /// Id of the ColumnSettings. + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// Custom field, which values are used for columns. Can be null. + /// + [JsonProperty("field")] + public CustomField Field { get; set; } + + /// + /// Columns that are shown on the board. + /// + [JsonProperty("columns")] + public List Columns { get; set; } + } +} diff --git a/src/YouTrackSharp/Agiles/CustomFilterField.cs b/src/YouTrackSharp/Agiles/CustomFilterField.cs new file mode 100644 index 0000000..9a80aae --- /dev/null +++ b/src/YouTrackSharp/Agiles/CustomFilterField.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; +using YouTrackSharp.Projects; + +namespace YouTrackSharp.Agiles { + /// + /// Represents a custom field of the issue. + /// + public class CustomFilterField : FilterField { + /// + /// Reference to settings of the custom field. Read-only. + /// + [JsonProperty("customField")] + public CustomField CustomField { get; set; } + } +} diff --git a/src/YouTrackSharp/Agiles/DatabaseAttributeValue.cs b/src/YouTrackSharp/Agiles/DatabaseAttributeValue.cs new file mode 100644 index 0000000..2a5f786 --- /dev/null +++ b/src/YouTrackSharp/Agiles/DatabaseAttributeValue.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace YouTrackSharp.Agiles { + /// + /// Represents string reference to the value. + /// + public class DatabaseAttributeValue { + /// + /// Id of the DatabaseAttributeValue. + /// + [JsonProperty("id")] + public string Id { get; set; } + } +} diff --git a/src/YouTrackSharp/Agiles/FieldBasedColorCoding.cs b/src/YouTrackSharp/Agiles/FieldBasedColorCoding.cs new file mode 100644 index 0000000..96ec5a8 --- /dev/null +++ b/src/YouTrackSharp/Agiles/FieldBasedColorCoding.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; +using YouTrackSharp.Projects; + +namespace YouTrackSharp.Agiles { + /// + /// Allows to set card's color based on a value of some custom field. + /// + public class FieldBasedColorCoding : ColorCoding { + /// + /// Sets card color based on this custom field. Can be null. + /// + [JsonProperty("prototype")] + public CustomField Prototype { get; set; } + } +} diff --git a/src/YouTrackSharp/Agiles/FieldStyle.cs b/src/YouTrackSharp/Agiles/FieldStyle.cs new file mode 100644 index 0000000..aef18f0 --- /dev/null +++ b/src/YouTrackSharp/Agiles/FieldStyle.cs @@ -0,0 +1,26 @@ +using Newtonsoft.Json; + +namespace YouTrackSharp.Agiles { + /// + /// Represents the style settings of the field in YouTrack. + /// + public class FieldStyle { + /// + /// Id of the FieldStyle. + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// Background color. Read-only. Can be null. + /// + [JsonProperty("background")] + public string Background { get; set; } + + /// + /// Foreground color. Read-only. Can be null. + /// + [JsonProperty("foreground")] + public string Foreground { get; set; } + } +} diff --git a/src/YouTrackSharp/Agiles/FilterField.cs b/src/YouTrackSharp/Agiles/FilterField.cs new file mode 100644 index 0000000..fc4a4b3 --- /dev/null +++ b/src/YouTrackSharp/Agiles/FilterField.cs @@ -0,0 +1,26 @@ +using Newtonsoft.Json; + +namespace YouTrackSharp.Agiles { + /// + /// Represents an issue property, which can be a predefined field, a custom field, a link, and so on. + /// + public class FilterField { + /// + /// Id of the FilterField. + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// Presentation of the field. Read-only. + /// + [JsonProperty("presentation")] + public string Presentation { get; set; } + + /// + /// The name of the field. Read-only. + /// + [JsonProperty("name")] + public string Name { get; set; } + } +} diff --git a/src/YouTrackSharp/Agiles/IssueBasedSwimlaneSettings.cs b/src/YouTrackSharp/Agiles/IssueBasedSwimlaneSettings.cs new file mode 100644 index 0000000..6879d3e --- /dev/null +++ b/src/YouTrackSharp/Agiles/IssueBasedSwimlaneSettings.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace YouTrackSharp.Agiles { + /// + /// Base entity for different swimlane settings + /// + public class IssueBasedSwimlaneSettings : SwimlaneSettings { + /// + /// CustomField which values are used to identify swimlane. + /// + [JsonProperty("field")] + public FilterField Field { get; set; } + + /// + /// Value of a field that a card would have by default. Can be null. + /// + [JsonProperty("defaultCardType")] + public SwimlaneValue DefaultCardType { get; set; } + + /// + /// When issue has one of this values, it becomes a swimlane on this board. + /// + [JsonProperty("values")] + public List Values { get; set; } + } +} diff --git a/src/YouTrackSharp/Agiles/PredefinedFilterField.cs b/src/YouTrackSharp/Agiles/PredefinedFilterField.cs new file mode 100644 index 0000000..34731b7 --- /dev/null +++ b/src/YouTrackSharp/Agiles/PredefinedFilterField.cs @@ -0,0 +1,8 @@ +namespace YouTrackSharp.Agiles { + /// + /// Represents a predefined field of the issue. Predefined fields are always present in an issue and |cannot be + /// customized in a project. For example, project, created, |updated, tags, and so on. + /// + public class PredefinedFilterField : FilterField { + } +} diff --git a/src/YouTrackSharp/Agiles/Project.cs b/src/YouTrackSharp/Agiles/Project.cs new file mode 100644 index 0000000..2b3dce0 --- /dev/null +++ b/src/YouTrackSharp/Agiles/Project.cs @@ -0,0 +1,27 @@ +using Newtonsoft.Json; + +namespace YouTrackSharp.Agiles { + /// + /// Represents a project in the context of an agile board. The class only provides the id, short name and name of the + /// project see for more info on how to access a YouTrack project. + /// + public class Project { + /// + /// Id of the Project. + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// The ID of the project. This short name is also a prefix for an issue ID. Can be null. + /// + [JsonProperty("shortName")] + public string ShortName { get; set; } + + /// + /// The name of the issue folder. Can be null. + /// + [JsonProperty("name")] + public string Name { get; set; } + } +} diff --git a/src/YouTrackSharp/Agiles/ProjectBasedColorCoding.cs b/src/YouTrackSharp/Agiles/ProjectBasedColorCoding.cs new file mode 100644 index 0000000..b8dc5ab --- /dev/null +++ b/src/YouTrackSharp/Agiles/ProjectBasedColorCoding.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace YouTrackSharp.Agiles { + /// + /// Allows to set card's color based on it's project + /// + public class ProjectBasedColorCoding : ColorCoding { + /// + /// Collection of per-project color settings + /// + [JsonProperty("projectColors")] + public List ProjectColors { get; set; } + } +} diff --git a/src/YouTrackSharp/Agiles/ProjectColor.cs b/src/YouTrackSharp/Agiles/ProjectColor.cs new file mode 100644 index 0000000..cd76c44 --- /dev/null +++ b/src/YouTrackSharp/Agiles/ProjectColor.cs @@ -0,0 +1,26 @@ +using Newtonsoft.Json; + +namespace YouTrackSharp.Agiles { + /// + /// Represent color setting for one project on the board. + /// + public class ProjectColor { + /// + /// Id of the ProjectColor. + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// A project, to which color this setting describes. Read-only. Can be null. + /// + [JsonProperty("project")] + public Project Project { get; set; } + + /// + /// A color, that issues of this project will have on the board. Read-only. Can be null. + /// + [JsonProperty("color")] + public FieldStyle Color { get; set; } + } +} diff --git a/src/YouTrackSharp/Agiles/Sprint.cs b/src/YouTrackSharp/Agiles/Sprint.cs new file mode 100644 index 0000000..d621a96 --- /dev/null +++ b/src/YouTrackSharp/Agiles/Sprint.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; + +namespace YouTrackSharp.Agiles { + /// + /// Represents a sprint in the context of an agile board. The class only provides the sprint's id and name, see for more info on how to access a YouTrack sprint. + /// + public class Sprint { + /// + /// Id of the Sprint. + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// Name of the sprint. Can be null. + /// + [JsonProperty("name")] + public string Name { get; set; } + } +} diff --git a/src/YouTrackSharp/Agiles/SprintsSettings.cs b/src/YouTrackSharp/Agiles/SprintsSettings.cs new file mode 100644 index 0000000..cc3de8b --- /dev/null +++ b/src/YouTrackSharp/Agiles/SprintsSettings.cs @@ -0,0 +1,62 @@ +using Newtonsoft.Json; +using YouTrackSharp.Projects; + +namespace YouTrackSharp.Agiles { + /// + /// Describes sprints configuration. + /// + public class SprintsSettings { + /// + /// Id of the SprintsSettings. + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// If true, issues should be added to the board manually. If false, issues are shown on the board based on the + /// query and/or value of a field. + /// + [JsonProperty("isExplicit")] + public bool IsExplicit { get; set; } + + /// + /// If true, cards can be present on several sprints of this board. + /// + [JsonProperty("cardOnSeveralSprints")] + public bool CardOnSeveralSprints { get; set; } + + /// + /// New cards are added to this sprint by default. This setting applies only if isExplicit == true. Can be null. + /// + [JsonProperty("defaultSprint")] + public Sprint DefaultSprint { get; set; } + + /// + /// If true, agile board has no distinct sprints in UI. However, in API it will look like it has only one active + /// (not-archived) sprint. + /// + [JsonProperty("disableSprints")] + public bool DisableSprints { get; set; } + + /// + /// Issues that match this query will appear on the board. This setting applies only if isExplicit == false. Can be + /// null. + /// + [JsonProperty("explicitQuery")] + public string ExplicitQuery { get; set; } + + /// + /// Based on the value of this field, issues will be assigned to the sprints. This setting applies only if + /// isExplicit == false. Can be null. + /// + [JsonProperty("sprintSyncField")] + public CustomField SprintSyncField { get; set; } + + /// + /// If true, subtasks of the cards, that are present on the board, will be hidden if they match board query. This + /// setting applies only if isExplicit == false. + /// + [JsonProperty("hideSubtasksOfCards")] + public bool HideSubtasksOfCards { get; set; } + } +} diff --git a/src/YouTrackSharp/Agiles/SwimlaneEntityAttributeValue.cs b/src/YouTrackSharp/Agiles/SwimlaneEntityAttributeValue.cs new file mode 100644 index 0000000..2b54a6f --- /dev/null +++ b/src/YouTrackSharp/Agiles/SwimlaneEntityAttributeValue.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; + +namespace YouTrackSharp.Agiles { + /// + /// Represents a single swimlane in case of AttributeBasedSwimlaneSettings. + /// + public class SwimlaneEntityAttributeValue : DatabaseAttributeValue { + /// + /// Name of the swimlane. Can be null. + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// If true, issues in this swimlane are considered to be resolved. Can be updated only for newly created value. + /// Read-only. + /// + [JsonProperty("isResolved")] + public bool IsResolved { get; set; } + } +} diff --git a/src/YouTrackSharp/Agiles/SwimlaneSettings.cs b/src/YouTrackSharp/Agiles/SwimlaneSettings.cs new file mode 100644 index 0000000..dbc4669 --- /dev/null +++ b/src/YouTrackSharp/Agiles/SwimlaneSettings.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; + +namespace YouTrackSharp.Agiles { + /// + /// Base entity for different swimlane settings + /// + public class SwimlaneSettings { + /// + /// Id of the SwimlaneSettings. + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// Name of a value. Read-only. Can be null. + /// + [JsonProperty("name")] + public string Name { get; set; } + } +} diff --git a/src/YouTrackSharp/Agiles/SwimlaneValue.cs b/src/YouTrackSharp/Agiles/SwimlaneValue.cs new file mode 100644 index 0000000..fab9436 --- /dev/null +++ b/src/YouTrackSharp/Agiles/SwimlaneValue.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; + +namespace YouTrackSharp.Agiles { + /// + /// Represents single swimlane in case of IssueBasedSwimlaneSettings. + /// + public class SwimlaneValue { + /// + /// Id of the SwimlaneValue. + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// Name of a value. Read-only. Can be null. + /// + [JsonProperty("name")] + public string Name { get; set; } + } +} diff --git a/src/YouTrackSharp/Agiles/User.cs b/src/YouTrackSharp/Agiles/User.cs new file mode 100644 index 0000000..4f76d16 --- /dev/null +++ b/src/YouTrackSharp/Agiles/User.cs @@ -0,0 +1,28 @@ +using Newtonsoft.Json; + +namespace YouTrackSharp.Agiles { + /// + /// Represents a user in the context of an agile board. The class only provides the id, ringId (hub ID) and full name + /// of the user, see for more info on how to access a YouTrack user. + /// + public class User { + /// + /// Id of the User. + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// ID of the user in Hub. You can use this ID for operations in Hub, and for matching users between YouTrack and + /// Hub. Read-only. Can be null. + /// + [JsonProperty("ringId")] + public string RingId { get; set; } + + /// + /// Full name of the user. + /// + [JsonProperty("fullName")] + public string FullName { get; set; } + } +} diff --git a/src/YouTrackSharp/Agiles/WIPLimit.cs b/src/YouTrackSharp/Agiles/WIPLimit.cs new file mode 100644 index 0000000..352e4cb --- /dev/null +++ b/src/YouTrackSharp/Agiles/WIPLimit.cs @@ -0,0 +1,26 @@ +using Newtonsoft.Json; + +namespace YouTrackSharp.Agiles { + /// + /// Represents WIP limits for particular column. If they are not satisfied, the column will be highlighted in UI. + /// + public class WIPLimit { + /// + /// Id of the WIPLimit. + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// Maximum number of cards in column. Can be null. + /// + [JsonProperty("max")] + public int Max { get; set; } + + /// + /// Minimum number of cards in column. Can be null. + /// + [JsonProperty("min")] + public int Min { get; set; } + } +} From b70309dc4e3d6e31ddf50c49ee1721bb344c2d5f Mon Sep 17 00:00:00 2001 From: zelodyc Date: Tue, 2 Mar 2021 00:46:45 -0800 Subject: [PATCH 02/14] Add serialization / deserialization / url encoding attributes for known sub-types and verbose-only fields --- .../KnownTypeAttribute.cs | 24 +++++++++++++++++++ .../VerboseAttribute.cs | 9 +++++++ 2 files changed, 33 insertions(+) create mode 100644 src/YouTrackSharp/SerializationAttributes/KnownTypeAttribute.cs create mode 100644 src/YouTrackSharp/SerializationAttributes/VerboseAttribute.cs diff --git a/src/YouTrackSharp/SerializationAttributes/KnownTypeAttribute.cs b/src/YouTrackSharp/SerializationAttributes/KnownTypeAttribute.cs new file mode 100644 index 0000000..dd3362f --- /dev/null +++ b/src/YouTrackSharp/SerializationAttributes/KnownTypeAttribute.cs @@ -0,0 +1,24 @@ +using System; + +namespace YouTrackSharp.SerializationAttributes { + /// + /// Attribute used to decorate a parent class with its known subtypes.
+ /// This is used to identify candidate subclasses when generating REST requests (to include the subclasses' + /// properties). + ///
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public class KnownTypeAttribute : Attribute { + /// + /// Known subclass's type + /// + public Type Type { get; } + + /// + /// Creates an instance of the , with the given subclass' type. + /// + /// + public KnownTypeAttribute(Type type) { + Type = type; + } + } +} \ No newline at end of file diff --git a/src/YouTrackSharp/SerializationAttributes/VerboseAttribute.cs b/src/YouTrackSharp/SerializationAttributes/VerboseAttribute.cs new file mode 100644 index 0000000..6378eac --- /dev/null +++ b/src/YouTrackSharp/SerializationAttributes/VerboseAttribute.cs @@ -0,0 +1,9 @@ +using System; + +namespace YouTrackSharp.SerializationAttributes { + /// + /// This attribute is used to mark a property as being only retrieved when verbose information was requested. + /// + [AttributeUsage(AttributeTargets.Property)] + public class VerboseAttribute : Attribute { } +} \ No newline at end of file From fcaed30949b4920f8b85919963c22d166a210ca9 Mon Sep 17 00:00:00 2001 From: zelodyc Date: Tue, 2 Mar 2021 01:18:58 -0800 Subject: [PATCH 03/14] Add support for json deserialization of polymorphic fields (to concrete sub-types) --- src/YouTrackSharp/Json/KnownTypeConverter.cs | 60 +++++++++ .../Json/TypedJObjectConverter.cs | 26 ++++ .../Json/KnownTypeConverterTestFixtures.cs | 65 +++++++++ .../Json/KnownTypeConverterTests.cs | 123 ++++++++++++++++++ 4 files changed, 274 insertions(+) create mode 100644 src/YouTrackSharp/Json/KnownTypeConverter.cs create mode 100644 src/YouTrackSharp/Json/TypedJObjectConverter.cs create mode 100644 tests/YouTrackSharp.Tests/Json/KnownTypeConverterTestFixtures.cs create mode 100644 tests/YouTrackSharp.Tests/Json/KnownTypeConverterTests.cs diff --git a/src/YouTrackSharp/Json/KnownTypeConverter.cs b/src/YouTrackSharp/Json/KnownTypeConverter.cs new file mode 100644 index 0000000..d8eee6f --- /dev/null +++ b/src/YouTrackSharp/Json/KnownTypeConverter.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using YouTrackSharp.SerializationAttributes; + +namespace YouTrackSharp.Json { + /// + /// This allows to convert a JSON string into a concrete object that extends a given base + /// type.
+ /// This is used in conjunction with the , defined on + /// the base class, and which lists the possible sub-types of that class.

+ /// Among these, the concrete sub-type instantiated is inferred from the Json object's "$type" field, which is + /// compared to the defined known types (ignoring their namespace).
+ /// If the "$type" field is undefined, or no matching the "$type" parameter is + /// found, the base class is instantiated instead. + ///
+ /// Base type + public class KnownTypeConverter : JsonConverter where T:new() { + private readonly TypedJObjectConverter _objectConverter; + + public KnownTypeConverter() { + _objectConverter = new TypedJObjectConverter(); + } + + /// + public override void WriteJson(JsonWriter writer, T value, JsonSerializer serializer) { + throw new NotImplementedException(); + } + + /// + /// Reads the JSON representation of the object.
+ /// This method will instanciate a subclass of the given , based on the "$type" parameter + /// of the json string, which is compared to defined for that base class.
+ /// If the "$type" parameter is not defined, or no matching is found, the json is + /// deserialized to an instance of the base class directly. + ///
+ /// The to read from. + /// Type of the object. + /// The existing value of object being read. If there is no existing value then null will be used. + /// The existing value has a value. + /// The calling serializer. + /// The object value. + public override T ReadJson(JsonReader reader, Type objectType, T existingValue, bool hasExistingValue, JsonSerializer serializer) { + List types = typeof(T).GetCustomAttributes().Select(attr => attr.Type).ToList(); + + JObject obj = JObject.Load(reader); + + return _objectConverter.ReadObject(obj, types, serializer); + } + + /// + public override bool CanRead => true; + + /// + public override bool CanWrite => false; + } +} \ No newline at end of file diff --git a/src/YouTrackSharp/Json/TypedJObjectConverter.cs b/src/YouTrackSharp/Json/TypedJObjectConverter.cs new file mode 100644 index 0000000..5a56f59 --- /dev/null +++ b/src/YouTrackSharp/Json/TypedJObjectConverter.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace YouTrackSharp.Json { + public class TypedJObjectConverter { + public T ReadObject(JObject token, List knownTypes, JsonSerializer serializer) { + if (token == null) throw new ArgumentException("Invalid token"); + + string jsonType = token["$type"]?.ToString(); + if (jsonType == null) { + return token.ToObject(serializer); + } + + Type type = knownTypes.FirstOrDefault(t => t.Name.EndsWith(jsonType)); + + if (type == null) { + return token.ToObject(serializer); + } + + return (T)token.ToObject(type, serializer); + } + } +} \ No newline at end of file diff --git a/tests/YouTrackSharp.Tests/Json/KnownTypeConverterTestFixtures.cs b/tests/YouTrackSharp.Tests/Json/KnownTypeConverterTestFixtures.cs new file mode 100644 index 0000000..c3a70e7 --- /dev/null +++ b/tests/YouTrackSharp.Tests/Json/KnownTypeConverterTestFixtures.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; +using Newtonsoft.Json; +using YouTrackSharp.Json; +using YouTrackSharp.SerializationAttributes; + +namespace YouTrackSharp.Tests.Json { + [UsedImplicitly] + public class KnownTypeConverterTestFixtures { + public class Json { + public string GetJsonForChildA(int id, string name) { + return $"{{ \"id\": {id}, \"name\": \"{name}\", \"$type\": \"ChildA\" }}"; + } + + public string GetJsonForChildB(int id, string title) { + return $"{{ \"id\": {id}, \"title\": \"{title}\", \"$type\": \"ChildB\" }}"; + } + + public string GetJsonForUnknownType(int id, string title) { + return $"{{ \"id\": {id}, \"title\": \"{title}\", \"$type\": \"UnknownType\" }}"; + } + + public string GetJsonForUnspecifiedType(int id, string title) { + return $"{{ \"id\": {id}, \"title\": \"{title}\" }}"; + } + + public string GetJsonForCompoundType(int id, string name, int childId, string childName) { + string childJson = GetJsonForChildA(childId, childName); + + return $"{{ \"id\": {id}, \"name\": \"{name}\", \"child\": {childJson}, \"$type\": \"CompoundType\" }}"; + } + } + + public class CompoundType { + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("child")] + [JsonConverter(typeof(KnownTypeConverter))] + public BaseType Child { get; set; } + } + + [KnownType(typeof(ChildA))] + [KnownType(typeof(ChildB))] + public class BaseType { + [JsonProperty("id")] + public int Id { get; set; } + } + + public class ChildA : BaseType { + [JsonProperty("name")] + public string Name { get; set; } + } + + public class ChildB : BaseType { + [JsonProperty("title")] + public string Title { get; set; } + } + } +} \ No newline at end of file diff --git a/tests/YouTrackSharp.Tests/Json/KnownTypeConverterTests.cs b/tests/YouTrackSharp.Tests/Json/KnownTypeConverterTests.cs new file mode 100644 index 0000000..65d72f8 --- /dev/null +++ b/tests/YouTrackSharp.Tests/Json/KnownTypeConverterTests.cs @@ -0,0 +1,123 @@ +using System.IO; +using JetBrains.Annotations; +using Newtonsoft.Json; +using Xunit; +using YouTrackSharp.Json; +using Fixtures = YouTrackSharp.Tests.Json.KnownTypeConverterTestFixtures; + +namespace YouTrackSharp.Tests.Json { + [UsedImplicitly] + public class KnownTypeConverterTests { + public class ReadJson { + [Fact] + public void Identifies_Correct_Subtype_ChildTypeA() { + KnownTypeConverter converter = new KnownTypeConverter(); + Fixtures.Json fixtures = new Fixtures.Json(); + + string expectedName = "Example Name"; + int expectedId = 123; + + using JsonTextReader reader = + new JsonTextReader(new StringReader(fixtures.GetJsonForChildA(expectedId, expectedName))); + reader.Read(); + + // Act + Fixtures.ChildA result = + converter.ReadJson(reader, typeof(Fixtures.BaseType), null, + new JsonSerializer()) as Fixtures.ChildA; + + Assert.NotNull(result); + Assert.Equal(expectedId, result.Id); + Assert.Equal(expectedName, result.Name); + } + + [Fact] + public void Identifies_Correct_Subtype_ChildTypeB() { + KnownTypeConverter converter = new KnownTypeConverter(); + Fixtures.Json fixtures = new Fixtures.Json(); + + string expectedTitle = "Example Title"; + int expectedId = 123; + + using JsonTextReader reader = + new JsonTextReader(new StringReader(fixtures.GetJsonForChildB(expectedId, expectedTitle))); + reader.Read(); + + // Act + Fixtures.ChildB result = + converter.ReadJson(reader, typeof(Fixtures.BaseType), null, + new JsonSerializer()) as Fixtures.ChildB; + + Assert.NotNull(result); + Assert.Equal(expectedId, result.Id); + Assert.Equal(expectedTitle, result.Title); + } + + [Fact] + public void Unknown_Subtype_Defaults_To_BaseType() { + KnownTypeConverter converter = new KnownTypeConverter(); + Fixtures.Json fixtures = new Fixtures.Json(); + + string expectedTitle = "Example Title"; + int expectedId = 123; + + using JsonTextReader reader = + new JsonTextReader(new StringReader(fixtures.GetJsonForUnknownType(expectedId, expectedTitle))); + reader.Read(); + + // Act + Fixtures.BaseType result = + converter.ReadJson(reader, typeof(Fixtures.BaseType), null, + new JsonSerializer()) as Fixtures.BaseType; + + Assert.NotNull(result); + Assert.Equal(expectedId, result.Id); + } + + [Fact] + public void Unspecified_Type_Defaults_To_BaseType() { + KnownTypeConverter converter = new KnownTypeConverter(); + Fixtures.Json fixtures = new Fixtures.Json(); + + string expectedTitle = "Example Title"; + int expectedId = 123; + + using JsonTextReader reader = + new JsonTextReader(new StringReader(fixtures.GetJsonForUnspecifiedType(expectedId, expectedTitle))); + reader.Read(); + + // Act + Fixtures.BaseType result = + converter.ReadJson(reader, typeof(Fixtures.BaseType), null, + new JsonSerializer()) as Fixtures.BaseType; + + Assert.NotNull(result); + Assert.Equal(expectedId, result.Id); + } + + [Fact] + public void Deserialize_Type_With_Polymorphic_Field() { + string expectedName = "Example Name"; + int expectedId = 123; + + string expectedChildName = "Child Name"; + int expectedChildId = 456; + + Fixtures.Json fixtures = new Fixtures.Json(); + string json = fixtures.GetJsonForCompoundType(expectedId, expectedName, expectedChildId, expectedChildName); + + Fixtures.CompoundType result = JsonConvert.DeserializeObject(json); + + + Assert.NotNull(result); + Assert.Equal(expectedId, result.Id); + Assert.Equal(expectedName, result.Name); + + Fixtures.ChildA child = result.Child as Fixtures.ChildA; + Assert.NotNull(child); + Assert.Equal(expectedChildId, child.Id); + Assert.Equal(expectedChildName, child.Name); + } + } + } +} \ No newline at end of file From e49766e1e6e03efca8ef02c3c26b8380ffabdca3 Mon Sep 17 00:00:00 2001 From: zelodyc Date: Tue, 2 Mar 2021 01:35:30 -0800 Subject: [PATCH 04/14] Add support for deserialization of lists of polymorphic objects (to concrete sub-types) --- .../Json/KnownTypeListConverter.cs | 59 ++++++++++++++ .../Json/KnownTypeConverterTestFixtures.cs | 81 +++++++++++++++++++ .../Json/KnownTypeListConverterTests.cs | 60 ++++++++++++++ 3 files changed, 200 insertions(+) create mode 100644 src/YouTrackSharp/Json/KnownTypeListConverter.cs create mode 100644 tests/YouTrackSharp.Tests/Json/KnownTypeListConverterTests.cs diff --git a/src/YouTrackSharp/Json/KnownTypeListConverter.cs b/src/YouTrackSharp/Json/KnownTypeListConverter.cs new file mode 100644 index 0000000..6f12c4c --- /dev/null +++ b/src/YouTrackSharp/Json/KnownTypeListConverter.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using YouTrackSharp.SerializationAttributes; + +namespace YouTrackSharp.Json { + /// + /// This class allows to convert a JSON array into a list of objects, each extending a given base type.
+ /// This is used in conjunction with the , defined on + /// the base class, and which lists the possible sub-types of that class.

+ /// Among these, the concrete sub-type instantiated is inferred from each Json object's "$type" field, which is + /// compared to the defined known types (ignoring their namespace).
+ /// If the "$type" field is undefined, or no matching the "$type" parameter is + /// found, the base class is instantiated instead. + ///
+ /// Base type + public class KnownTypeListConverter : JsonConverter> where T:new() { + private readonly TypedJObjectConverter _objectConverter; + + public KnownTypeListConverter() { + _objectConverter = new TypedJObjectConverter(); + } + + /// + public override void WriteJson(JsonWriter writer, List value, JsonSerializer serializer) { + throw new NotImplementedException(); + } + + /// + /// Reads the JSON representation of the object.
+ /// This method will instanciate a subclass of the given , based on the "$type" parameter + /// of the json string, which is compared to defined for that base class.
+ /// If the "$type" parameter is not defined, or no matching is found, the json is + /// deserialized to an instance of the base class directly. + ///
+ /// The to read from. + /// Type of the object. + /// The existing value of object being read. If there is no existing value then null will be used. + /// The existing value has a value. + /// The calling serializer. + /// The object value. + public override List ReadJson(JsonReader reader, Type objectType, List existingValue, bool hasExistingValue, JsonSerializer serializer) { + List knownTypes = typeof(T).GetCustomAttributes().Select(attr => attr.Type).ToList(); + + JArray array = JArray.Load(reader); + + return array.Select(token => _objectConverter.ReadObject(token as JObject, knownTypes, serializer)).ToList(); + } + + /// + public override bool CanRead => true; + + /// + public override bool CanWrite => false; + } +} \ No newline at end of file diff --git a/tests/YouTrackSharp.Tests/Json/KnownTypeConverterTestFixtures.cs b/tests/YouTrackSharp.Tests/Json/KnownTypeConverterTestFixtures.cs index c3a70e7..7b603a7 100644 --- a/tests/YouTrackSharp.Tests/Json/KnownTypeConverterTestFixtures.cs +++ b/tests/YouTrackSharp.Tests/Json/KnownTypeConverterTestFixtures.cs @@ -10,27 +10,108 @@ namespace YouTrackSharp.Tests.Json { [UsedImplicitly] public class KnownTypeConverterTestFixtures { public class Json { + /// + /// Creates a Json string representation of a instance, with given id and name. + /// + /// Id + /// Name + /// Json representation of with given id and name public string GetJsonForChildA(int id, string name) { return $"{{ \"id\": {id}, \"name\": \"{name}\", \"$type\": \"ChildA\" }}"; } + /// + /// Creates a Json string representation of a instance, with given id and title. + /// + /// Id + /// Title + /// Json representation of with given id and title public string GetJsonForChildB(int id, string title) { return $"{{ \"id\": {id}, \"title\": \"{title}\", \"$type\": \"ChildB\" }}"; } + /// + /// Creates a Json string representation of an unknown sub-type of (unknown to + /// serialization), with given id and title. + /// + /// Id of object + /// Title of object + /// Json representation of an unknown sub-type of public string GetJsonForUnknownType(int id, string title) { return $"{{ \"id\": {id}, \"title\": \"{title}\", \"$type\": \"UnknownType\" }}"; } + /// + /// Creates a Json string representation of an object, with no "$type" field, with given id and title. + /// + /// Id of object + /// Title of object + /// Json representation of an untyped object public string GetJsonForUnspecifiedType(int id, string title) { return $"{{ \"id\": {id}, \"title\": \"{title}\" }}"; } + /// + /// Creates a Json representation of a , with given id, name.
+ /// The instance is a json representation of concrete type , + /// with given child id and child name. + ///
+ /// Id + /// Name + /// Id of child + /// Name of child + /// + /// Json representation of , with instance variable. + /// public string GetJsonForCompoundType(int id, string name, int childId, string childName) { string childJson = GetJsonForChildA(childId, childName); return $"{{ \"id\": {id}, \"name\": \"{name}\", \"child\": {childJson}, \"$type\": \"CompoundType\" }}"; } + + /// + /// Creates json representation of , with given id and name. + /// The is a list made of multiple and + /// instances, created from the given enumerable. + /// This enumerable contains a tuple per child to create, with the concrete type to use ( or + /// , the child id and its name). + /// + /// Id + /// Name + /// Children specifications + /// + /// Json representation of , with array of children of types + /// or . + /// + public string GetJsonForCompoundTypeWithList(int id, string name, IEnumerable> children) { + IEnumerable childrenJson = + children.Select(child => GetJsonForChild(child.Item1, child.Item2, child.Item3)); + + string childrenJsonArray = string.Join(", ", childrenJson); + + return $"{{ \"id\": {id}, \"name\": \"{name}\", \"children\": " + + $"[{childrenJsonArray}], \"$type\": \"CompoundTypeWithList\" }}"; + } + + private string GetJsonForChild(Type concreteType, int id, string nameOrTitle) { + if (concreteType == typeof(ChildA)) { + return GetJsonForChildA(id, nameOrTitle); + } + + return GetJsonForChildB(id, nameOrTitle); + } + } + + public class CompoundTypeWithList { + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("children")] + [JsonConverter(typeof(KnownTypeListConverter))] + public List Children { get; set; } } public class CompoundType { diff --git a/tests/YouTrackSharp.Tests/Json/KnownTypeListConverterTests.cs b/tests/YouTrackSharp.Tests/Json/KnownTypeListConverterTests.cs new file mode 100644 index 0000000..688836a --- /dev/null +++ b/tests/YouTrackSharp.Tests/Json/KnownTypeListConverterTests.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; +using Newtonsoft.Json; +using Xunit; +using Fixtures = YouTrackSharp.Tests.Json.KnownTypeConverterTestFixtures; + +namespace YouTrackSharp.Tests.Json { + [UsedImplicitly] + public class KnownTypeListConverterTests { + public class ReadJson { + [Fact] + public void Deserialize_Type_With_Polymorphic_Collection() { + string expectedName = "Example Name"; + int expectedId = 123; + + IList> expectedChildren = new List>(); + + for (int i = 0; i < 10; i++) { + Type type = i % 2 == 0 ? typeof(Fixtures.ChildA) : typeof(Fixtures.ChildB); + expectedChildren.Add(new Tuple(type, i, $"Child {i}")); + } + + Fixtures.Json fixtures = new Fixtures.Json(); + string json = fixtures.GetJsonForCompoundTypeWithList(expectedId, expectedName, expectedChildren); + + Fixtures.CompoundTypeWithList result = JsonConvert.DeserializeObject(json); + + Assert.NotNull(result); + Assert.Equal(expectedId, result.Id); + Assert.Equal(expectedName, result.Name); + + Assert.NotNull(result.Children); + foreach (Fixtures.BaseType child in result.Children) { + int id = child.Id; + Tuple expectedChild = expectedChildren.FirstOrDefault(c => c.Item2 == id); + + Assert.NotNull(expectedChild); + + Assert.Equal(expectedChild.Item1, child.GetType()); + + switch (child) { + case Fixtures.ChildA childA: + Assert.Equal(expectedChild.Item3, childA.Name); + break; + + case Fixtures.ChildB childB: + Assert.Equal(expectedChild.Item3, childB.Title); + break; + + default: + Assert.True(false, "Child was not deserialized to recognizable type"); + break; + } + } + } + } + } +} \ No newline at end of file From 2c2fd9da560ff39166b3e6fce6bd2184443b014e Mon Sep 17 00:00:00 2001 From: zelodyc Date: Tue, 2 Mar 2021 01:44:43 -0800 Subject: [PATCH 05/14] Create field-syntax encoder, to generate the field parameter for YouTrack queries directly from model class --- src/YouTrackSharp/Internal/Field.cs | 36 +++ .../Internal/FieldSyntaxEncoder.cs | 228 ++++++++++++++++++ .../FieldSyntaxEncoderTestFixtures.cs | 117 +++++++++ .../Internal/FieldSyntaxEncoderTests.cs | 150 ++++++++++++ 4 files changed, 531 insertions(+) create mode 100644 src/YouTrackSharp/Internal/Field.cs create mode 100644 src/YouTrackSharp/Internal/FieldSyntaxEncoder.cs create mode 100644 tests/YouTrackSharp.Tests/Internal/FieldSyntaxEncoderTestFixtures.cs create mode 100644 tests/YouTrackSharp.Tests/Internal/FieldSyntaxEncoderTests.cs diff --git a/src/YouTrackSharp/Internal/Field.cs b/src/YouTrackSharp/Internal/Field.cs new file mode 100644 index 0000000..4a5cf92 --- /dev/null +++ b/src/YouTrackSharp/Internal/Field.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Linq; + +namespace YouTrackSharp.Internal { + /// + /// Simplified representation of an object's field. + /// It describes the name of the field and its sub-fields. + /// The 'subfields' are the members of the this field's type
+ ///
+ /// + /// This class allows to encode the recursive structure of an object's fields. + /// An object's field has a type, which itself has other members, each being of a type, and so on, down to the + /// primitive types.
+ ///
+ public class Field { + /// + /// Name of the object's field + /// + public string Name { get; } + + /// + /// Sub-fields of this field's type + /// + public List Subfields { get; } + + /// + /// Creates an instance of , with the given name, type and subfields. + /// + /// Name of the field + /// Sub-fields of the field's type + public Field(string name, IEnumerable subfields) { + Name = name; + Subfields = subfields.ToList(); + } + } +} \ No newline at end of file diff --git a/src/YouTrackSharp/Internal/FieldSyntaxEncoder.cs b/src/YouTrackSharp/Internal/FieldSyntaxEncoder.cs new file mode 100644 index 0000000..5559b63 --- /dev/null +++ b/src/YouTrackSharp/Internal/FieldSyntaxEncoder.cs @@ -0,0 +1,228 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Newtonsoft.Json; +using YouTrackSharp.SerializationAttributes; + +namespace YouTrackSharp.Internal { + /// + /// Used to convert a given to the Youtrack REST API's 'fields' syntax, described at the following + /// location: + /// https://www.jetbrains.com/help/youtrack/standalone/api-fields-syntax.html + /// . + /// + public class FieldSyntaxEncoder { + /// + /// Cached used to store types that were already resolved. + /// + /// + /// The result of the field query can vary depending on the verbose option and the max nesting level. + /// Therefore, the key used is a combination of the , verbosity and max depth. + /// + private Dictionary, string> Cache { get; } + + /// + /// Creates an instance of + /// + public FieldSyntaxEncoder() { + Cache = new Dictionary, string>(); + } + + /// + /// Encodes the given 's members to the Youtrack REST API's 'fields'-syntax, described at + /// + /// https://www.jetbrains.com/help/youtrack/standalone/api-fields-syntax.html + /// .
+ /// This method uses reflexion to retrieve and encodes all public writable properties, decorated with a + /// .
+ /// It also concatenates the fields retrieved from known subclasses of the given type (defined by the + /// ).
+ /// Because the base class and the different subclasses may have fields with similar names (but different types and + /// sub-fields), all the fields discovered this way are "factored" together. Therefore, if two known subclasses both + /// have a field with the same name, but each with different types and so, different sub-fields, the two fields will + /// be factored into one containing the union of their respective sub-fields.
+ /// For example:
+ /// - The first subclass has a field named A with subfields B, C and D, ie. A(B,C,D)
+ /// - The second subclass also has a field A but with subfields D,E and F, ie. A(D,E,F)
+ /// In that case, these two fields will be combined into one, with name A and subfields B, C, D, E and F, ie. + /// A(B,C,D,E,F). Note that D appears only once. + ///
+ /// Type to encode + /// + /// Verbosity. If true, will include all public writable properties of the given , marked + /// with the . + /// But if false, it will exclude from these all the ones that were marked with the + /// . + /// + /// Max sub-field depth to go from the given type. + /// Fields of the given type, encoded to Youtrack + /// REST API's 'field' syntax + /// + /// + /// + /// This method does not work well with types that have (direct or indirect) reference loops. + /// For example, a class A that contains two fields of type B and C, where the type B itself contains a field of + /// type A. This would result in a stack-overflow. + /// + public string Encode(Type type, bool verbose = true, int maxDepth = Int32.MaxValue) { + type = GetLeafType(type); + + Tuple key = new Tuple(type, verbose, maxDepth); + + if (Cache.ContainsKey(key)) return Cache[key]; + + IEnumerable fields = GetFields(type, verbose, maxDepth, 0); + + Cache[key] = string.Join(",", fields.Select(ToString)); + + return Cache[key]; + } + + /// + /// Converts the recursive structure into a string representation, following Youtrack REST API's + /// 'field' syntax + /// Example: lines(fromPoint(x,y),toPoint(x,y)),color + /// + /// to convert + /// + private string ToString(Field field) { + if (!field.Subfields.Any()) return field.Name; + + string subfields = string.Join(",", field.Subfields.Select(ToString)); + + return $"{field.Name}({subfields})"; + } + + /// + /// Recursive method which builds the structure for the given 's properties. + /// This method uses reflexion to convert all the public writable properties of teh given , + /// decorated with a to a
. + /// It also retrieves the fields from known subclasses of the given type (defined by the + /// ).
+ /// Because the base class and the different subclasses may have fields with similar names (but different types and + /// sub-fields), all the fields discovered this way are "factored" together. Therefore, if two known subclasses both + /// have a field with the same name, but different type, each with different sub-fields, the two fields will + /// be factored into one containing the union of their respective sub-fields.
+ /// For example:
+ /// - The first subclass has a field named A with subfields B, C and D, ie. A(B,C,D)
+ /// - The second subclass also has a field A but with subfields D,E and F, ie. A(D,E,F)
+ /// In that case, these two fields will be combined into one, with name A and subfields B, C, D, E and F, ie. + /// A(B,C,D,E,F). Note that D appears only once. + ///
+ /// Type to convert + /// + /// Verbosity. If true, will include all public writable properties of the given , marked + /// with the . + /// But if false, it will exclude from these all the ones that were marked with the + /// . + /// + /// Max sub-field depth to go from the given type. + /// Current sub-field level (to compare with ) + /// Fields of the given type, encoded to Youtrack + /// REST API's 'field' syntax + /// + /// + /// + /// This method does not work well with types that have (direct or indirect) reference loops. + /// For example, a class A that contains two fields of type B and C, where the type B itself contains a field of + /// type A. This would result in a stack-overflow. + /// + private IEnumerable GetFields(Type type, bool verbose, int maxDepth, int level) { + if (level == maxDepth) return Enumerable.Empty(); + + List fields = new List(); + + IEnumerable properties = GetProperties(type, verbose); + foreach (PropertyInfo propertyInfo in properties) { + Type propertyType = GetLeafType(propertyInfo.PropertyType); + string propertyName = propertyInfo.GetCustomAttribute()?.PropertyName; + + IEnumerable subfields = GetFields(propertyType, verbose, maxDepth, level + 1); + + fields.Add(new Field(propertyName, subfields)); + } + + return Merge(fields); + } + + /// + /// Merges the given fields into a factored representation, where fields with the same name are combined into one. + /// For example, if there are two fields named "A"
+ /// - The first with subfields B, C and D, ie. A(B,C,D)
+ /// - The second with subfields D,E and F, ie. A(D,E,F)
+ /// These two fields will be combined into one, with name A and subfields B, C, D, E and F, ie. + /// A(B,C,D,E,F). Note that D appears only once. + ///
+ /// s to factor + /// Factored fields + private IEnumerable Merge(IEnumerable fields) { + IEnumerable> groups = fields.GroupBy(field => field.Name); + + foreach (IGrouping group in groups) { + IEnumerable subfields = group.SelectMany(f => f.Subfields); + subfields = Merge(subfields); + + yield return new Field(group.Key, subfields); + } + } + + /// + /// Retrieves the of all public, writable properties marked with a + /// of the given and + /// all of its known subclasses (defined by the . + /// If is set to false, it will exclude the ones marked with a + /// . + /// + /// from which to retrieve the properties + /// + /// If false the properties marked with will be excluded from the results. + /// + /// + /// The of all public, writable properties marked with a + /// of the given and + /// all of its known subclasses. + /// + private IEnumerable GetProperties(Type type, bool verbose) { + PropertyInfo[] properties = type.GetProperties(); + + IEnumerable propertyInfos = properties.Where(p => p.CanWrite) + .Where(p => p.GetCustomAttribute() != null); + + if (!verbose) { + propertyInfos = propertyInfos.Where(p => p.GetCustomAttribute() == null); + } + + IEnumerable knownSubtypes = type.GetCustomAttributes(false).Select(attr => attr.Type); + IEnumerable subtypesProperties = + knownSubtypes.SelectMany(subtype => GetProperties(subtype, verbose)); + + return propertyInfos.Concat(subtypesProperties); + } + + /// + /// Returns the "leaf" type argument of any generic type.
+ /// Note: Generic types are nested, the "leaf" meaning the non-generic type at the bottom of the nesting chain + ///
+ /// For example, a of , will yield . + /// And a of of + /// will also yield .
+ ///
+ /// For generic types with multiple type parameters, it will always follow the last, which is usually the value type. + /// For example, a of key and value , + /// will yield .
+ /// A of key and value of + /// , will also yield . + ///
+ /// Input + /// Leaf type if the input type is generic, otherwise itself + /// + /// The returned type is guaranteed to be non-generic. + /// + private Type GetLeafType(Type type) { + if (!type.IsGenericType) return type; + + return GetLeafType(type.GetGenericArguments().Last()); + } + } +} \ No newline at end of file diff --git a/tests/YouTrackSharp.Tests/Internal/FieldSyntaxEncoderTestFixtures.cs b/tests/YouTrackSharp.Tests/Internal/FieldSyntaxEncoderTestFixtures.cs new file mode 100644 index 0000000..1ebf3d7 --- /dev/null +++ b/tests/YouTrackSharp.Tests/Internal/FieldSyntaxEncoderTestFixtures.cs @@ -0,0 +1,117 @@ +using System.Collections.Generic; +using JetBrains.Annotations; +using Newtonsoft.Json; +using YouTrackSharp.SerializationAttributes; + +namespace YouTrackSharp.Tests.Internal { + [UsedImplicitly] + public class FieldSyntaxEncoderTestFixtures { + public class FlatType { + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [Verbose] + [JsonProperty("description")] + public string Description { get; set; } + } + + public class NestedTypes { + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("info")] + public Info Info { get; set; } + } + + public class DeepNestedTypes { + [JsonProperty("id")] + public int Id { get; set; } + + [Verbose] + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("contact")] + public DetailedContact Contact { get; set; } + } + + public class DeepNestedTypesWithInheritance { + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("name")] + public string name { get; set; } + + [JsonProperty("contact")] + public Contact Contact { get; set; } + } + + public class DeepNestedTypesWithCollection { + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("name")] + public string name { get; set; } + + [JsonProperty("contacts")] + public IList Contacts { get; set; } + } + + public class CyclicReference { + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("name")] + public string name { get; set; } + + [Verbose] + [JsonProperty("cyclic")] + public CyclicReference Cyclic { get; set; } + } + + [KnownType(typeof(SimpleContact))] + [KnownType(typeof(DetailedContact))] + public class Contact { + [JsonProperty("id")] + public int Id { get; set; } + } + + public class SimpleContact : Contact { + [JsonProperty("fullName")] + public string FullName { get; set; } + + [Verbose] + [JsonProperty("info")] + public string Info { get; set; } + } + + public class DetailedContact : Contact { + [JsonProperty("firstName")] + public string FirstName { get; set; } + + [JsonProperty("lastName")] + public string LastName { get; set; } + + [Verbose] + [JsonProperty("info")] + public Info Info { get; set; } + } + + public class Info { + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("address")] + public string Address { get; set; } + + [JsonProperty("phoneNumber")] + public string PhoneNumber { get; set; } + } + } +} \ No newline at end of file diff --git a/tests/YouTrackSharp.Tests/Internal/FieldSyntaxEncoderTests.cs b/tests/YouTrackSharp.Tests/Internal/FieldSyntaxEncoderTests.cs new file mode 100644 index 0000000..bf35adf --- /dev/null +++ b/tests/YouTrackSharp.Tests/Internal/FieldSyntaxEncoderTests.cs @@ -0,0 +1,150 @@ +using JetBrains.Annotations; +using Xunit; +using YouTrackSharp.Internal; + +namespace YouTrackSharp.Tests.Internal { + [UsedImplicitly] + public class FieldSyntaxEncoderTests { + public class UrlEncoding { + [Fact] + public void Encode_Flat_Type() { + FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); + string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.FlatType)); + + string expected = "id,name,description"; + + Assert.Equal(expected, encoded); + } + + [Fact] + public void Encode_Flat_Type_Non_Verbose() { + FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); + string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.FlatType), false); + + string expected = "id,name"; + + Assert.Equal(expected, encoded); + } + + [Fact] + public void Encode_Single_Nested_Types() { + FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); + string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.NestedTypes)); + + string expected = "id,name,info(id,address,phoneNumber)"; + + Assert.Equal(expected, encoded); + } + + [Fact] + public void Encode_Deep_Nested_Types() { + FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); + string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.DeepNestedTypes)); + + string expected = "id,name,contact(firstName,lastName,info(id,address,phoneNumber),id)"; + + Assert.Equal(expected, encoded); + } + + [Fact] + public void Encode_Nested_Types_With_Inheritance_Merges_Common_Field_Info() { + FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); + string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.DeepNestedTypesWithInheritance)); + + string expected = "id,name,contact(id,fullName,info(id,address,phoneNumber),firstName,lastName)"; + + Assert.Equal(expected, encoded); + } + + [Fact] + public void Encode_Nested_Types_With_Collection() { + FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); + string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.DeepNestedTypesWithCollection)); + + string expected = "id,name,contacts(id,fullName,info(id,address,phoneNumber),firstName,lastName)"; + + Assert.Equal(expected, encoded); + } + + [Fact] + public void Encode_Nested_Types_Max_Depth_0() { + FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); + string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.DeepNestedTypes), true, 0); + + string expected = string.Empty; + + Assert.Equal(expected, encoded); + } + + [Fact] + public void Encode_Nested_Types_Max_Depth_1() { + FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); + string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.DeepNestedTypes), true, 1); + + string expected = "id,name,contact"; + + Assert.Equal(expected, encoded); + } + + [Fact] + public void Encode_Nested_Types_Max_Depth_2() { + FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); + string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.DeepNestedTypes), true, 2); + + string expected = "id,name,contact(firstName,lastName,info,id)"; + + Assert.Equal(expected, encoded); + } + + [Fact] + public void Encode_Nested_Types_Max_Depth_3() { + FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); + string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.DeepNestedTypes), true, 3); + + string expected = "id,name,contact(firstName,lastName,info(id,address,phoneNumber),id)"; + + Assert.Equal(expected, encoded); + } + + [Fact] + public void Encode_Flat_Type_Not_Verbose() { + FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); + string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.FlatType), false); + + string expected = "id,name"; + + Assert.Equal(expected, encoded); + } + + [Fact] + public void Encode_Nested_Type_Not_Verbose() { + FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); + string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.DeepNestedTypes), false); + + string expected = "id,contact(firstName,lastName,id)"; + + Assert.Equal(expected, encoded); + } + + [Fact] + public void Control_Cyclic_References_With_Max_Depth() { + FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); + string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.CyclicReference), true, 3); + + string expected = "id,name,cyclic(id,name,cyclic(id,name,cyclic))"; + + Assert.Equal(expected, encoded); + } + + [Fact] + public void Skip_Cyclic_References_With_Verbose_Disabled() { + FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); + string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.CyclicReference), false); + + string expected = "id,name"; + + Assert.Equal(expected, encoded); + } + } + } +} \ No newline at end of file From cd0c8a6fa1c2fb878ea1a7bc1a552dcd2c75d27e Mon Sep 17 00:00:00 2001 From: zelodyc Date: Wed, 3 Mar 2021 00:22:27 -0800 Subject: [PATCH 06/14] Add KnownType attribute to parent classes. Set WIPLimit's max and min properties nullable. --- src/YouTrackSharp/Agiles/ColorCoding.cs | 3 +++ src/YouTrackSharp/Agiles/DatabaseAttributeValue.cs | 3 +++ src/YouTrackSharp/Agiles/FilterField.cs | 3 +++ src/YouTrackSharp/Agiles/SwimlaneSettings.cs | 3 +++ src/YouTrackSharp/Agiles/WIPLimit.cs | 4 ++-- 5 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/YouTrackSharp/Agiles/ColorCoding.cs b/src/YouTrackSharp/Agiles/ColorCoding.cs index a235c76..55bb7c3 100644 --- a/src/YouTrackSharp/Agiles/ColorCoding.cs +++ b/src/YouTrackSharp/Agiles/ColorCoding.cs @@ -1,10 +1,13 @@ using Newtonsoft.Json; +using YouTrackSharp.SerializationAttributes; namespace YouTrackSharp.Agiles { /// /// Describe rules according to which different colors are used for cards on agile board. /// + [KnownType(typeof(FieldBasedColorCoding))] + [KnownType(typeof(ProjectBasedColorCoding))] public class ColorCoding { /// /// Id of the ColorCoding. diff --git a/src/YouTrackSharp/Agiles/DatabaseAttributeValue.cs b/src/YouTrackSharp/Agiles/DatabaseAttributeValue.cs index 2a5f786..a913ced 100644 --- a/src/YouTrackSharp/Agiles/DatabaseAttributeValue.cs +++ b/src/YouTrackSharp/Agiles/DatabaseAttributeValue.cs @@ -1,9 +1,12 @@ using Newtonsoft.Json; +using YouTrackSharp.SerializationAttributes; namespace YouTrackSharp.Agiles { /// /// Represents string reference to the value. /// + [KnownType(typeof(SwimlaneEntityAttributeValue))] + [KnownType(typeof(AgileColumnFieldValue))] public class DatabaseAttributeValue { /// /// Id of the DatabaseAttributeValue. diff --git a/src/YouTrackSharp/Agiles/FilterField.cs b/src/YouTrackSharp/Agiles/FilterField.cs index fc4a4b3..20f3256 100644 --- a/src/YouTrackSharp/Agiles/FilterField.cs +++ b/src/YouTrackSharp/Agiles/FilterField.cs @@ -1,9 +1,12 @@ using Newtonsoft.Json; +using YouTrackSharp.SerializationAttributes; namespace YouTrackSharp.Agiles { /// /// Represents an issue property, which can be a predefined field, a custom field, a link, and so on. /// + [KnownType(typeof(PredefinedFilterField))] + [KnownType(typeof(CustomFilterField))] public class FilterField { /// /// Id of the FilterField. diff --git a/src/YouTrackSharp/Agiles/SwimlaneSettings.cs b/src/YouTrackSharp/Agiles/SwimlaneSettings.cs index dbc4669..391a0eb 100644 --- a/src/YouTrackSharp/Agiles/SwimlaneSettings.cs +++ b/src/YouTrackSharp/Agiles/SwimlaneSettings.cs @@ -1,9 +1,12 @@ using Newtonsoft.Json; +using YouTrackSharp.SerializationAttributes; namespace YouTrackSharp.Agiles { /// /// Base entity for different swimlane settings /// + [KnownType(typeof(AttributeBasedSwimlaneSettings))] + [KnownType(typeof(IssueBasedSwimlaneSettings))] public class SwimlaneSettings { /// /// Id of the SwimlaneSettings. diff --git a/src/YouTrackSharp/Agiles/WIPLimit.cs b/src/YouTrackSharp/Agiles/WIPLimit.cs index 352e4cb..4167c12 100644 --- a/src/YouTrackSharp/Agiles/WIPLimit.cs +++ b/src/YouTrackSharp/Agiles/WIPLimit.cs @@ -15,12 +15,12 @@ public class WIPLimit { /// Maximum number of cards in column. Can be null. /// [JsonProperty("max")] - public int Max { get; set; } + public int? Max { get; set; } /// /// Minimum number of cards in column. Can be null. /// [JsonProperty("min")] - public int Min { get; set; } + public int? Min { get; set; } } } From 5f405629759bbe7ea59cb6d17ebe0bd04b69f063 Mon Sep 17 00:00:00 2001 From: zelodyc Date: Wed, 3 Mar 2021 00:30:15 -0800 Subject: [PATCH 07/14] Add the KnownTypeConverter to polymorphic fields to allow deserializing to concrete subclasses --- src/YouTrackSharp/Agiles/Agile.cs | 3 +++ src/YouTrackSharp/Agiles/AttributeBasedSwimlaneSettings.cs | 2 ++ src/YouTrackSharp/Agiles/IssueBasedSwimlaneSettings.cs | 2 ++ 3 files changed, 7 insertions(+) diff --git a/src/YouTrackSharp/Agiles/Agile.cs b/src/YouTrackSharp/Agiles/Agile.cs index b03955a..db39104 100644 --- a/src/YouTrackSharp/Agiles/Agile.cs +++ b/src/YouTrackSharp/Agiles/Agile.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Newtonsoft.Json; +using YouTrackSharp.Json; using YouTrackSharp.Management; using YouTrackSharp.Projects; @@ -103,6 +104,7 @@ public class Agile { /// Settings of the board swimlanes. Can be null. /// [JsonProperty("swimlaneSettings")] + [JsonConverter(typeof(KnownTypeConverter))] public SwimlaneSettings SwimlaneSettings { get; set; } /// @@ -115,6 +117,7 @@ public class Agile { /// Color coding settings for the board. Can be null. /// [JsonProperty("colorCoding")] + [JsonConverter(typeof(KnownTypeConverter))] public ColorCoding ColorCoding { get; set; } /// diff --git a/src/YouTrackSharp/Agiles/AttributeBasedSwimlaneSettings.cs b/src/YouTrackSharp/Agiles/AttributeBasedSwimlaneSettings.cs index ef03972..10d9b4a 100644 --- a/src/YouTrackSharp/Agiles/AttributeBasedSwimlaneSettings.cs +++ b/src/YouTrackSharp/Agiles/AttributeBasedSwimlaneSettings.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Newtonsoft.Json; +using YouTrackSharp.Json; namespace YouTrackSharp.Agiles { /// @@ -11,6 +12,7 @@ public class AttributeBasedSwimlaneSettings : SwimlaneSettings { /// CustomField which values are used to identify swimlane. /// [JsonProperty("field")] + [JsonConverter(typeof(KnownTypeConverter))] public FilterField Field { get; set; } /// diff --git a/src/YouTrackSharp/Agiles/IssueBasedSwimlaneSettings.cs b/src/YouTrackSharp/Agiles/IssueBasedSwimlaneSettings.cs index 6879d3e..e3b8a78 100644 --- a/src/YouTrackSharp/Agiles/IssueBasedSwimlaneSettings.cs +++ b/src/YouTrackSharp/Agiles/IssueBasedSwimlaneSettings.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Newtonsoft.Json; +using YouTrackSharp.Json; namespace YouTrackSharp.Agiles { /// @@ -10,6 +11,7 @@ public class IssueBasedSwimlaneSettings : SwimlaneSettings { /// CustomField which values are used to identify swimlane. /// [JsonProperty("field")] + [JsonConverter(typeof(KnownTypeConverter))] public FilterField Field { get; set; } /// From 5e4fa004bfb20e37a5e91d38bfa393713ac0db2b Mon Sep 17 00:00:00 2001 From: zelodyc Date: Wed, 3 Mar 2021 00:44:21 -0800 Subject: [PATCH 08/14] Added the VerboseAttribute to verbose-only fields --- src/YouTrackSharp/Agiles/Agile.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/YouTrackSharp/Agiles/Agile.cs b/src/YouTrackSharp/Agiles/Agile.cs index db39104..78af765 100644 --- a/src/YouTrackSharp/Agiles/Agile.cs +++ b/src/YouTrackSharp/Agiles/Agile.cs @@ -3,6 +3,7 @@ using YouTrackSharp.Json; using YouTrackSharp.Management; using YouTrackSharp.Projects; +using YouTrackSharp.SerializationAttributes; namespace YouTrackSharp.Agiles { /// @@ -30,24 +31,28 @@ public class Agile { /// /// The user group that can view this board. Can be null. /// + [Verbose] [JsonProperty("visibleFor")] public Group VisibleFor { get; set; } /// /// When true, the board is visible to everyone who can view all projects that are associated with the board. /// + [Verbose] [JsonProperty("visibleForProjectBased")] public bool VisibleForProjectBased { get; set; } /// /// Group of users who can update board settings. Can be null. /// + [Verbose] [JsonProperty("updateableBy")] public Group UpdateableBy { get; set; } /// /// When true, anyone who can update the associated projects can update the board. /// + [Verbose] [JsonProperty("updateableByProjectBased")] public bool UpdateableByProjectBased { get; set; } @@ -55,54 +60,63 @@ public class Agile { /// When true, the orphan swimlane is placed at the top of the board. Otherwise, the orphans swimlane is located /// below all other swimlanes. /// + [Verbose] [JsonProperty("orphansAtTheTop")] public bool OrphansAtTheTop { get; set; } /// /// When true, the orphans swimlane is not displayed on the board. /// + [Verbose] [JsonProperty("hideOrphansSwimlane")] public bool HideOrphansSwimlane { get; set; } /// /// A custom field that is used as the estimation field for the board. Can be null. /// + [Verbose] [JsonProperty("estimationField")] public CustomField EstimationField { get; set; } /// /// A custom field that is used as the original estimation field for the board. Can be null. /// + [Verbose] [JsonProperty("originalEstimationField")] public CustomField OriginalEstimationField { get; set; } /// /// A collection of projects associated with the board. /// + [Verbose] [JsonProperty("projects")] public List Projects { get; set; } /// /// The set of sprints that are associated with the board. /// + [Verbose] [JsonProperty("sprints")] public List Sprints { get; set; } /// /// A sprint that is actual for the current date. Read-only. Can be null. /// + [Verbose] [JsonProperty("currentSprint")] public Sprint CurrentSprint { get; set; } /// /// Column settings of the board. Read-only. /// + [Verbose] [JsonProperty("columnSettings")] public ColumnSettings ColumnSettings { get; set; } /// /// Settings of the board swimlanes. Can be null. /// + [Verbose] [JsonProperty("swimlaneSettings")] [JsonConverter(typeof(KnownTypeConverter))] public SwimlaneSettings SwimlaneSettings { get; set; } @@ -110,12 +124,14 @@ public class Agile { /// /// Settings of the board sprints. Read-only. /// + [Verbose] [JsonProperty("sprintsSettings")] public SprintsSettings SprintsSettings { get; set; } /// /// Color coding settings for the board. Can be null. /// + [Verbose] [JsonProperty("colorCoding")] [JsonConverter(typeof(KnownTypeConverter))] public ColorCoding ColorCoding { get; set; } @@ -123,6 +139,7 @@ public class Agile { /// /// Status of the board. Read-only. /// + [Verbose] [JsonProperty("status")] public AgileStatus Status { get; set; } } From dda938e3a3750003ea1af302bc12f81df26634ec Mon Sep 17 00:00:00 2001 From: zelodyc Date: Wed, 3 Mar 2021 01:07:24 -0800 Subject: [PATCH 09/14] Correct deserialization of null fields in KnownTypeConverter / KnownTypeListConverter --- src/YouTrackSharp/Json/KnownTypeConverter.cs | 2 + .../Json/KnownTypeListConverter.cs | 2 + .../Json/KnownTypeConverterTestFixtures.cs | 48 ++++++++++++++++++- .../Json/KnownTypeConverterTests.cs | 19 ++++++++ .../Json/KnownTypeListConverterTests.cs | 34 +++++++++++++ 5 files changed, 104 insertions(+), 1 deletion(-) diff --git a/src/YouTrackSharp/Json/KnownTypeConverter.cs b/src/YouTrackSharp/Json/KnownTypeConverter.cs index d8eee6f..8183156 100644 --- a/src/YouTrackSharp/Json/KnownTypeConverter.cs +++ b/src/YouTrackSharp/Json/KnownTypeConverter.cs @@ -44,6 +44,8 @@ public override void WriteJson(JsonWriter writer, T value, JsonSerializer serial /// The calling serializer. /// The object value. public override T ReadJson(JsonReader reader, Type objectType, T existingValue, bool hasExistingValue, JsonSerializer serializer) { + if (reader.TokenType == JsonToken.Null) return default; + List types = typeof(T).GetCustomAttributes().Select(attr => attr.Type).ToList(); JObject obj = JObject.Load(reader); diff --git a/src/YouTrackSharp/Json/KnownTypeListConverter.cs b/src/YouTrackSharp/Json/KnownTypeListConverter.cs index 6f12c4c..375bad2 100644 --- a/src/YouTrackSharp/Json/KnownTypeListConverter.cs +++ b/src/YouTrackSharp/Json/KnownTypeListConverter.cs @@ -43,6 +43,8 @@ public override void WriteJson(JsonWriter writer, List value, JsonSerializer /// The calling serializer. /// The object value. public override List ReadJson(JsonReader reader, Type objectType, List existingValue, bool hasExistingValue, JsonSerializer serializer) { + if (reader.TokenType == JsonToken.Null) return null; + List knownTypes = typeof(T).GetCustomAttributes().Select(attr => attr.Type).ToList(); JArray array = JArray.Load(reader); diff --git a/tests/YouTrackSharp.Tests/Json/KnownTypeConverterTestFixtures.cs b/tests/YouTrackSharp.Tests/Json/KnownTypeConverterTestFixtures.cs index 7b603a7..86a7b03 100644 --- a/tests/YouTrackSharp.Tests/Json/KnownTypeConverterTestFixtures.cs +++ b/tests/YouTrackSharp.Tests/Json/KnownTypeConverterTestFixtures.cs @@ -70,7 +70,19 @@ public string GetJsonForCompoundType(int id, string name, int childId, string ch } /// - /// Creates json representation of , with given id and name. + /// Creates a Json representation of a , with given id, name, but null child
+ ///
+ /// Id + /// Name + /// + /// Json representation of , with null child. + /// + public string GetJsonForCompoundTypeWithNullField(int id, string name) { + return $"{{ \"id\": {id}, \"name\": \"{name}\", \"child\": null, \"$type\": \"CompoundType\" }}"; + } + + /// + /// Creates Json representation of , with given id and name. /// The is a list made of multiple and /// instances, created from the given enumerable. /// This enumerable contains a tuple per child to create, with the concrete type to use ( or @@ -93,6 +105,14 @@ public string GetJsonForCompoundTypeWithList(int id, string name, IEnumerable + /// Creates Json representation of or , depending on the given + /// + /// + /// Concrete type ( or ) + /// Id of child + /// Name (for ) or Title (for ) + /// Json representation of given private string GetJsonForChild(Type concreteType, int id, string nameOrTitle) { if (concreteType == typeof(ChildA)) { return GetJsonForChildA(id, nameOrTitle); @@ -100,6 +120,32 @@ private string GetJsonForChild(Type concreteType, int id, string nameOrTitle) { return GetJsonForChildB(id, nameOrTitle); } + + /// + /// Creates json representation of , with given id and name, but + /// with children field set to null. + /// + /// Id + /// Name + /// + /// Json representation of with null children. + /// + public string GetJsonForCompoundTypeWithNullList(int id, string name) { + return $"{{ \"id\": {id}, \"name\": \"{name}\", \"children\": null, \"$type\": \"CompoundTypeWithList\" }}"; + } + + /// + /// Creates json representation of , with given id and name, but + /// with the children field set to an empty array. + /// + /// Id + /// Name + /// + /// Json representation of with empty children array. + /// + public string GetJsonForCompoundTypeWithEmptyList(int id, string name) { + return $"{{ \"id\": {id}, \"name\": \"{name}\", \"children\": [], \"$type\": \"CompoundTypeWithList\" }}"; + } } public class CompoundTypeWithList { diff --git a/tests/YouTrackSharp.Tests/Json/KnownTypeConverterTests.cs b/tests/YouTrackSharp.Tests/Json/KnownTypeConverterTests.cs index 65d72f8..b326ce2 100644 --- a/tests/YouTrackSharp.Tests/Json/KnownTypeConverterTests.cs +++ b/tests/YouTrackSharp.Tests/Json/KnownTypeConverterTests.cs @@ -118,6 +118,25 @@ public void Deserialize_Type_With_Polymorphic_Field() { Assert.Equal(expectedChildId, child.Id); Assert.Equal(expectedChildName, child.Name); } + + [Fact] + public void Deserialize_Type_With_Null_Polymorphic_Field() { + string expectedName = "Example Name"; + int expectedId = 123; + + Fixtures.Json fixtures = new Fixtures.Json(); + string json = fixtures.GetJsonForCompoundTypeWithNullField(expectedId, expectedName); + + Fixtures.CompoundType result = JsonConvert.DeserializeObject(json); + + + Assert.NotNull(result); + Assert.Equal(expectedId, result.Id); + Assert.Equal(expectedName, result.Name); + + Fixtures.ChildA child = result.Child as Fixtures.ChildA; + Assert.Null(child); + } } } } \ No newline at end of file diff --git a/tests/YouTrackSharp.Tests/Json/KnownTypeListConverterTests.cs b/tests/YouTrackSharp.Tests/Json/KnownTypeListConverterTests.cs index 688836a..18d59ac 100644 --- a/tests/YouTrackSharp.Tests/Json/KnownTypeListConverterTests.cs +++ b/tests/YouTrackSharp.Tests/Json/KnownTypeListConverterTests.cs @@ -55,6 +55,40 @@ public void Deserialize_Type_With_Polymorphic_Collection() { } } } + + [Fact] + public void Deserialize_Type_With_Polymorphic_Collection_Null() { + string expectedName = "Example Name"; + int expectedId = 123; + + Fixtures.Json fixtures = new Fixtures.Json(); + string json = fixtures.GetJsonForCompoundTypeWithNullList(expectedId, expectedName); + + Fixtures.CompoundTypeWithList result = JsonConvert.DeserializeObject(json); + + Assert.NotNull(result); + Assert.Equal(expectedId, result.Id); + Assert.Equal(expectedName, result.Name); + + Assert.Null(result.Children); + } + + [Fact] + public void Deserialize_Type_With_Polymorphic_Collection_Empty() { + string expectedName = "Example Name"; + int expectedId = 123; + + Fixtures.Json fixtures = new Fixtures.Json(); + string json = fixtures.GetJsonForCompoundTypeWithEmptyList(expectedId, expectedName); + + Fixtures.CompoundTypeWithList result = JsonConvert.DeserializeObject(json); + + Assert.NotNull(result); + Assert.Equal(expectedId, result.Id); + Assert.Equal(expectedName, result.Name); + Assert.NotNull(result.Children); + Assert.Empty(result.Children); + } } } } \ No newline at end of file From 9d984793fb15e4b925a7038fec9aa279146552f8 Mon Sep 17 00:00:00 2001 From: zelodyc Date: Thu, 4 Mar 2021 00:35:17 -0800 Subject: [PATCH 10/14] Added agile-specific UserGroup for 'visibleFor' and 'updateableBy' properties --- src/YouTrackSharp/Agiles/Agile.cs | 9 ++--- src/YouTrackSharp/Agiles/UserGroup.cs | 50 +++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 7 deletions(-) create mode 100644 src/YouTrackSharp/Agiles/UserGroup.cs diff --git a/src/YouTrackSharp/Agiles/Agile.cs b/src/YouTrackSharp/Agiles/Agile.cs index 78af765..afad599 100644 --- a/src/YouTrackSharp/Agiles/Agile.cs +++ b/src/YouTrackSharp/Agiles/Agile.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using Newtonsoft.Json; using YouTrackSharp.Json; -using YouTrackSharp.Management; using YouTrackSharp.Projects; using YouTrackSharp.SerializationAttributes; @@ -33,12 +32,11 @@ public class Agile { ///
[Verbose] [JsonProperty("visibleFor")] - public Group VisibleFor { get; set; } + public UserGroup VisibleFor { get; set; } /// /// When true, the board is visible to everyone who can view all projects that are associated with the board. /// - [Verbose] [JsonProperty("visibleForProjectBased")] public bool VisibleForProjectBased { get; set; } @@ -47,12 +45,11 @@ public class Agile { ///
[Verbose] [JsonProperty("updateableBy")] - public Group UpdateableBy { get; set; } + public UserGroup UpdateableBy { get; set; } /// /// When true, anyone who can update the associated projects can update the board. /// - [Verbose] [JsonProperty("updateableByProjectBased")] public bool UpdateableByProjectBased { get; set; } @@ -60,14 +57,12 @@ public class Agile { /// When true, the orphan swimlane is placed at the top of the board. Otherwise, the orphans swimlane is located /// below all other swimlanes. ///
- [Verbose] [JsonProperty("orphansAtTheTop")] public bool OrphansAtTheTop { get; set; } /// /// When true, the orphans swimlane is not displayed on the board. /// - [Verbose] [JsonProperty("hideOrphansSwimlane")] public bool HideOrphansSwimlane { get; set; } diff --git a/src/YouTrackSharp/Agiles/UserGroup.cs b/src/YouTrackSharp/Agiles/UserGroup.cs new file mode 100644 index 0000000..fa9d151 --- /dev/null +++ b/src/YouTrackSharp/Agiles/UserGroup.cs @@ -0,0 +1,50 @@ +using Newtonsoft.Json; + +namespace YouTrackSharp.Agiles { + /// + /// Represents a group of users. + /// + public class UserGroup { + /// + /// Id of the UserGroup. + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// The name of the group. Read-only. Can be null. + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// ID of the group in Hub. Use this ID for operations in Hub, and for matching groups between YouTrack and Hub. Read-only. Can be null. + /// + [JsonProperty("ringId")] + public string RingId { get; set; } + + /// + /// The number of users in the group. Read-only. + /// + [JsonProperty("userCount")] + public long UserCount { get; set; } + + /// + /// The URL of the group icon. Read-only. Can be null. + /// + [JsonProperty("icon")] + public string Icon { get; set; } + + /// + /// True if this group contains all users, otherwise false. Read-only. + /// + [JsonProperty("allUsersGroup")] + public bool AllUsersGroup { get; set; } + + /// + /// Project that has this group set as a team. Returns null, if there is no such project. Read-only. Can be null. + /// + [JsonProperty("teamForProject")] + public Project TeamForProject { get; set; } + } +} \ No newline at end of file From dfb922b9316b3b954e57e68a81558ed2252f4618 Mon Sep 17 00:00:00 2001 From: zelodyc Date: Thu, 4 Mar 2021 00:37:54 -0800 Subject: [PATCH 11/14] Added AgileService with initial query to retrieve all agile boards --- src/YouTrackSharp/Agiles/AgileService.cs | 53 +++++ src/YouTrackSharp/Agiles/IAgileService.cs | 25 +++ src/YouTrackSharp/ConnectionExtensions.cs | 12 ++ .../Infrastructure/ConnectionStub.cs | 63 ++++++ .../Infrastructure/Connections.cs | 10 +- .../Integration/Agiles/AgileServiceTest.cs | 28 +++ .../Integration/Agiles/GetAgiles.cs | 124 +++++++++++ .../Resources/CompleteAgile.json | 202 ++++++++++++++++++ .../YouTrackSharp.Tests.csproj | 4 + 9 files changed, 520 insertions(+), 1 deletion(-) create mode 100644 src/YouTrackSharp/Agiles/AgileService.cs create mode 100644 src/YouTrackSharp/Agiles/IAgileService.cs create mode 100644 tests/YouTrackSharp.Tests/Infrastructure/ConnectionStub.cs create mode 100644 tests/YouTrackSharp.Tests/Integration/Agiles/AgileServiceTest.cs create mode 100644 tests/YouTrackSharp.Tests/Integration/Agiles/GetAgiles.cs create mode 100644 tests/YouTrackSharp.Tests/Resources/CompleteAgile.json diff --git a/src/YouTrackSharp/Agiles/AgileService.cs b/src/YouTrackSharp/Agiles/AgileService.cs new file mode 100644 index 0000000..4bba5c7 --- /dev/null +++ b/src/YouTrackSharp/Agiles/AgileService.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Newtonsoft.Json; +using YouTrackSharp.Internal; + +namespace YouTrackSharp.Agiles { + /// + /// Service offering Agile-related operations. + /// + public class AgileService : IAgileService { + private readonly Connection _connection; + + private readonly FieldSyntaxEncoder _fieldSyntaxEncoder; + + /// + /// Creates an instance of the class. + /// + /// + /// A instance that provides a connection to the remote YouTrack server instance. + /// + /// + /// An instance that allows to encode types into Youtrack request URL format for fields + /// + public AgileService(Connection connection, FieldSyntaxEncoder fieldSyntaxEncoder) { + _connection = connection; + _fieldSyntaxEncoder = fieldSyntaxEncoder; + } + + /// + public async Task> GetAgileBoards(bool verbose = false) { + HttpClient client = await _connection.GetAuthenticatedHttpClient(); + + const int batchSize = 50; + List agileBoards = new List(); + List currentBatch; + + do { + string fields = _fieldSyntaxEncoder.Encode(typeof(Agile), verbose); + + HttpResponseMessage message = await client.GetAsync($"api/agiles?fields={fields}"); + + string response = await message.Content.ReadAsStringAsync(); + + currentBatch = JsonConvert.DeserializeObject>(response); + + agileBoards.AddRange(currentBatch); + } while (currentBatch.Count == batchSize); + + return agileBoards; + } + } +} \ No newline at end of file diff --git a/src/YouTrackSharp/Agiles/IAgileService.cs b/src/YouTrackSharp/Agiles/IAgileService.cs new file mode 100644 index 0000000..0b470d7 --- /dev/null +++ b/src/YouTrackSharp/Agiles/IAgileService.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace YouTrackSharp.Agiles { + public interface IAgileService { + /// + /// Retrieves the available agile boards from the server. + /// + /// + /// If the full representation of agile boards should be returned. + /// If this parameter is false, all the fields (and sub-fields) marked with the + /// are omitted (for more information, see + /// and related classes). + /// + /// + /// Uses the REST API + /// + /// Read a list of Agiles + /// + /// + /// A of available boards + /// When the call to the remote YouTrack server instance failed. + public Task> GetAgileBoards(bool verbose = false); + } +} \ No newline at end of file diff --git a/src/YouTrackSharp/ConnectionExtensions.cs b/src/YouTrackSharp/ConnectionExtensions.cs index 9258eeb..117018b 100644 --- a/src/YouTrackSharp/ConnectionExtensions.cs +++ b/src/YouTrackSharp/ConnectionExtensions.cs @@ -1,5 +1,7 @@ using System; using YouTrackSharp.AgileBoards; +using YouTrackSharp.Agiles; +using YouTrackSharp.Internal; using YouTrackSharp.Issues; using YouTrackSharp.Management; using YouTrackSharp.Projects; @@ -71,6 +73,16 @@ public static IProjectCustomFieldsService ProjectCustomFieldsService(this Connec { return new ProjectCustomFieldsService(connection); } + + /// + /// Creates a . + /// + /// The to create a service with. + /// for working with YouTrack agile boards. + public static IAgileService CreateAgileService(this Connection connection) + { + return new AgileService(connection, new FieldSyntaxEncoder()); + } /// /// Creates a . diff --git a/tests/YouTrackSharp.Tests/Infrastructure/ConnectionStub.cs b/tests/YouTrackSharp.Tests/Infrastructure/ConnectionStub.cs new file mode 100644 index 0000000..bd45a21 --- /dev/null +++ b/tests/YouTrackSharp.Tests/Infrastructure/ConnectionStub.cs @@ -0,0 +1,63 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace YouTrackSharp.Tests.Infrastructure { + /// + /// Connection mock used to return predefined HTTP responses, for testing purposes. + /// + public class ConnectionStub : Connection { + private Func ExecuteRequest { get; } + + private TimeSpan TimeOut => TimeSpan.FromSeconds(100); + + /// + /// Creates an instance of with give response delegate + /// + /// Request delegate + public ConnectionStub(Func executeRequest) : base("http://fake.connection.com/") { + ExecuteRequest = executeRequest; + } + + /// + /// Creates an configured to return a predefined message and HTTP status + /// on request. + /// + /// configured to return a predefined message and HTTP status + public override Task GetAuthenticatedHttpClient() { + return Task.FromResult(CreateClient()); + } + + private HttpClient CreateClient() { + HttpClient httpClient = new HttpClient(new HttpClientHandlerStub(ExecuteRequest)); + httpClient.BaseAddress = ServerUri; + httpClient.Timeout = TimeOut; + + return httpClient; + } + } + + /// + /// mock, that returns a predefined reply and HTTP status code. + /// + public class HttpClientHandlerStub : HttpClientHandler { + private Func ExecuteRequest { get; } + + /// + /// Creates an instance that delegates HttpRequestMessages + /// + /// Request delegate + public HttpClientHandlerStub(Func executeRequest) { + ExecuteRequest = executeRequest; + } + + /// + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { + HttpResponseMessage reply = ExecuteRequest?.Invoke(request); + + return Task.FromResult(reply); + } + } +} \ No newline at end of file diff --git a/tests/YouTrackSharp.Tests/Infrastructure/Connections.cs b/tests/YouTrackSharp.Tests/Infrastructure/Connections.cs index 2a45479..8c56fb6 100644 --- a/tests/YouTrackSharp.Tests/Infrastructure/Connections.cs +++ b/tests/YouTrackSharp.Tests/Infrastructure/Connections.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Net; using System.Net.Http; using System.Security.Authentication; @@ -22,7 +23,14 @@ public static string ServerUrl public static Connection Demo3Token => new BearerTokenConnection(ServerUrl, "perm:ZGVtbzM=.WW91VHJhY2tTaGFycA==.L04RdcCnjyW2UPCVg1qyb6dQflpzFy", ConfigureTestsHandler); - + + public static Connection ConnectionStub(string content, HttpStatusCode status = HttpStatusCode.OK) { + HttpResponseMessage response = new HttpResponseMessage(status); + response.Content = new StringContent(content); + + return new ConnectionStub(_ => response); + } + public static class TestData { public static readonly List ValidConnections diff --git a/tests/YouTrackSharp.Tests/Integration/Agiles/AgileServiceTest.cs b/tests/YouTrackSharp.Tests/Integration/Agiles/AgileServiceTest.cs new file mode 100644 index 0000000..5c0d589 --- /dev/null +++ b/tests/YouTrackSharp.Tests/Integration/Agiles/AgileServiceTest.cs @@ -0,0 +1,28 @@ +using System.IO; +using System.Net; +using System.Net.Http; +using System.Reflection; +using YouTrackSharp.Tests.Infrastructure; + +namespace YouTrackSharp.Tests.Integration.Agiles { + public partial class AgileServiceTest { + private static string DemoBoardId => "108-2"; + private static string DemoBoardNamePrefix => "Test Board597fb561-ea1f-4095-9636-859ae4439605"; + + private static string DemoSprintId => "109-2"; + private static string DemoSprintName => "First sprint"; + + private static string CompleteAgileJson => GetTextResource("YouTrackSharp.Tests.Resources.CompleteAgile.json"); + + private static string GetTextResource(string name) { + Assembly assembly = Assembly.GetExecutingAssembly(); + + using Stream stream = assembly.GetManifestResourceStream(name); + if (stream == null) return string.Empty; + + using StreamReader reader = new StreamReader(stream); + + return reader.ReadToEnd(); + } + } +} \ No newline at end of file diff --git a/tests/YouTrackSharp.Tests/Integration/Agiles/GetAgiles.cs b/tests/YouTrackSharp.Tests/Integration/Agiles/GetAgiles.cs new file mode 100644 index 0000000..45485fd --- /dev/null +++ b/tests/YouTrackSharp.Tests/Integration/Agiles/GetAgiles.cs @@ -0,0 +1,124 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Xunit; +using YouTrackSharp.Agiles; +using YouTrackSharp.Tests.Infrastructure; + +namespace YouTrackSharp.Tests.Integration.Agiles { + [UsedImplicitly] + public partial class AgileServiceTest { + public class GetAgiles { + [Fact] + public async Task Valid_Connection_Return_Existing_Agile_Verbose() { + // Arrange + IAgileService agileService = Connections.Demo1Token.CreateAgileService(); + + // Act + ICollection result = await agileService.GetAgileBoards(true); + + // Assert + Assert.NotNull(result); + Assert.NotEmpty(result); + + Agile demoBoard = result.FirstOrDefault(); + Assert.NotNull(demoBoard); + Assert.Equal(DemoBoardId, demoBoard.Id); + Assert.Equal(DemoBoardNamePrefix, demoBoard.Name); + + Assert.NotNull(demoBoard.ColumnSettings); + Assert.NotNull(demoBoard.Projects); + Assert.NotNull(demoBoard.Sprints); + Assert.NotNull(demoBoard.Projects); + Assert.NotNull(demoBoard.Sprints); + Assert.NotNull(demoBoard.Status); + + Assert.NotNull(demoBoard.ColumnSettings); + Assert.NotNull(demoBoard.CurrentSprint); + Assert.NotNull(demoBoard.EstimationField); + Assert.NotNull(demoBoard.SprintsSettings); + Assert.NotNull(demoBoard.SwimlaneSettings); + + + // These fields are null in the agile test + Assert.Null(demoBoard.ColorCoding); + Assert.Null(demoBoard.UpdateableBy); + Assert.Null(demoBoard.VisibleFor); + Assert.Null(demoBoard.OriginalEstimationField); + + Sprint sprint = demoBoard.Sprints.FirstOrDefault(); + Assert.NotNull(sprint); + Assert.Equal(DemoSprintId, sprint.Id); + Assert.Equal(DemoSprintName, sprint.Name); + } + + [Fact] + public async Task Verbose_Disabled_Returns_Agile_Minimum_Info() { + // Arrange + IAgileService agileService = Connections.Demo1Token.CreateAgileService(); + + // Act + ICollection result = await agileService.GetAgileBoards(false); + + // Assert + Assert.NotNull(result); + Assert.NotEmpty(result); + + Agile demoBoard = result.FirstOrDefault(); + Assert.NotNull(demoBoard); + + Assert.Equal(DemoBoardId, demoBoard.Id); + Assert.Equal(DemoBoardNamePrefix, demoBoard.Name); + } + + [Fact] + public async Task Invalid_Connection_Throws_UnauthorizedConnectionException() { + // Arrange + IAgileService agileService = Connections.UnauthorizedConnection.CreateAgileService(); + + // Act & Assert + await Assert.ThrowsAsync(async () => await agileService.GetAgileBoards()); + } + + [Fact] + public async Task Full_Agile_Json_Gets_Deserialized_Successfully() { + // Arrange + IAgileService agileService = Connections.ConnectionStub(CompleteAgileJson).CreateAgileService(); + + // Act + ICollection result = await agileService.GetAgileBoards(true); + + // Assert + Assert.NotNull(result); + Assert.NotEmpty(result); + + Agile demoBoard = result.FirstOrDefault(); + Assert.NotNull(demoBoard); + Assert.Equal(DemoBoardId, demoBoard.Id); + Assert.Equal(DemoBoardNamePrefix, demoBoard.Name); + + Assert.NotNull(demoBoard.ColumnSettings); + Assert.NotNull(demoBoard.Projects); + Assert.NotNull(demoBoard.Sprints); + Assert.NotNull(demoBoard.Projects); + Assert.NotNull(demoBoard.Sprints); + Assert.NotNull(demoBoard.Status); + Assert.NotNull(demoBoard.ColumnSettings); + Assert.NotNull(demoBoard.CurrentSprint); + Assert.NotNull(demoBoard.EstimationField); + Assert.NotNull(demoBoard.SprintsSettings); + Assert.NotNull(demoBoard.SwimlaneSettings); + Assert.NotNull(demoBoard.ColorCoding); + Assert.NotNull(demoBoard.UpdateableBy); + Assert.NotNull(demoBoard.VisibleFor); + Assert.NotNull(demoBoard.OriginalEstimationField); + + Sprint sprint = demoBoard.Sprints.FirstOrDefault(); + Assert.NotNull(sprint); + Assert.Equal(DemoSprintId, sprint.Id); + Assert.Equal(DemoSprintName, sprint.Name); + } + } + } +} \ No newline at end of file diff --git a/tests/YouTrackSharp.Tests/Resources/CompleteAgile.json b/tests/YouTrackSharp.Tests/Resources/CompleteAgile.json new file mode 100644 index 0000000..d2e2d97 --- /dev/null +++ b/tests/YouTrackSharp.Tests/Resources/CompleteAgile.json @@ -0,0 +1,202 @@ +[ + { + "projects": [ + { + "shortName": "DP1", + "name": "DemoProject1", + "id": "0-1", + "$type": "Project" + } + ], + "swimlaneSettings": { + "field": { + "customField": { + "name": "Type", + "$type": "CustomField" + }, + "presentation": "Type", + "id": "51-2", + "name": "Type", + "$type": "CustomFilterField" + }, + "values": [ + { + "name": "Feature", + "id": "Feature", + "$type": "SwimlaneValue" + } + ], + "defaultCardType": { + "name": "Task", + "id": "Task", + "$type": "SwimlaneValue" + }, + "id": "105-3", + "$type": "IssueBasedSwimlaneSettings" + }, + "estimationField": { + "name": "Estimation", + "$type": "CustomField" + }, + "sprints": [ + { + "name": "First sprint", + "id": "109-2", + "$type": "Sprint" + } + ], + "hideOrphansSwimlane": false, + "orphansAtTheTop": false, + "visibleForProjectBased": true, + "updateableByProjectBased": true, + "columnSettings": { + "field": { + "name": "State", + "$type": "CustomField" + }, + "columns": [ + { + "fieldValues": [ + { + "name": "Open", + "isResolved": false, + "id": "111-12", + "$type": "AgileColumnFieldValue" + } + ], + "ordinal": 0, + "presentation": "Open", + "wipLimit": null, + "isResolved": false, + "id": "110-8", + "$type": "AgileColumn" + }, + { + "fieldValues": [ + { + "name": "In Progress", + "isResolved": false, + "id": "111-13", + "$type": "AgileColumnFieldValue" + } + ], + "ordinal": 1, + "presentation": "In Progress", + "wipLimit": null, + "isResolved": false, + "id": "110-9", + "$type": "AgileColumn" + }, + { + "fieldValues": [ + { + "name": "Fixed", + "isResolved": true, + "id": "111-14", + "$type": "AgileColumnFieldValue" + } + ], + "ordinal": 2, + "presentation": "Fixed", + "wipLimit": null, + "isResolved": true, + "id": "110-10", + "$type": "AgileColumn" + }, + { + "fieldValues": [ + { + "name": "Verified", + "isResolved": true, + "id": "111-15", + "$type": "AgileColumnFieldValue" + } + ], + "ordinal": 3, + "presentation": "Verified", + "wipLimit": null, + "isResolved": true, + "id": "110-11", + "$type": "AgileColumn" + } + ], + "id": "108-2", + "$type": "ColumnSettings" + }, + "colorCoding": { + "prototype": { + "name": "Type", + "$type": "CustomField" + }, + "id": "108-5", + "$type": "FieldBasedColorCoding" + }, + "currentSprint": { + "name": "First sprint", + "id": "109-2", + "$type": "Sprint" + }, + "originalEstimationField": { + "name": "OriginalEstimationField", + "$type": "CustomField" + }, + "sprintsSettings": { + "explicitQuery": null, + "isExplicit": true, + "disableSprints": false, + "hideSubtasksOfCards": false, + "cardOnSeveralSprints": false, + "defaultSprint": null, + "sprintSyncField": null, + "id": "108-2", + "$type": "SprintsSettings" + }, + "visibleFor": { + "allUsersGroup" : false, + "icon" : "String", + "name" : "String", + "ringId" : "String", + "teamForProject" : { + "shortName": "DP1", + "name": "DemoProject1", + "id": "0-1", + "$type": "Project" + }, + "usersCount" : 2, + "id" : "String", + "$type" : "UserGroup" + }, + "updateableBy": { + "allUsersGroup" : false, + "icon" : "String", + "name" : "String", + "ringId" : "String", + "teamForProject" : { + "shortName": "DP1", + "name": "DemoProject1", + "id": "0-1", + "$type": "Project" + }, + "usersCount" : 2, + "id" : "String", + "$type" : "UserGroup" + }, + "status": { + "errors": [], + "valid": true, + "hasJobs": false, + "warnings": [], + "id": "boardStatus", + "$type": "AgileStatus" + }, + "owner": { + "fullName": "Demo User 1", + "ringId": "0692a47b-3670-452e-8e73-8b166a774705", + "id": "1-2", + "$type": "User" + }, + "name": "Test Board597fb561-ea1f-4095-9636-859ae4439605", + "id": "108-2", + "$type": "Agile" + } +] \ No newline at end of file diff --git a/tests/YouTrackSharp.Tests/YouTrackSharp.Tests.csproj b/tests/YouTrackSharp.Tests/YouTrackSharp.Tests.csproj index 7192467..26b6aff 100644 --- a/tests/YouTrackSharp.Tests/YouTrackSharp.Tests.csproj +++ b/tests/YouTrackSharp.Tests/YouTrackSharp.Tests.csproj @@ -18,4 +18,8 @@ YouTrackSharp + + + + \ No newline at end of file From bfc2d810648225a70cba367e751b95878f9d3c9b Mon Sep 17 00:00:00 2001 From: zelodyc Date: Thu, 4 Mar 2021 01:04:18 -0800 Subject: [PATCH 12/14] Adjusted formatting to match project's style (no behavior change) --- src/YouTrackSharp/Agiles/Agile.cs | 272 +++++------ src/YouTrackSharp/Agiles/AgileColumn.cs | 72 +-- .../Agiles/AgileColumnFieldValue.cs | 32 +- src/YouTrackSharp/Agiles/AgileService.cs | 91 ++-- src/YouTrackSharp/Agiles/AgileStatus.cs | 64 +-- .../Agiles/AttributeBasedSwimlaneSettings.cs | 36 +- src/YouTrackSharp/Agiles/ColorCoding.cs | 27 +- src/YouTrackSharp/Agiles/ColumnSettings.cs | 42 +- src/YouTrackSharp/Agiles/CustomFilterField.cs | 22 +- .../Agiles/DatabaseAttributeValue.cs | 26 +- .../Agiles/FieldBasedColorCoding.cs | 22 +- src/YouTrackSharp/Agiles/FieldStyle.cs | 42 +- src/YouTrackSharp/Agiles/FilterField.cs | 46 +- src/YouTrackSharp/Agiles/IAgileService.cs | 44 +- .../Agiles/IssueBasedSwimlaneSettings.cs | 44 +- .../Agiles/PredefinedFilterField.cs | 17 +- src/YouTrackSharp/Agiles/Project.cs | 44 +- .../Agiles/ProjectBasedColorCoding.cs | 22 +- src/YouTrackSharp/Agiles/ProjectColor.cs | 42 +- src/YouTrackSharp/Agiles/Sprint.cs | 34 +- src/YouTrackSharp/Agiles/SprintsSettings.cs | 102 ++-- .../Agiles/SwimlaneEntityAttributeValue.cs | 34 +- src/YouTrackSharp/Agiles/SwimlaneSettings.cs | 36 +- src/YouTrackSharp/Agiles/SwimlaneValue.cs | 32 +- src/YouTrackSharp/Agiles/User.cs | 46 +- src/YouTrackSharp/Agiles/UserGroup.cs | 92 ++-- src/YouTrackSharp/Agiles/WIPLimit.cs | 42 +- src/YouTrackSharp/Internal/Field.cs | 59 +-- .../Internal/FieldSyntaxEncoder.cs | 440 ++++++++++-------- src/YouTrackSharp/Json/KnownTypeConverter.cs | 109 +++-- .../Json/KnownTypeListConverter.cs | 109 +++-- .../Json/TypedJObjectConverter.cs | 36 +- .../KnownTypeAttribute.cs | 37 +- .../VerboseAttribute.cs | 14 +- .../Infrastructure/ConnectionStub.cs | 103 ++-- .../Infrastructure/Connections.cs | 3 +- .../Integration/Agiles/AgileServiceTest.cs | 41 +- .../Integration/Agiles/GetAgiles.cs | 236 +++++----- .../FieldSyntaxEncoderTestFixtures.cs | 234 +++++----- .../Internal/FieldSyntaxEncoderTests.cs | 227 ++++----- .../Json/KnownTypeConverterTestFixtures.cs | 366 ++++++++------- .../Json/KnownTypeConverterTests.cs | 276 +++++------ .../Json/KnownTypeListConverterTests.cs | 178 +++---- 43 files changed, 2053 insertions(+), 1840 deletions(-) diff --git a/src/YouTrackSharp/Agiles/Agile.cs b/src/YouTrackSharp/Agiles/Agile.cs index afad599..536af0e 100644 --- a/src/YouTrackSharp/Agiles/Agile.cs +++ b/src/YouTrackSharp/Agiles/Agile.cs @@ -4,138 +4,140 @@ using YouTrackSharp.Projects; using YouTrackSharp.SerializationAttributes; -namespace YouTrackSharp.Agiles { - /// - /// Represents an agile board configuration. - /// - public class Agile { - /// - /// Id of the Agile. - /// - [JsonProperty("id")] - public string Id { get; set; } - - /// - /// The name of the agile board. Can be null. - /// - [JsonProperty("name")] - public string Name { get; set; } - - /// - /// Owner of the agile board. Can be null. - /// - [JsonProperty("owner")] - public User Owner { get; set; } - - /// - /// The user group that can view this board. Can be null. - /// - [Verbose] - [JsonProperty("visibleFor")] - public UserGroup VisibleFor { get; set; } - - /// - /// When true, the board is visible to everyone who can view all projects that are associated with the board. - /// - [JsonProperty("visibleForProjectBased")] - public bool VisibleForProjectBased { get; set; } - - /// - /// Group of users who can update board settings. Can be null. - /// - [Verbose] - [JsonProperty("updateableBy")] - public UserGroup UpdateableBy { get; set; } - - /// - /// When true, anyone who can update the associated projects can update the board. - /// - [JsonProperty("updateableByProjectBased")] - public bool UpdateableByProjectBased { get; set; } - - /// - /// When true, the orphan swimlane is placed at the top of the board. Otherwise, the orphans swimlane is located - /// below all other swimlanes. - /// - [JsonProperty("orphansAtTheTop")] - public bool OrphansAtTheTop { get; set; } - - /// - /// When true, the orphans swimlane is not displayed on the board. - /// - [JsonProperty("hideOrphansSwimlane")] - public bool HideOrphansSwimlane { get; set; } - - /// - /// A custom field that is used as the estimation field for the board. Can be null. - /// - [Verbose] - [JsonProperty("estimationField")] - public CustomField EstimationField { get; set; } - - /// - /// A custom field that is used as the original estimation field for the board. Can be null. - /// - [Verbose] - [JsonProperty("originalEstimationField")] - public CustomField OriginalEstimationField { get; set; } - - /// - /// A collection of projects associated with the board. - /// - [Verbose] - [JsonProperty("projects")] - public List Projects { get; set; } - - /// - /// The set of sprints that are associated with the board. - /// - [Verbose] - [JsonProperty("sprints")] - public List Sprints { get; set; } - - /// - /// A sprint that is actual for the current date. Read-only. Can be null. - /// - [Verbose] - [JsonProperty("currentSprint")] - public Sprint CurrentSprint { get; set; } - - /// - /// Column settings of the board. Read-only. - /// - [Verbose] - [JsonProperty("columnSettings")] - public ColumnSettings ColumnSettings { get; set; } - - /// - /// Settings of the board swimlanes. Can be null. - /// - [Verbose] - [JsonProperty("swimlaneSettings")] - [JsonConverter(typeof(KnownTypeConverter))] - public SwimlaneSettings SwimlaneSettings { get; set; } - - /// - /// Settings of the board sprints. Read-only. - /// - [Verbose] - [JsonProperty("sprintsSettings")] - public SprintsSettings SprintsSettings { get; set; } - - /// - /// Color coding settings for the board. Can be null. - /// - [Verbose] - [JsonProperty("colorCoding")] - [JsonConverter(typeof(KnownTypeConverter))] - public ColorCoding ColorCoding { get; set; } - - /// - /// Status of the board. Read-only. - /// - [Verbose] - [JsonProperty("status")] - public AgileStatus Status { get; set; } - } -} +namespace YouTrackSharp.Agiles +{ + /// + /// Represents an agile board configuration. + /// + public class Agile + { + /// + /// Id of the Agile. + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// The name of the agile board. Can be null. + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// Owner of the agile board. Can be null. + /// + [JsonProperty("owner")] + public User Owner { get; set; } + + /// + /// The user group that can view this board. Can be null. + /// + [Verbose] + [JsonProperty("visibleFor")] + public UserGroup VisibleFor { get; set; } + + /// + /// When true, the board is visible to everyone who can view all projects that are associated with the board. + /// + [JsonProperty("visibleForProjectBased")] + public bool VisibleForProjectBased { get; set; } + + /// + /// Group of users who can update board settings. Can be null. + /// + [Verbose] + [JsonProperty("updateableBy")] + public UserGroup UpdateableBy { get; set; } + + /// + /// When true, anyone who can update the associated projects can update the board. + /// + [JsonProperty("updateableByProjectBased")] + public bool UpdateableByProjectBased { get; set; } + + /// + /// When true, the orphan swimlane is placed at the top of the board. Otherwise, the orphans swimlane is located + /// below all other swimlanes. + /// + [JsonProperty("orphansAtTheTop")] + public bool OrphansAtTheTop { get; set; } + + /// + /// When true, the orphans swimlane is not displayed on the board. + /// + [JsonProperty("hideOrphansSwimlane")] + public bool HideOrphansSwimlane { get; set; } + + /// + /// A custom field that is used as the estimation field for the board. Can be null. + /// + [Verbose] + [JsonProperty("estimationField")] + public CustomField EstimationField { get; set; } + + /// + /// A custom field that is used as the original estimation field for the board. Can be null. + /// + [Verbose] + [JsonProperty("originalEstimationField")] + public CustomField OriginalEstimationField { get; set; } + + /// + /// A collection of projects associated with the board. + /// + [Verbose] + [JsonProperty("projects")] + public List Projects { get; set; } + + /// + /// The set of sprints that are associated with the board. + /// + [Verbose] + [JsonProperty("sprints")] + public List Sprints { get; set; } + + /// + /// A sprint that is actual for the current date. Read-only. Can be null. + /// + [Verbose] + [JsonProperty("currentSprint")] + public Sprint CurrentSprint { get; set; } + + /// + /// Column settings of the board. Read-only. + /// + [Verbose] + [JsonProperty("columnSettings")] + public ColumnSettings ColumnSettings { get; set; } + + /// + /// Settings of the board swimlanes. Can be null. + /// + [Verbose] + [JsonProperty("swimlaneSettings")] + [JsonConverter(typeof(KnownTypeConverter))] + public SwimlaneSettings SwimlaneSettings { get; set; } + + /// + /// Settings of the board sprints. Read-only. + /// + [Verbose] + [JsonProperty("sprintsSettings")] + public SprintsSettings SprintsSettings { get; set; } + + /// + /// Color coding settings for the board. Can be null. + /// + [Verbose] + [JsonProperty("colorCoding")] + [JsonConverter(typeof(KnownTypeConverter))] + public ColorCoding ColorCoding { get; set; } + + /// + /// Status of the board. Read-only. + /// + [Verbose] + [JsonProperty("status")] + public AgileStatus Status { get; set; } + } +} \ No newline at end of file diff --git a/src/YouTrackSharp/Agiles/AgileColumn.cs b/src/YouTrackSharp/Agiles/AgileColumn.cs index 07b25d6..8f2a90e 100644 --- a/src/YouTrackSharp/Agiles/AgileColumn.cs +++ b/src/YouTrackSharp/Agiles/AgileColumn.cs @@ -1,45 +1,47 @@ using System.Collections.Generic; using Newtonsoft.Json; -namespace YouTrackSharp.Agiles { - /// - /// Represents settings for a single board column - /// - public class AgileColumn { +namespace YouTrackSharp.Agiles +{ /// - /// Id of the AgileColumn. + /// Represents settings for a single board column /// - [JsonProperty("id")] - public string Id { get; set; } + public class AgileColumn + { + /// + /// Id of the AgileColumn. + /// + [JsonProperty("id")] + public string Id { get; set; } - /// - /// Text presentation of values stored in a column. Read-only. Can be null. - /// - [JsonProperty("presentation")] - public string Presentation { get; set; } + /// + /// Text presentation of values stored in a column. Read-only. Can be null. + /// + [JsonProperty("presentation")] + public string Presentation { get; set; } - /// - /// true if a column represents resolved state of an issue. Can be updated only for newly created value. Read-only. - /// - [JsonProperty("isResolved")] - public bool IsResolved { get; set; } + /// + /// true if a column represents resolved state of an issue. Can be updated only for newly created value. Read-only. + /// + [JsonProperty("isResolved")] + public bool IsResolved { get; set; } - /// - /// Order of this column on board, counting from left to right. - /// - [JsonProperty("ordinal")] - public int Ordinal { get; set; } + /// + /// Order of this column on board, counting from left to right. + /// + [JsonProperty("ordinal")] + public int Ordinal { get; set; } - /// - /// WIP limit for this column. Can be null. - /// - [JsonProperty("wipLimit")] - public WIPLimit WipLimit { get; set; } + /// + /// WIP limit for this column. Can be null. + /// + [JsonProperty("wipLimit")] + public WIPLimit WipLimit { get; set; } - /// - /// Field values represented by this column. - /// - [JsonProperty("fieldValues")] - public List FieldValues { get; set; } - } -} + /// + /// Field values represented by this column. + /// + [JsonProperty("fieldValues")] + public List FieldValues { get; set; } + } +} \ No newline at end of file diff --git a/src/YouTrackSharp/Agiles/AgileColumnFieldValue.cs b/src/YouTrackSharp/Agiles/AgileColumnFieldValue.cs index 44cb647..30b240e 100644 --- a/src/YouTrackSharp/Agiles/AgileColumnFieldValue.cs +++ b/src/YouTrackSharp/Agiles/AgileColumnFieldValue.cs @@ -1,20 +1,22 @@ using Newtonsoft.Json; -namespace YouTrackSharp.Agiles { - /// - /// Represents a field value or values, parameterizing agile column. - /// - public class AgileColumnFieldValue : DatabaseAttributeValue { +namespace YouTrackSharp.Agiles +{ /// - /// Presentation of a field value or values. Can be null. + /// Represents a field value or values, parameterizing agile column. /// - [JsonProperty("name")] - public string Name { get; set; } + public class AgileColumnFieldValue : DatabaseAttributeValue + { + /// + /// Presentation of a field value or values. Can be null. + /// + [JsonProperty("name")] + public string Name { get; set; } - /// - /// True, if field has type State and teh value is resolved or all values are resolved. Read-only. - /// - [JsonProperty("isResolved")] - public bool IsResolved { get; set; } - } -} + /// + /// True, if field has type State and teh value is resolved or all values are resolved. Read-only. + /// + [JsonProperty("isResolved")] + public bool IsResolved { get; set; } + } +} \ No newline at end of file diff --git a/src/YouTrackSharp/Agiles/AgileService.cs b/src/YouTrackSharp/Agiles/AgileService.cs index 4bba5c7..0f8ead3 100644 --- a/src/YouTrackSharp/Agiles/AgileService.cs +++ b/src/YouTrackSharp/Agiles/AgileService.cs @@ -4,50 +4,55 @@ using Newtonsoft.Json; using YouTrackSharp.Internal; -namespace YouTrackSharp.Agiles { - /// - /// Service offering Agile-related operations. - /// - public class AgileService : IAgileService { - private readonly Connection _connection; - - private readonly FieldSyntaxEncoder _fieldSyntaxEncoder; - +namespace YouTrackSharp.Agiles +{ /// - /// Creates an instance of the class. + /// Service offering Agile-related operations. /// - /// - /// A instance that provides a connection to the remote YouTrack server instance. - /// - /// - /// An instance that allows to encode types into Youtrack request URL format for fields - /// - public AgileService(Connection connection, FieldSyntaxEncoder fieldSyntaxEncoder) { - _connection = connection; - _fieldSyntaxEncoder = fieldSyntaxEncoder; - } - - /// - public async Task> GetAgileBoards(bool verbose = false) { - HttpClient client = await _connection.GetAuthenticatedHttpClient(); - - const int batchSize = 50; - List agileBoards = new List(); - List currentBatch; - - do { - string fields = _fieldSyntaxEncoder.Encode(typeof(Agile), verbose); - - HttpResponseMessage message = await client.GetAsync($"api/agiles?fields={fields}"); - - string response = await message.Content.ReadAsStringAsync(); - - currentBatch = JsonConvert.DeserializeObject>(response); - - agileBoards.AddRange(currentBatch); - } while (currentBatch.Count == batchSize); - - return agileBoards; + public class AgileService : IAgileService + { + private readonly Connection _connection; + + private readonly FieldSyntaxEncoder _fieldSyntaxEncoder; + + /// + /// Creates an instance of the class. + /// + /// + /// A instance that provides a connection to the remote YouTrack server instance. + /// + /// + /// An instance that allows to encode types into Youtrack request URL format for fields + /// + public AgileService(Connection connection, FieldSyntaxEncoder fieldSyntaxEncoder) + { + _connection = connection; + _fieldSyntaxEncoder = fieldSyntaxEncoder; + } + + /// + public async Task> GetAgileBoards(bool verbose = false) + { + HttpClient client = await _connection.GetAuthenticatedHttpClient(); + + const int batchSize = 50; + List agileBoards = new List(); + List currentBatch; + + do + { + string fields = _fieldSyntaxEncoder.Encode(typeof(Agile), verbose); + + HttpResponseMessage message = await client.GetAsync($"api/agiles?fields={fields}"); + + string response = await message.Content.ReadAsStringAsync(); + + currentBatch = JsonConvert.DeserializeObject>(response); + + agileBoards.AddRange(currentBatch); + } while (currentBatch.Count == batchSize); + + return agileBoards; + } } - } } \ No newline at end of file diff --git a/src/YouTrackSharp/Agiles/AgileStatus.cs b/src/YouTrackSharp/Agiles/AgileStatus.cs index fe13538..1958fad 100644 --- a/src/YouTrackSharp/Agiles/AgileStatus.cs +++ b/src/YouTrackSharp/Agiles/AgileStatus.cs @@ -1,40 +1,42 @@ using System.Collections.Generic; using Newtonsoft.Json; -namespace YouTrackSharp.Agiles { - /// - /// Shows if the board has any configuration problems. - /// - public class AgileStatus { +namespace YouTrackSharp.Agiles +{ /// - /// Id of the AgileStatus. + /// Shows if the board has any configuration problems. /// - [JsonProperty("id")] - public string Id { get; set; } + public class AgileStatus + { + /// + /// Id of the AgileStatus. + /// + [JsonProperty("id")] + public string Id { get; set; } - /// - /// true if the board is in valid state and can be used. Read-only. - /// - [JsonProperty("valid")] - public bool Valid { get; set; } + /// + /// true if the board is in valid state and can be used. Read-only. + /// + [JsonProperty("valid")] + public bool Valid { get; set; } - /// - /// If `true`, then a background job is currently being executed for the board. In this case, while a background - /// job is running, the board cannot be updated. Read-only. - /// - [JsonProperty("hasJobs")] - public bool HasJobs { get; set; } + /// + /// If `true`, then a background job is currently being executed for the board. In this case, while a background + /// job is running, the board cannot be updated. Read-only. + /// + [JsonProperty("hasJobs")] + public bool HasJobs { get; set; } - /// - /// List of configuration errors found for this board. Read-only. - /// - [JsonProperty("errors")] - public List Errors { get; set; } + /// + /// List of configuration errors found for this board. Read-only. + /// + [JsonProperty("errors")] + public List Errors { get; set; } - /// - /// List of configuration-related warnings found for this board. Read-only. - /// - [JsonProperty("warnings")] - public List Warnings { get; set; } - } -} + /// + /// List of configuration-related warnings found for this board. Read-only. + /// + [JsonProperty("warnings")] + public List Warnings { get; set; } + } +} \ No newline at end of file diff --git a/src/YouTrackSharp/Agiles/AttributeBasedSwimlaneSettings.cs b/src/YouTrackSharp/Agiles/AttributeBasedSwimlaneSettings.cs index 10d9b4a..e14a385 100644 --- a/src/YouTrackSharp/Agiles/AttributeBasedSwimlaneSettings.cs +++ b/src/YouTrackSharp/Agiles/AttributeBasedSwimlaneSettings.cs @@ -2,23 +2,25 @@ using Newtonsoft.Json; using YouTrackSharp.Json; -namespace YouTrackSharp.Agiles { - /// - /// Settings of swimlanes that are identified by the set of values in the selected field. For example, you can set - /// swimlanes to represent issues for each Assignee. - /// - public class AttributeBasedSwimlaneSettings : SwimlaneSettings { +namespace YouTrackSharp.Agiles +{ /// - /// CustomField which values are used to identify swimlane. + /// Settings of swimlanes that are identified by the set of values in the selected field. For example, you can set + /// swimlanes to represent issues for each Assignee. /// - [JsonProperty("field")] - [JsonConverter(typeof(KnownTypeConverter))] - public FilterField Field { get; set; } + public class AttributeBasedSwimlaneSettings : SwimlaneSettings + { + /// + /// CustomField which values are used to identify swimlane. + /// + [JsonProperty("field")] + [JsonConverter(typeof(KnownTypeConverter))] + public FilterField Field { get; set; } - /// - /// Swimlanes that are visible on the Board. - /// - [JsonProperty("values")] - public List Values { get; set; } - } -} + /// + /// Swimlanes that are visible on the Board. + /// + [JsonProperty("values")] + public List Values { get; set; } + } +} \ No newline at end of file diff --git a/src/YouTrackSharp/Agiles/ColorCoding.cs b/src/YouTrackSharp/Agiles/ColorCoding.cs index 55bb7c3..e158fba 100644 --- a/src/YouTrackSharp/Agiles/ColorCoding.cs +++ b/src/YouTrackSharp/Agiles/ColorCoding.cs @@ -1,18 +1,19 @@ using Newtonsoft.Json; using YouTrackSharp.SerializationAttributes; -namespace YouTrackSharp.Agiles { - - /// - /// Describe rules according to which different colors are used for cards on agile board. - /// - [KnownType(typeof(FieldBasedColorCoding))] - [KnownType(typeof(ProjectBasedColorCoding))] - public class ColorCoding { +namespace YouTrackSharp.Agiles +{ /// - /// Id of the ColorCoding. + /// Describe rules according to which different colors are used for cards on agile board. /// - [JsonProperty("id")] - public string Id { get; set; } - } -} + [KnownType(typeof(FieldBasedColorCoding))] + [KnownType(typeof(ProjectBasedColorCoding))] + public class ColorCoding + { + /// + /// Id of the ColorCoding. + /// + [JsonProperty("id")] + public string Id { get; set; } + } +} \ No newline at end of file diff --git a/src/YouTrackSharp/Agiles/ColumnSettings.cs b/src/YouTrackSharp/Agiles/ColumnSettings.cs index 69f8514..10e9194 100644 --- a/src/YouTrackSharp/Agiles/ColumnSettings.cs +++ b/src/YouTrackSharp/Agiles/ColumnSettings.cs @@ -2,27 +2,29 @@ using Newtonsoft.Json; using YouTrackSharp.Projects; -namespace YouTrackSharp.Agiles { - /// - /// Agile board columns settings. - /// - public class ColumnSettings { +namespace YouTrackSharp.Agiles +{ /// - /// Id of the ColumnSettings. + /// Agile board columns settings. /// - [JsonProperty("id")] - public string Id { get; set; } + public class ColumnSettings + { + /// + /// Id of the ColumnSettings. + /// + [JsonProperty("id")] + public string Id { get; set; } - /// - /// Custom field, which values are used for columns. Can be null. - /// - [JsonProperty("field")] - public CustomField Field { get; set; } + /// + /// Custom field, which values are used for columns. Can be null. + /// + [JsonProperty("field")] + public CustomField Field { get; set; } - /// - /// Columns that are shown on the board. - /// - [JsonProperty("columns")] - public List Columns { get; set; } - } -} + /// + /// Columns that are shown on the board. + /// + [JsonProperty("columns")] + public List Columns { get; set; } + } +} \ No newline at end of file diff --git a/src/YouTrackSharp/Agiles/CustomFilterField.cs b/src/YouTrackSharp/Agiles/CustomFilterField.cs index 9a80aae..22d6610 100644 --- a/src/YouTrackSharp/Agiles/CustomFilterField.cs +++ b/src/YouTrackSharp/Agiles/CustomFilterField.cs @@ -1,15 +1,17 @@ using Newtonsoft.Json; using YouTrackSharp.Projects; -namespace YouTrackSharp.Agiles { - /// - /// Represents a custom field of the issue. - /// - public class CustomFilterField : FilterField { +namespace YouTrackSharp.Agiles +{ /// - /// Reference to settings of the custom field. Read-only. + /// Represents a custom field of the issue. /// - [JsonProperty("customField")] - public CustomField CustomField { get; set; } - } -} + public class CustomFilterField : FilterField + { + /// + /// Reference to settings of the custom field. Read-only. + /// + [JsonProperty("customField")] + public CustomField CustomField { get; set; } + } +} \ No newline at end of file diff --git a/src/YouTrackSharp/Agiles/DatabaseAttributeValue.cs b/src/YouTrackSharp/Agiles/DatabaseAttributeValue.cs index a913ced..51e6ed5 100644 --- a/src/YouTrackSharp/Agiles/DatabaseAttributeValue.cs +++ b/src/YouTrackSharp/Agiles/DatabaseAttributeValue.cs @@ -1,17 +1,19 @@ using Newtonsoft.Json; using YouTrackSharp.SerializationAttributes; -namespace YouTrackSharp.Agiles { - /// - /// Represents string reference to the value. - /// - [KnownType(typeof(SwimlaneEntityAttributeValue))] - [KnownType(typeof(AgileColumnFieldValue))] - public class DatabaseAttributeValue { +namespace YouTrackSharp.Agiles +{ /// - /// Id of the DatabaseAttributeValue. + /// Represents string reference to the value. /// - [JsonProperty("id")] - public string Id { get; set; } - } -} + [KnownType(typeof(SwimlaneEntityAttributeValue))] + [KnownType(typeof(AgileColumnFieldValue))] + public class DatabaseAttributeValue + { + /// + /// Id of the DatabaseAttributeValue. + /// + [JsonProperty("id")] + public string Id { get; set; } + } +} \ No newline at end of file diff --git a/src/YouTrackSharp/Agiles/FieldBasedColorCoding.cs b/src/YouTrackSharp/Agiles/FieldBasedColorCoding.cs index 96ec5a8..f8526fe 100644 --- a/src/YouTrackSharp/Agiles/FieldBasedColorCoding.cs +++ b/src/YouTrackSharp/Agiles/FieldBasedColorCoding.cs @@ -1,15 +1,17 @@ using Newtonsoft.Json; using YouTrackSharp.Projects; -namespace YouTrackSharp.Agiles { - /// - /// Allows to set card's color based on a value of some custom field. - /// - public class FieldBasedColorCoding : ColorCoding { +namespace YouTrackSharp.Agiles +{ /// - /// Sets card color based on this custom field. Can be null. + /// Allows to set card's color based on a value of some custom field. /// - [JsonProperty("prototype")] - public CustomField Prototype { get; set; } - } -} + public class FieldBasedColorCoding : ColorCoding + { + /// + /// Sets card color based on this custom field. Can be null. + /// + [JsonProperty("prototype")] + public CustomField Prototype { get; set; } + } +} \ No newline at end of file diff --git a/src/YouTrackSharp/Agiles/FieldStyle.cs b/src/YouTrackSharp/Agiles/FieldStyle.cs index aef18f0..c7f4a6e 100644 --- a/src/YouTrackSharp/Agiles/FieldStyle.cs +++ b/src/YouTrackSharp/Agiles/FieldStyle.cs @@ -1,26 +1,28 @@ using Newtonsoft.Json; -namespace YouTrackSharp.Agiles { - /// - /// Represents the style settings of the field in YouTrack. - /// - public class FieldStyle { +namespace YouTrackSharp.Agiles +{ /// - /// Id of the FieldStyle. + /// Represents the style settings of the field in YouTrack. /// - [JsonProperty("id")] - public string Id { get; set; } + public class FieldStyle + { + /// + /// Id of the FieldStyle. + /// + [JsonProperty("id")] + public string Id { get; set; } - /// - /// Background color. Read-only. Can be null. - /// - [JsonProperty("background")] - public string Background { get; set; } + /// + /// Background color. Read-only. Can be null. + /// + [JsonProperty("background")] + public string Background { get; set; } - /// - /// Foreground color. Read-only. Can be null. - /// - [JsonProperty("foreground")] - public string Foreground { get; set; } - } -} + /// + /// Foreground color. Read-only. Can be null. + /// + [JsonProperty("foreground")] + public string Foreground { get; set; } + } +} \ No newline at end of file diff --git a/src/YouTrackSharp/Agiles/FilterField.cs b/src/YouTrackSharp/Agiles/FilterField.cs index 20f3256..1760ff4 100644 --- a/src/YouTrackSharp/Agiles/FilterField.cs +++ b/src/YouTrackSharp/Agiles/FilterField.cs @@ -1,29 +1,31 @@ using Newtonsoft.Json; using YouTrackSharp.SerializationAttributes; -namespace YouTrackSharp.Agiles { - /// - /// Represents an issue property, which can be a predefined field, a custom field, a link, and so on. - /// - [KnownType(typeof(PredefinedFilterField))] - [KnownType(typeof(CustomFilterField))] - public class FilterField { +namespace YouTrackSharp.Agiles +{ /// - /// Id of the FilterField. + /// Represents an issue property, which can be a predefined field, a custom field, a link, and so on. /// - [JsonProperty("id")] - public string Id { get; set; } + [KnownType(typeof(PredefinedFilterField))] + [KnownType(typeof(CustomFilterField))] + public class FilterField + { + /// + /// Id of the FilterField. + /// + [JsonProperty("id")] + public string Id { get; set; } - /// - /// Presentation of the field. Read-only. - /// - [JsonProperty("presentation")] - public string Presentation { get; set; } + /// + /// Presentation of the field. Read-only. + /// + [JsonProperty("presentation")] + public string Presentation { get; set; } - /// - /// The name of the field. Read-only. - /// - [JsonProperty("name")] - public string Name { get; set; } - } -} + /// + /// The name of the field. Read-only. + /// + [JsonProperty("name")] + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/src/YouTrackSharp/Agiles/IAgileService.cs b/src/YouTrackSharp/Agiles/IAgileService.cs index 0b470d7..9b4393a 100644 --- a/src/YouTrackSharp/Agiles/IAgileService.cs +++ b/src/YouTrackSharp/Agiles/IAgileService.cs @@ -1,25 +1,27 @@ using System.Collections.Generic; using System.Threading.Tasks; -namespace YouTrackSharp.Agiles { - public interface IAgileService { - /// - /// Retrieves the available agile boards from the server. - /// - /// - /// If the full representation of agile boards should be returned. - /// If this parameter is false, all the fields (and sub-fields) marked with the - /// are omitted (for more information, see - /// and related classes). - /// - /// - /// Uses the REST API - /// - /// Read a list of Agiles - /// - /// - /// A of available boards - /// When the call to the remote YouTrack server instance failed. - public Task> GetAgileBoards(bool verbose = false); - } +namespace YouTrackSharp.Agiles +{ + public interface IAgileService + { + /// + /// Retrieves the available agile boards from the server. + /// + /// + /// If the full representation of agile boards should be returned. + /// If this parameter is false, all the fields (and sub-fields) marked with the + /// are omitted (for more information, see + /// and related classes). + /// + /// + /// Uses the REST API + /// + /// Read a list of Agiles + /// + /// + /// A of available boards + /// When the call to the remote YouTrack server instance failed. + public Task> GetAgileBoards(bool verbose = false); + } } \ No newline at end of file diff --git a/src/YouTrackSharp/Agiles/IssueBasedSwimlaneSettings.cs b/src/YouTrackSharp/Agiles/IssueBasedSwimlaneSettings.cs index e3b8a78..b818c2d 100644 --- a/src/YouTrackSharp/Agiles/IssueBasedSwimlaneSettings.cs +++ b/src/YouTrackSharp/Agiles/IssueBasedSwimlaneSettings.cs @@ -2,28 +2,30 @@ using Newtonsoft.Json; using YouTrackSharp.Json; -namespace YouTrackSharp.Agiles { - /// - /// Base entity for different swimlane settings - /// - public class IssueBasedSwimlaneSettings : SwimlaneSettings { +namespace YouTrackSharp.Agiles +{ /// - /// CustomField which values are used to identify swimlane. + /// Base entity for different swimlane settings /// - [JsonProperty("field")] - [JsonConverter(typeof(KnownTypeConverter))] - public FilterField Field { get; set; } + public class IssueBasedSwimlaneSettings : SwimlaneSettings + { + /// + /// CustomField which values are used to identify swimlane. + /// + [JsonProperty("field")] + [JsonConverter(typeof(KnownTypeConverter))] + public FilterField Field { get; set; } - /// - /// Value of a field that a card would have by default. Can be null. - /// - [JsonProperty("defaultCardType")] - public SwimlaneValue DefaultCardType { get; set; } + /// + /// Value of a field that a card would have by default. Can be null. + /// + [JsonProperty("defaultCardType")] + public SwimlaneValue DefaultCardType { get; set; } - /// - /// When issue has one of this values, it becomes a swimlane on this board. - /// - [JsonProperty("values")] - public List Values { get; set; } - } -} + /// + /// When issue has one of this values, it becomes a swimlane on this board. + /// + [JsonProperty("values")] + public List Values { get; set; } + } +} \ No newline at end of file diff --git a/src/YouTrackSharp/Agiles/PredefinedFilterField.cs b/src/YouTrackSharp/Agiles/PredefinedFilterField.cs index 34731b7..19e4a1a 100644 --- a/src/YouTrackSharp/Agiles/PredefinedFilterField.cs +++ b/src/YouTrackSharp/Agiles/PredefinedFilterField.cs @@ -1,8 +1,9 @@ -namespace YouTrackSharp.Agiles { - /// - /// Represents a predefined field of the issue. Predefined fields are always present in an issue and |cannot be - /// customized in a project. For example, project, created, |updated, tags, and so on. - /// - public class PredefinedFilterField : FilterField { - } -} +namespace YouTrackSharp.Agiles +{ + /// + /// Represents a predefined field of the issue. Predefined fields are always present in an issue and |cannot be + /// customized in a project. For example, project, created, |updated, tags, and so on. + /// + public class PredefinedFilterField : FilterField + { } +} \ No newline at end of file diff --git a/src/YouTrackSharp/Agiles/Project.cs b/src/YouTrackSharp/Agiles/Project.cs index 2b3dce0..42576e8 100644 --- a/src/YouTrackSharp/Agiles/Project.cs +++ b/src/YouTrackSharp/Agiles/Project.cs @@ -1,27 +1,29 @@ using Newtonsoft.Json; -namespace YouTrackSharp.Agiles { - /// - /// Represents a project in the context of an agile board. The class only provides the id, short name and name of the - /// project see for more info on how to access a YouTrack project. - /// - public class Project { +namespace YouTrackSharp.Agiles +{ /// - /// Id of the Project. + /// Represents a project in the context of an agile board. The class only provides the id, short name and name of the + /// project see for more info on how to access a YouTrack project. /// - [JsonProperty("id")] - public string Id { get; set; } + public class Project + { + /// + /// Id of the Project. + /// + [JsonProperty("id")] + public string Id { get; set; } - /// - /// The ID of the project. This short name is also a prefix for an issue ID. Can be null. - /// - [JsonProperty("shortName")] - public string ShortName { get; set; } + /// + /// The ID of the project. This short name is also a prefix for an issue ID. Can be null. + /// + [JsonProperty("shortName")] + public string ShortName { get; set; } - /// - /// The name of the issue folder. Can be null. - /// - [JsonProperty("name")] - public string Name { get; set; } - } -} + /// + /// The name of the issue folder. Can be null. + /// + [JsonProperty("name")] + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/src/YouTrackSharp/Agiles/ProjectBasedColorCoding.cs b/src/YouTrackSharp/Agiles/ProjectBasedColorCoding.cs index b8dc5ab..13ede65 100644 --- a/src/YouTrackSharp/Agiles/ProjectBasedColorCoding.cs +++ b/src/YouTrackSharp/Agiles/ProjectBasedColorCoding.cs @@ -1,15 +1,17 @@ using System.Collections.Generic; using Newtonsoft.Json; -namespace YouTrackSharp.Agiles { - /// - /// Allows to set card's color based on it's project - /// - public class ProjectBasedColorCoding : ColorCoding { +namespace YouTrackSharp.Agiles +{ /// - /// Collection of per-project color settings + /// Allows to set card's color based on it's project /// - [JsonProperty("projectColors")] - public List ProjectColors { get; set; } - } -} + public class ProjectBasedColorCoding : ColorCoding + { + /// + /// Collection of per-project color settings + /// + [JsonProperty("projectColors")] + public List ProjectColors { get; set; } + } +} \ No newline at end of file diff --git a/src/YouTrackSharp/Agiles/ProjectColor.cs b/src/YouTrackSharp/Agiles/ProjectColor.cs index cd76c44..e4280f2 100644 --- a/src/YouTrackSharp/Agiles/ProjectColor.cs +++ b/src/YouTrackSharp/Agiles/ProjectColor.cs @@ -1,26 +1,28 @@ using Newtonsoft.Json; -namespace YouTrackSharp.Agiles { - /// - /// Represent color setting for one project on the board. - /// - public class ProjectColor { +namespace YouTrackSharp.Agiles +{ /// - /// Id of the ProjectColor. + /// Represent color setting for one project on the board. /// - [JsonProperty("id")] - public string Id { get; set; } + public class ProjectColor + { + /// + /// Id of the ProjectColor. + /// + [JsonProperty("id")] + public string Id { get; set; } - /// - /// A project, to which color this setting describes. Read-only. Can be null. - /// - [JsonProperty("project")] - public Project Project { get; set; } + /// + /// A project, to which color this setting describes. Read-only. Can be null. + /// + [JsonProperty("project")] + public Project Project { get; set; } - /// - /// A color, that issues of this project will have on the board. Read-only. Can be null. - /// - [JsonProperty("color")] - public FieldStyle Color { get; set; } - } -} + /// + /// A color, that issues of this project will have on the board. Read-only. Can be null. + /// + [JsonProperty("color")] + public FieldStyle Color { get; set; } + } +} \ No newline at end of file diff --git a/src/YouTrackSharp/Agiles/Sprint.cs b/src/YouTrackSharp/Agiles/Sprint.cs index d621a96..b7a9485 100644 --- a/src/YouTrackSharp/Agiles/Sprint.cs +++ b/src/YouTrackSharp/Agiles/Sprint.cs @@ -1,21 +1,23 @@ using Newtonsoft.Json; -namespace YouTrackSharp.Agiles { - /// - /// Represents a sprint in the context of an agile board. The class only provides the sprint's id and name, see for more info on how to access a YouTrack sprint. - /// - public class Sprint { +namespace YouTrackSharp.Agiles +{ /// - /// Id of the Sprint. + /// Represents a sprint in the context of an agile board. The class only provides the sprint's id and name, see for more info on how to access a YouTrack sprint. /// - [JsonProperty("id")] - public string Id { get; set; } + public class Sprint + { + /// + /// Id of the Sprint. + /// + [JsonProperty("id")] + public string Id { get; set; } - /// - /// Name of the sprint. Can be null. - /// - [JsonProperty("name")] - public string Name { get; set; } - } -} + /// + /// Name of the sprint. Can be null. + /// + [JsonProperty("name")] + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/src/YouTrackSharp/Agiles/SprintsSettings.cs b/src/YouTrackSharp/Agiles/SprintsSettings.cs index cc3de8b..ab32bde 100644 --- a/src/YouTrackSharp/Agiles/SprintsSettings.cs +++ b/src/YouTrackSharp/Agiles/SprintsSettings.cs @@ -1,62 +1,64 @@ using Newtonsoft.Json; using YouTrackSharp.Projects; -namespace YouTrackSharp.Agiles { - /// - /// Describes sprints configuration. - /// - public class SprintsSettings { +namespace YouTrackSharp.Agiles +{ /// - /// Id of the SprintsSettings. + /// Describes sprints configuration. /// - [JsonProperty("id")] - public string Id { get; set; } + public class SprintsSettings + { + /// + /// Id of the SprintsSettings. + /// + [JsonProperty("id")] + public string Id { get; set; } - /// - /// If true, issues should be added to the board manually. If false, issues are shown on the board based on the - /// query and/or value of a field. - /// - [JsonProperty("isExplicit")] - public bool IsExplicit { get; set; } + /// + /// If true, issues should be added to the board manually. If false, issues are shown on the board based on the + /// query and/or value of a field. + /// + [JsonProperty("isExplicit")] + public bool IsExplicit { get; set; } - /// - /// If true, cards can be present on several sprints of this board. - /// - [JsonProperty("cardOnSeveralSprints")] - public bool CardOnSeveralSprints { get; set; } + /// + /// If true, cards can be present on several sprints of this board. + /// + [JsonProperty("cardOnSeveralSprints")] + public bool CardOnSeveralSprints { get; set; } - /// - /// New cards are added to this sprint by default. This setting applies only if isExplicit == true. Can be null. - /// - [JsonProperty("defaultSprint")] - public Sprint DefaultSprint { get; set; } + /// + /// New cards are added to this sprint by default. This setting applies only if isExplicit == true. Can be null. + /// + [JsonProperty("defaultSprint")] + public Sprint DefaultSprint { get; set; } - /// - /// If true, agile board has no distinct sprints in UI. However, in API it will look like it has only one active - /// (not-archived) sprint. - /// - [JsonProperty("disableSprints")] - public bool DisableSprints { get; set; } + /// + /// If true, agile board has no distinct sprints in UI. However, in API it will look like it has only one active + /// (not-archived) sprint. + /// + [JsonProperty("disableSprints")] + public bool DisableSprints { get; set; } - /// - /// Issues that match this query will appear on the board. This setting applies only if isExplicit == false. Can be - /// null. - /// - [JsonProperty("explicitQuery")] - public string ExplicitQuery { get; set; } + /// + /// Issues that match this query will appear on the board. This setting applies only if isExplicit == false. Can be + /// null. + /// + [JsonProperty("explicitQuery")] + public string ExplicitQuery { get; set; } - /// - /// Based on the value of this field, issues will be assigned to the sprints. This setting applies only if - /// isExplicit == false. Can be null. - /// - [JsonProperty("sprintSyncField")] - public CustomField SprintSyncField { get; set; } + /// + /// Based on the value of this field, issues will be assigned to the sprints. This setting applies only if + /// isExplicit == false. Can be null. + /// + [JsonProperty("sprintSyncField")] + public CustomField SprintSyncField { get; set; } - /// - /// If true, subtasks of the cards, that are present on the board, will be hidden if they match board query. This - /// setting applies only if isExplicit == false. - /// - [JsonProperty("hideSubtasksOfCards")] - public bool HideSubtasksOfCards { get; set; } - } -} + /// + /// If true, subtasks of the cards, that are present on the board, will be hidden if they match board query. This + /// setting applies only if isExplicit == false. + /// + [JsonProperty("hideSubtasksOfCards")] + public bool HideSubtasksOfCards { get; set; } + } +} \ No newline at end of file diff --git a/src/YouTrackSharp/Agiles/SwimlaneEntityAttributeValue.cs b/src/YouTrackSharp/Agiles/SwimlaneEntityAttributeValue.cs index 2b54a6f..d777396 100644 --- a/src/YouTrackSharp/Agiles/SwimlaneEntityAttributeValue.cs +++ b/src/YouTrackSharp/Agiles/SwimlaneEntityAttributeValue.cs @@ -1,21 +1,23 @@ using Newtonsoft.Json; -namespace YouTrackSharp.Agiles { - /// - /// Represents a single swimlane in case of AttributeBasedSwimlaneSettings. - /// - public class SwimlaneEntityAttributeValue : DatabaseAttributeValue { +namespace YouTrackSharp.Agiles +{ /// - /// Name of the swimlane. Can be null. + /// Represents a single swimlane in case of AttributeBasedSwimlaneSettings. /// - [JsonProperty("name")] - public string Name { get; set; } + public class SwimlaneEntityAttributeValue : DatabaseAttributeValue + { + /// + /// Name of the swimlane. Can be null. + /// + [JsonProperty("name")] + public string Name { get; set; } - /// - /// If true, issues in this swimlane are considered to be resolved. Can be updated only for newly created value. - /// Read-only. - /// - [JsonProperty("isResolved")] - public bool IsResolved { get; set; } - } -} + /// + /// If true, issues in this swimlane are considered to be resolved. Can be updated only for newly created value. + /// Read-only. + /// + [JsonProperty("isResolved")] + public bool IsResolved { get; set; } + } +} \ No newline at end of file diff --git a/src/YouTrackSharp/Agiles/SwimlaneSettings.cs b/src/YouTrackSharp/Agiles/SwimlaneSettings.cs index 391a0eb..479bc7f 100644 --- a/src/YouTrackSharp/Agiles/SwimlaneSettings.cs +++ b/src/YouTrackSharp/Agiles/SwimlaneSettings.cs @@ -1,23 +1,25 @@ using Newtonsoft.Json; using YouTrackSharp.SerializationAttributes; -namespace YouTrackSharp.Agiles { - /// - /// Base entity for different swimlane settings - /// - [KnownType(typeof(AttributeBasedSwimlaneSettings))] - [KnownType(typeof(IssueBasedSwimlaneSettings))] - public class SwimlaneSettings { +namespace YouTrackSharp.Agiles +{ /// - /// Id of the SwimlaneSettings. + /// Base entity for different swimlane settings /// - [JsonProperty("id")] - public string Id { get; set; } + [KnownType(typeof(AttributeBasedSwimlaneSettings))] + [KnownType(typeof(IssueBasedSwimlaneSettings))] + public class SwimlaneSettings + { + /// + /// Id of the SwimlaneSettings. + /// + [JsonProperty("id")] + public string Id { get; set; } - /// - /// Name of a value. Read-only. Can be null. - /// - [JsonProperty("name")] - public string Name { get; set; } - } -} + /// + /// Name of a value. Read-only. Can be null. + /// + [JsonProperty("name")] + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/src/YouTrackSharp/Agiles/SwimlaneValue.cs b/src/YouTrackSharp/Agiles/SwimlaneValue.cs index fab9436..5f2433a 100644 --- a/src/YouTrackSharp/Agiles/SwimlaneValue.cs +++ b/src/YouTrackSharp/Agiles/SwimlaneValue.cs @@ -1,20 +1,22 @@ using Newtonsoft.Json; -namespace YouTrackSharp.Agiles { - /// - /// Represents single swimlane in case of IssueBasedSwimlaneSettings. - /// - public class SwimlaneValue { +namespace YouTrackSharp.Agiles +{ /// - /// Id of the SwimlaneValue. + /// Represents single swimlane in case of IssueBasedSwimlaneSettings. /// - [JsonProperty("id")] - public string Id { get; set; } + public class SwimlaneValue + { + /// + /// Id of the SwimlaneValue. + /// + [JsonProperty("id")] + public string Id { get; set; } - /// - /// Name of a value. Read-only. Can be null. - /// - [JsonProperty("name")] - public string Name { get; set; } - } -} + /// + /// Name of a value. Read-only. Can be null. + /// + [JsonProperty("name")] + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/src/YouTrackSharp/Agiles/User.cs b/src/YouTrackSharp/Agiles/User.cs index 4f76d16..25f9951 100644 --- a/src/YouTrackSharp/Agiles/User.cs +++ b/src/YouTrackSharp/Agiles/User.cs @@ -1,28 +1,30 @@ using Newtonsoft.Json; -namespace YouTrackSharp.Agiles { - /// - /// Represents a user in the context of an agile board. The class only provides the id, ringId (hub ID) and full name - /// of the user, see for more info on how to access a YouTrack user. - /// - public class User { +namespace YouTrackSharp.Agiles +{ /// - /// Id of the User. + /// Represents a user in the context of an agile board. The class only provides the id, ringId (hub ID) and full name + /// of the user, see for more info on how to access a YouTrack user. /// - [JsonProperty("id")] - public string Id { get; set; } + public class User + { + /// + /// Id of the User. + /// + [JsonProperty("id")] + public string Id { get; set; } - /// - /// ID of the user in Hub. You can use this ID for operations in Hub, and for matching users between YouTrack and - /// Hub. Read-only. Can be null. - /// - [JsonProperty("ringId")] - public string RingId { get; set; } + /// + /// ID of the user in Hub. You can use this ID for operations in Hub, and for matching users between YouTrack and + /// Hub. Read-only. Can be null. + /// + [JsonProperty("ringId")] + public string RingId { get; set; } - /// - /// Full name of the user. - /// - [JsonProperty("fullName")] - public string FullName { get; set; } - } -} + /// + /// Full name of the user. + /// + [JsonProperty("fullName")] + public string FullName { get; set; } + } +} \ No newline at end of file diff --git a/src/YouTrackSharp/Agiles/UserGroup.cs b/src/YouTrackSharp/Agiles/UserGroup.cs index fa9d151..0c68e62 100644 --- a/src/YouTrackSharp/Agiles/UserGroup.cs +++ b/src/YouTrackSharp/Agiles/UserGroup.cs @@ -1,50 +1,52 @@ using Newtonsoft.Json; -namespace YouTrackSharp.Agiles { - /// - /// Represents a group of users. - /// - public class UserGroup { +namespace YouTrackSharp.Agiles +{ /// - /// Id of the UserGroup. + /// Represents a group of users. /// - [JsonProperty("id")] - public string Id { get; set; } - - /// - /// The name of the group. Read-only. Can be null. - /// - [JsonProperty("name")] - public string Name { get; set; } - - /// - /// ID of the group in Hub. Use this ID for operations in Hub, and for matching groups between YouTrack and Hub. Read-only. Can be null. - /// - [JsonProperty("ringId")] - public string RingId { get; set; } - - /// - /// The number of users in the group. Read-only. - /// - [JsonProperty("userCount")] - public long UserCount { get; set; } - - /// - /// The URL of the group icon. Read-only. Can be null. - /// - [JsonProperty("icon")] - public string Icon { get; set; } - - /// - /// True if this group contains all users, otherwise false. Read-only. - /// - [JsonProperty("allUsersGroup")] - public bool AllUsersGroup { get; set; } - - /// - /// Project that has this group set as a team. Returns null, if there is no such project. Read-only. Can be null. - /// - [JsonProperty("teamForProject")] - public Project TeamForProject { get; set; } - } + public class UserGroup + { + /// + /// Id of the UserGroup. + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// The name of the group. Read-only. Can be null. + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// ID of the group in Hub. Use this ID for operations in Hub, and for matching groups between YouTrack and Hub. Read-only. Can be null. + /// + [JsonProperty("ringId")] + public string RingId { get; set; } + + /// + /// The number of users in the group. Read-only. + /// + [JsonProperty("userCount")] + public long UserCount { get; set; } + + /// + /// The URL of the group icon. Read-only. Can be null. + /// + [JsonProperty("icon")] + public string Icon { get; set; } + + /// + /// True if this group contains all users, otherwise false. Read-only. + /// + [JsonProperty("allUsersGroup")] + public bool AllUsersGroup { get; set; } + + /// + /// Project that has this group set as a team. Returns null, if there is no such project. Read-only. Can be null. + /// + [JsonProperty("teamForProject")] + public Project TeamForProject { get; set; } + } } \ No newline at end of file diff --git a/src/YouTrackSharp/Agiles/WIPLimit.cs b/src/YouTrackSharp/Agiles/WIPLimit.cs index 4167c12..3d31b1b 100644 --- a/src/YouTrackSharp/Agiles/WIPLimit.cs +++ b/src/YouTrackSharp/Agiles/WIPLimit.cs @@ -1,26 +1,28 @@ using Newtonsoft.Json; -namespace YouTrackSharp.Agiles { - /// - /// Represents WIP limits for particular column. If they are not satisfied, the column will be highlighted in UI. - /// - public class WIPLimit { +namespace YouTrackSharp.Agiles +{ /// - /// Id of the WIPLimit. + /// Represents WIP limits for particular column. If they are not satisfied, the column will be highlighted in UI. /// - [JsonProperty("id")] - public string Id { get; set; } + public class WIPLimit + { + /// + /// Id of the WIPLimit. + /// + [JsonProperty("id")] + public string Id { get; set; } - /// - /// Maximum number of cards in column. Can be null. - /// - [JsonProperty("max")] - public int? Max { get; set; } + /// + /// Maximum number of cards in column. Can be null. + /// + [JsonProperty("max")] + public int? Max { get; set; } - /// - /// Minimum number of cards in column. Can be null. - /// - [JsonProperty("min")] - public int? Min { get; set; } - } -} + /// + /// Minimum number of cards in column. Can be null. + /// + [JsonProperty("min")] + public int? Min { get; set; } + } +} \ No newline at end of file diff --git a/src/YouTrackSharp/Internal/Field.cs b/src/YouTrackSharp/Internal/Field.cs index 4a5cf92..31557d0 100644 --- a/src/YouTrackSharp/Internal/Field.cs +++ b/src/YouTrackSharp/Internal/Field.cs @@ -1,36 +1,39 @@ using System.Collections.Generic; using System.Linq; -namespace YouTrackSharp.Internal { - /// - /// Simplified representation of an object's field. - /// It describes the name of the field and its sub-fields. - /// The 'subfields' are the members of the this field's type
- ///
- /// - /// This class allows to encode the recursive structure of an object's fields. - /// An object's field has a type, which itself has other members, each being of a type, and so on, down to the - /// primitive types.
- ///
- public class Field { +namespace YouTrackSharp.Internal +{ /// - /// Name of the object's field + /// Simplified representation of an object's field. + /// It describes the name of the field and its sub-fields. + /// The 'subfields' are the members of the this field's type
///
- public string Name { get; } - - /// - /// Sub-fields of this field's type - /// - public List Subfields { get; } + /// + /// This class allows to encode the recursive structure of an object's fields. + /// An object's field has a type, which itself has other members, each being of a type, and so on, down to the + /// primitive types.
+ ///
+ public class Field + { + /// + /// Name of the object's field + /// + public string Name { get; } - /// - /// Creates an instance of , with the given name, type and subfields. - /// - /// Name of the field - /// Sub-fields of the field's type - public Field(string name, IEnumerable subfields) { - Name = name; - Subfields = subfields.ToList(); + /// + /// Sub-fields of this field's type + /// + public List Subfields { get; } + + /// + /// Creates an instance of , with the given name, type and subfields. + /// + /// Name of the field + /// Sub-fields of the field's type + public Field(string name, IEnumerable subfields) + { + Name = name; + Subfields = subfields.ToList(); + } } - } } \ No newline at end of file diff --git a/src/YouTrackSharp/Internal/FieldSyntaxEncoder.cs b/src/YouTrackSharp/Internal/FieldSyntaxEncoder.cs index 5559b63..53d210f 100644 --- a/src/YouTrackSharp/Internal/FieldSyntaxEncoder.cs +++ b/src/YouTrackSharp/Internal/FieldSyntaxEncoder.cs @@ -5,224 +5,252 @@ using Newtonsoft.Json; using YouTrackSharp.SerializationAttributes; -namespace YouTrackSharp.Internal { - /// - /// Used to convert a given to the Youtrack REST API's 'fields' syntax, described at the following - /// location: - /// https://www.jetbrains.com/help/youtrack/standalone/api-fields-syntax.html - /// . - /// - public class FieldSyntaxEncoder { +namespace YouTrackSharp.Internal +{ /// - /// Cached used to store types that were already resolved. + /// Used to convert a given to the Youtrack REST API's 'fields' syntax, described at the following + /// location: + /// https://www.jetbrains.com/help/youtrack/standalone/api-fields-syntax.html + /// . /// - /// - /// The result of the field query can vary depending on the verbose option and the max nesting level. - /// Therefore, the key used is a combination of the , verbosity and max depth. - /// - private Dictionary, string> Cache { get; } + public class FieldSyntaxEncoder + { + /// + /// Cached used to store types that were already resolved. + /// + /// + /// The result of the field query can vary depending on the verbose option and the max nesting level. + /// Therefore, the key used is a combination of the , verbosity and max depth. + /// + private Dictionary, string> Cache { get; } - /// - /// Creates an instance of - /// - public FieldSyntaxEncoder() { - Cache = new Dictionary, string>(); - } + /// + /// Creates an instance of + /// + public FieldSyntaxEncoder() + { + Cache = new Dictionary, string>(); + } - /// - /// Encodes the given 's members to the Youtrack REST API's 'fields'-syntax, described at - /// - /// https://www.jetbrains.com/help/youtrack/standalone/api-fields-syntax.html - /// .
- /// This method uses reflexion to retrieve and encodes all public writable properties, decorated with a - /// .
- /// It also concatenates the fields retrieved from known subclasses of the given type (defined by the - /// ).
- /// Because the base class and the different subclasses may have fields with similar names (but different types and - /// sub-fields), all the fields discovered this way are "factored" together. Therefore, if two known subclasses both - /// have a field with the same name, but each with different types and so, different sub-fields, the two fields will - /// be factored into one containing the union of their respective sub-fields.
- /// For example:
- /// - The first subclass has a field named A with subfields B, C and D, ie. A(B,C,D)
- /// - The second subclass also has a field A but with subfields D,E and F, ie. A(D,E,F)
- /// In that case, these two fields will be combined into one, with name A and subfields B, C, D, E and F, ie. - /// A(B,C,D,E,F). Note that D appears only once. - ///
- /// Type to encode - /// - /// Verbosity. If true, will include all public writable properties of the given , marked - /// with the . - /// But if false, it will exclude from these all the ones that were marked with the - /// . - /// - /// Max sub-field depth to go from the given type. - /// Fields of the given type, encoded to Youtrack - /// REST API's 'field' syntax - /// - /// - /// - /// This method does not work well with types that have (direct or indirect) reference loops. - /// For example, a class A that contains two fields of type B and C, where the type B itself contains a field of - /// type A. This would result in a stack-overflow. - /// - public string Encode(Type type, bool verbose = true, int maxDepth = Int32.MaxValue) { - type = GetLeafType(type); - - Tuple key = new Tuple(type, verbose, maxDepth); - - if (Cache.ContainsKey(key)) return Cache[key]; - - IEnumerable fields = GetFields(type, verbose, maxDepth, 0); - - Cache[key] = string.Join(",", fields.Select(ToString)); - - return Cache[key]; - } + /// + /// Encodes the given 's members to the Youtrack REST API's 'fields'-syntax, described at + /// + /// https://www.jetbrains.com/help/youtrack/standalone/api-fields-syntax.html + /// .
+ /// This method uses reflexion to retrieve and encodes all public writable properties, decorated with a + /// .
+ /// It also concatenates the fields retrieved from known subclasses of the given type (defined by the + /// ).
+ /// Because the base class and the different subclasses may have fields with similar names (but different types and + /// sub-fields), all the fields discovered this way are "factored" together. Therefore, if two known subclasses both + /// have a field with the same name, but each with different types and so, different sub-fields, the two fields will + /// be factored into one containing the union of their respective sub-fields.
+ /// For example:
+ /// - The first subclass has a field named A with subfields B, C and D, ie. A(B,C,D)
+ /// - The second subclass also has a field A but with subfields D,E and F, ie. A(D,E,F)
+ /// In that case, these two fields will be combined into one, with name A and subfields B, C, D, E and F, ie. + /// A(B,C,D,E,F). Note that D appears only once. + ///
+ /// Type to encode + /// + /// Verbosity. If true, will include all public writable properties of the given , marked + /// with the . + /// But if false, it will exclude from these all the ones that were marked with the + /// . + /// + /// Max sub-field depth to go from the given type. + /// Fields of the given type, encoded to Youtrack + /// REST API's 'field' syntax + /// + /// + /// + /// This method does not work well with types that have (direct or indirect) reference loops. + /// For example, a class A that contains two fields of type B and C, where the type B itself contains a field of + /// type A. This would result in a stack-overflow. + /// + public string Encode(Type type, bool verbose = true, int maxDepth = Int32.MaxValue) + { + type = GetLeafType(type); - /// - /// Converts the recursive structure into a string representation, following Youtrack REST API's - /// 'field' syntax - /// Example: lines(fromPoint(x,y),toPoint(x,y)),color - /// - /// to convert - /// - private string ToString(Field field) { - if (!field.Subfields.Any()) return field.Name; + Tuple key = new Tuple(type, verbose, maxDepth); - string subfields = string.Join(",", field.Subfields.Select(ToString)); + if (Cache.ContainsKey(key)) + { + return Cache[key]; + } - return $"{field.Name}({subfields})"; - } + IEnumerable fields = GetFields(type, verbose, maxDepth, 0); - /// - /// Recursive method which builds the structure for the given 's properties. - /// This method uses reflexion to convert all the public writable properties of teh given , - /// decorated with a to a
. - /// It also retrieves the fields from known subclasses of the given type (defined by the - /// ).
- /// Because the base class and the different subclasses may have fields with similar names (but different types and - /// sub-fields), all the fields discovered this way are "factored" together. Therefore, if two known subclasses both - /// have a field with the same name, but different type, each with different sub-fields, the two fields will - /// be factored into one containing the union of their respective sub-fields.
- /// For example:
- /// - The first subclass has a field named A with subfields B, C and D, ie. A(B,C,D)
- /// - The second subclass also has a field A but with subfields D,E and F, ie. A(D,E,F)
- /// In that case, these two fields will be combined into one, with name A and subfields B, C, D, E and F, ie. - /// A(B,C,D,E,F). Note that D appears only once. - ///
- /// Type to convert - /// - /// Verbosity. If true, will include all public writable properties of the given , marked - /// with the . - /// But if false, it will exclude from these all the ones that were marked with the - /// . - /// - /// Max sub-field depth to go from the given type. - /// Current sub-field level (to compare with ) - /// Fields of the given type, encoded to Youtrack - /// REST API's 'field' syntax - /// - /// - /// - /// This method does not work well with types that have (direct or indirect) reference loops. - /// For example, a class A that contains two fields of type B and C, where the type B itself contains a field of - /// type A. This would result in a stack-overflow. - /// - private IEnumerable GetFields(Type type, bool verbose, int maxDepth, int level) { - if (level == maxDepth) return Enumerable.Empty(); - - List fields = new List(); - - IEnumerable properties = GetProperties(type, verbose); - foreach (PropertyInfo propertyInfo in properties) { - Type propertyType = GetLeafType(propertyInfo.PropertyType); - string propertyName = propertyInfo.GetCustomAttribute()?.PropertyName; - - IEnumerable subfields = GetFields(propertyType, verbose, maxDepth, level + 1); - - fields.Add(new Field(propertyName, subfields)); - } - - return Merge(fields); - } + Cache[key] = string.Join(",", fields.Select(ToString)); - /// - /// Merges the given fields into a factored representation, where fields with the same name are combined into one. - /// For example, if there are two fields named "A"
- /// - The first with subfields B, C and D, ie. A(B,C,D)
- /// - The second with subfields D,E and F, ie. A(D,E,F)
- /// These two fields will be combined into one, with name A and subfields B, C, D, E and F, ie. - /// A(B,C,D,E,F). Note that D appears only once. - ///
- /// s to factor - /// Factored fields - private IEnumerable Merge(IEnumerable fields) { - IEnumerable> groups = fields.GroupBy(field => field.Name); + return Cache[key]; + } - foreach (IGrouping group in groups) { - IEnumerable subfields = group.SelectMany(f => f.Subfields); - subfields = Merge(subfields); + /// + /// Converts the recursive structure into a string representation, following Youtrack REST API's + /// 'field' syntax + /// Example: lines(fromPoint(x,y),toPoint(x,y)),color + /// + /// to convert + /// + private string ToString(Field field) + { + if (!field.Subfields.Any()) + { + return field.Name; + } - yield return new Field(group.Key, subfields); - } - } + string subfields = string.Join(",", field.Subfields.Select(ToString)); - /// - /// Retrieves the of all public, writable properties marked with a - /// of the given and - /// all of its known subclasses (defined by the . - /// If is set to false, it will exclude the ones marked with a - /// . - /// - /// from which to retrieve the properties - /// - /// If false the properties marked with will be excluded from the results. - /// - /// - /// The of all public, writable properties marked with a - /// of the given and - /// all of its known subclasses. - /// - private IEnumerable GetProperties(Type type, bool verbose) { - PropertyInfo[] properties = type.GetProperties(); - - IEnumerable propertyInfos = properties.Where(p => p.CanWrite) - .Where(p => p.GetCustomAttribute() != null); - - if (!verbose) { - propertyInfos = propertyInfos.Where(p => p.GetCustomAttribute() == null); - } - - IEnumerable knownSubtypes = type.GetCustomAttributes(false).Select(attr => attr.Type); - IEnumerable subtypesProperties = - knownSubtypes.SelectMany(subtype => GetProperties(subtype, verbose)); - - return propertyInfos.Concat(subtypesProperties); - } + return $"{field.Name}({subfields})"; + } - /// - /// Returns the "leaf" type argument of any generic type.
- /// Note: Generic types are nested, the "leaf" meaning the non-generic type at the bottom of the nesting chain - ///
- /// For example, a of , will yield . - /// And a of of - /// will also yield .
- ///
- /// For generic types with multiple type parameters, it will always follow the last, which is usually the value type. - /// For example, a of key and value , - /// will yield .
- /// A of key and value of - /// , will also yield . - ///
- /// Input - /// Leaf type if the input type is generic, otherwise itself - /// - /// The returned type is guaranteed to be non-generic. - /// - private Type GetLeafType(Type type) { - if (!type.IsGenericType) return type; - - return GetLeafType(type.GetGenericArguments().Last()); + /// + /// Recursive method which builds the structure for the given 's properties. + /// This method uses reflexion to convert all the public writable properties of teh given , + /// decorated with a to a
. + /// It also retrieves the fields from known subclasses of the given type (defined by the + /// ).
+ /// Because the base class and the different subclasses may have fields with similar names (but different types and + /// sub-fields), all the fields discovered this way are "factored" together. Therefore, if two known subclasses both + /// have a field with the same name, but different type, each with different sub-fields, the two fields will + /// be factored into one containing the union of their respective sub-fields.
+ /// For example:
+ /// - The first subclass has a field named A with subfields B, C and D, ie. A(B,C,D)
+ /// - The second subclass also has a field A but with subfields D,E and F, ie. A(D,E,F)
+ /// In that case, these two fields will be combined into one, with name A and subfields B, C, D, E and F, ie. + /// A(B,C,D,E,F). Note that D appears only once. + ///
+ /// Type to convert + /// + /// Verbosity. If true, will include all public writable properties of the given , marked + /// with the . + /// But if false, it will exclude from these all the ones that were marked with the + /// . + /// + /// Max sub-field depth to go from the given type. + /// Current sub-field level (to compare with ) + /// Fields of the given type, encoded to Youtrack + /// REST API's 'field' syntax + /// + /// + /// + /// This method does not work well with types that have (direct or indirect) reference loops. + /// For example, a class A that contains two fields of type B and C, where the type B itself contains a field of + /// type A. This would result in a stack-overflow. + /// + private IEnumerable GetFields(Type type, bool verbose, int maxDepth, int level) + { + if (level == maxDepth) + { + return Enumerable.Empty(); + } + + List fields = new List(); + + IEnumerable properties = GetProperties(type, verbose); + foreach (PropertyInfo propertyInfo in properties) + { + Type propertyType = GetLeafType(propertyInfo.PropertyType); + string propertyName = propertyInfo.GetCustomAttribute()?.PropertyName; + + IEnumerable subfields = GetFields(propertyType, verbose, maxDepth, level + 1); + + fields.Add(new Field(propertyName, subfields)); + } + + return Merge(fields); + } + + /// + /// Merges the given fields into a factored representation, where fields with the same name are combined into one. + /// For example, if there are two fields named "A"
+ /// - The first with subfields B, C and D, ie. A(B,C,D)
+ /// - The second with subfields D,E and F, ie. A(D,E,F)
+ /// These two fields will be combined into one, with name A and subfields B, C, D, E and F, ie. + /// A(B,C,D,E,F). Note that D appears only once. + ///
+ /// s to factor + /// Factored fields + private IEnumerable Merge(IEnumerable fields) + { + IEnumerable> groups = fields.GroupBy(field => field.Name); + + foreach (IGrouping group in groups) + { + IEnumerable subfields = group.SelectMany(f => f.Subfields); + subfields = Merge(subfields); + + yield return new Field(group.Key, subfields); + } + } + + /// + /// Retrieves the of all public, writable properties marked with a + /// of the given and + /// all of its known subclasses (defined by the . + /// If is set to false, it will exclude the ones marked with a + /// . + /// + /// from which to retrieve the properties + /// + /// If false the properties marked with will be excluded from the results. + /// + /// + /// The of all public, writable properties marked with a + /// of the given and + /// all of its known subclasses. + /// + private IEnumerable GetProperties(Type type, bool verbose) + { + PropertyInfo[] properties = type.GetProperties(); + + IEnumerable propertyInfos = properties.Where(p => p.CanWrite) + .Where( + p => + p.GetCustomAttribute() != + null); + + if (!verbose) + { + propertyInfos = propertyInfos.Where(p => p.GetCustomAttribute() == null); + } + + IEnumerable knownSubtypes = + type.GetCustomAttributes(false).Select(attr => attr.Type); + IEnumerable subtypesProperties = + knownSubtypes.SelectMany(subtype => GetProperties(subtype, verbose)); + + return propertyInfos.Concat(subtypesProperties); + } + + /// + /// Returns the "leaf" type argument of any generic type.
+ /// Note: Generic types are nested, the "leaf" meaning the non-generic type at the bottom of the nesting chain + ///
+ /// For example, a of , will yield . + /// And a of of + /// will also yield .
+ ///
+ /// For generic types with multiple type parameters, it will always follow the last, which is usually the value type. + /// For example, a of key and value , + /// will yield .
+ /// A of key and value of + /// , will also yield . + ///
+ /// Input + /// Leaf type if the input type is generic, otherwise itself + /// + /// The returned type is guaranteed to be non-generic. + /// + private Type GetLeafType(Type type) + { + if (!type.IsGenericType) + { + return type; + } + + return GetLeafType(type.GetGenericArguments().Last()); + } } - } } \ No newline at end of file diff --git a/src/YouTrackSharp/Json/KnownTypeConverter.cs b/src/YouTrackSharp/Json/KnownTypeConverter.cs index 8183156..5bbb793 100644 --- a/src/YouTrackSharp/Json/KnownTypeConverter.cs +++ b/src/YouTrackSharp/Json/KnownTypeConverter.cs @@ -6,57 +6,66 @@ using Newtonsoft.Json.Linq; using YouTrackSharp.SerializationAttributes; -namespace YouTrackSharp.Json { - /// - /// This allows to convert a JSON string into a concrete object that extends a given base - /// type.
- /// This is used in conjunction with the , defined on - /// the base class, and which lists the possible sub-types of that class.

- /// Among these, the concrete sub-type instantiated is inferred from the Json object's "$type" field, which is - /// compared to the defined known types (ignoring their namespace).
- /// If the "$type" field is undefined, or no matching the "$type" parameter is - /// found, the base class is instantiated instead. - ///
- /// Base type - public class KnownTypeConverter : JsonConverter where T:new() { - private readonly TypedJObjectConverter _objectConverter; - - public KnownTypeConverter() { - _objectConverter = new TypedJObjectConverter(); - } - - /// - public override void WriteJson(JsonWriter writer, T value, JsonSerializer serializer) { - throw new NotImplementedException(); - } - +namespace YouTrackSharp.Json +{ /// - /// Reads the JSON representation of the object.
- /// This method will instanciate a subclass of the given , based on the "$type" parameter - /// of the json string, which is compared to defined for that base class.
- /// If the "$type" parameter is not defined, or no matching is found, the json is - /// deserialized to an instance of the base class directly. + /// This allows to convert a JSON string into a concrete object that extends a given base + /// type.
+ /// This is used in conjunction with the , defined on + /// the base class, and which lists the possible sub-types of that class.

+ /// Among these, the concrete sub-type instantiated is inferred from the Json object's "$type" field, which is + /// compared to the defined known types (ignoring their namespace).
+ /// If the "$type" field is undefined, or no matching the "$type" parameter is + /// found, the base class is instantiated instead. ///
- /// The to read from. - /// Type of the object. - /// The existing value of object being read. If there is no existing value then null will be used. - /// The existing value has a value. - /// The calling serializer. - /// The object value. - public override T ReadJson(JsonReader reader, Type objectType, T existingValue, bool hasExistingValue, JsonSerializer serializer) { - if (reader.TokenType == JsonToken.Null) return default; - - List types = typeof(T).GetCustomAttributes().Select(attr => attr.Type).ToList(); - - JObject obj = JObject.Load(reader); - - return _objectConverter.ReadObject(obj, types, serializer); + /// Base type + public class KnownTypeConverter : JsonConverter where T : new() + { + private readonly TypedJObjectConverter _objectConverter; + + /// + public override bool CanRead => true; + + /// + public override bool CanWrite => false; + + public KnownTypeConverter() + { + _objectConverter = new TypedJObjectConverter(); + } + + /// + public override void WriteJson(JsonWriter writer, T value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + + /// + /// Reads the JSON representation of the object.
+ /// This method will instanciate a subclass of the given , based on the "$type" parameter + /// of the json string, which is compared to defined for that base class.
+ /// If the "$type" parameter is not defined, or no matching is found, the json is + /// deserialized to an instance of the base class directly. + ///
+ /// The to read from. + /// Type of the object. + /// The existing value of object being read. If there is no existing value then null will be used. + /// The existing value has a value. + /// The calling serializer. + /// The object value. + public override T ReadJson(JsonReader reader, Type objectType, T existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + { + return default; + } + + List types = typeof(T).GetCustomAttributes().Select(attr => attr.Type).ToList(); + + JObject obj = JObject.Load(reader); + + return _objectConverter.ReadObject(obj, types, serializer); + } } - - /// - public override bool CanRead => true; - - /// - public override bool CanWrite => false; - } } \ No newline at end of file diff --git a/src/YouTrackSharp/Json/KnownTypeListConverter.cs b/src/YouTrackSharp/Json/KnownTypeListConverter.cs index 375bad2..0d77324 100644 --- a/src/YouTrackSharp/Json/KnownTypeListConverter.cs +++ b/src/YouTrackSharp/Json/KnownTypeListConverter.cs @@ -6,56 +6,67 @@ using Newtonsoft.Json.Linq; using YouTrackSharp.SerializationAttributes; -namespace YouTrackSharp.Json { - /// - /// This class allows to convert a JSON array into a list of objects, each extending a given base type.
- /// This is used in conjunction with the , defined on - /// the base class, and which lists the possible sub-types of that class.

- /// Among these, the concrete sub-type instantiated is inferred from each Json object's "$type" field, which is - /// compared to the defined known types (ignoring their namespace).
- /// If the "$type" field is undefined, or no matching the "$type" parameter is - /// found, the base class is instantiated instead. - ///
- /// Base type - public class KnownTypeListConverter : JsonConverter> where T:new() { - private readonly TypedJObjectConverter _objectConverter; - - public KnownTypeListConverter() { - _objectConverter = new TypedJObjectConverter(); - } - - /// - public override void WriteJson(JsonWriter writer, List value, JsonSerializer serializer) { - throw new NotImplementedException(); - } - +namespace YouTrackSharp.Json +{ /// - /// Reads the JSON representation of the object.
- /// This method will instanciate a subclass of the given , based on the "$type" parameter - /// of the json string, which is compared to defined for that base class.
- /// If the "$type" parameter is not defined, or no matching is found, the json is - /// deserialized to an instance of the base class directly. + /// This class allows to convert a JSON array into a list of objects, each extending a given base type.
+ /// This is used in conjunction with the , defined on + /// the base class, and which lists the possible sub-types of that class.

+ /// Among these, the concrete sub-type instantiated is inferred from each Json object's "$type" field, which is + /// compared to the defined known types (ignoring their namespace).
+ /// If the "$type" field is undefined, or no matching the "$type" parameter is + /// found, the base class is instantiated instead. ///
- /// The to read from. - /// Type of the object. - /// The existing value of object being read. If there is no existing value then null will be used. - /// The existing value has a value. - /// The calling serializer. - /// The object value. - public override List ReadJson(JsonReader reader, Type objectType, List existingValue, bool hasExistingValue, JsonSerializer serializer) { - if (reader.TokenType == JsonToken.Null) return null; - - List knownTypes = typeof(T).GetCustomAttributes().Select(attr => attr.Type).ToList(); - - JArray array = JArray.Load(reader); - - return array.Select(token => _objectConverter.ReadObject(token as JObject, knownTypes, serializer)).ToList(); - } + /// Base type + public class KnownTypeListConverter : JsonConverter> where T : new() + { + private readonly TypedJObjectConverter _objectConverter; + + /// + public override bool CanRead => true; + + /// + public override bool CanWrite => false; - /// - public override bool CanRead => true; - - /// - public override bool CanWrite => false; - } + public KnownTypeListConverter() + { + _objectConverter = new TypedJObjectConverter(); + } + + /// + public override void WriteJson(JsonWriter writer, List value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + + /// + /// Reads the JSON representation of the object.
+ /// This method will instanciate a subclass of the given , based on the "$type" parameter + /// of the json string, which is compared to defined for that base class.
+ /// If the "$type" parameter is not defined, or no matching is found, the json is + /// deserialized to an instance of the base class directly. + ///
+ /// The to read from. + /// Type of the object. + /// The existing value of object being read. If there is no existing value then null will be used. + /// The existing value has a value. + /// The calling serializer. + /// The object value. + public override List ReadJson(JsonReader reader, Type objectType, List existingValue, + bool hasExistingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + { + return null; + } + + List knownTypes = + typeof(T).GetCustomAttributes().Select(attr => attr.Type).ToList(); + + JArray array = JArray.Load(reader); + + return array.Select(token => _objectConverter.ReadObject(token as JObject, knownTypes, serializer)) + .ToList(); + } + } } \ No newline at end of file diff --git a/src/YouTrackSharp/Json/TypedJObjectConverter.cs b/src/YouTrackSharp/Json/TypedJObjectConverter.cs index 5a56f59..e66d75d 100644 --- a/src/YouTrackSharp/Json/TypedJObjectConverter.cs +++ b/src/YouTrackSharp/Json/TypedJObjectConverter.cs @@ -4,23 +4,31 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace YouTrackSharp.Json { - public class TypedJObjectConverter { - public T ReadObject(JObject token, List knownTypes, JsonSerializer serializer) { - if (token == null) throw new ArgumentException("Invalid token"); +namespace YouTrackSharp.Json +{ + public class TypedJObjectConverter + { + public T ReadObject(JObject token, List knownTypes, JsonSerializer serializer) + { + if (token == null) + { + throw new ArgumentException("Invalid token"); + } - string jsonType = token["$type"]?.ToString(); - if (jsonType == null) { - return token.ToObject(serializer); - } + string jsonType = token["$type"]?.ToString(); + if (jsonType == null) + { + return token.ToObject(serializer); + } - Type type = knownTypes.FirstOrDefault(t => t.Name.EndsWith(jsonType)); + Type type = knownTypes.FirstOrDefault(t => t.Name.EndsWith(jsonType)); - if (type == null) { - return token.ToObject(serializer); - } + if (type == null) + { + return token.ToObject(serializer); + } - return (T)token.ToObject(type, serializer); + return (T)token.ToObject(type, serializer); + } } - } } \ No newline at end of file diff --git a/src/YouTrackSharp/SerializationAttributes/KnownTypeAttribute.cs b/src/YouTrackSharp/SerializationAttributes/KnownTypeAttribute.cs index dd3362f..317b51a 100644 --- a/src/YouTrackSharp/SerializationAttributes/KnownTypeAttribute.cs +++ b/src/YouTrackSharp/SerializationAttributes/KnownTypeAttribute.cs @@ -1,24 +1,27 @@ using System; -namespace YouTrackSharp.SerializationAttributes { - /// - /// Attribute used to decorate a parent class with its known subtypes.
- /// This is used to identify candidate subclasses when generating REST requests (to include the subclasses' - /// properties). - ///
- [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] - public class KnownTypeAttribute : Attribute { +namespace YouTrackSharp.SerializationAttributes +{ /// - /// Known subclass's type + /// Attribute used to decorate a parent class with its known subtypes.
+ /// This is used to identify candidate subclasses when generating REST requests (to include the subclasses' + /// properties). ///
- public Type Type { get; } + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public class KnownTypeAttribute : Attribute + { + /// + /// Known subclass's type + /// + public Type Type { get; } - /// - /// Creates an instance of the , with the given subclass' type. - /// - /// - public KnownTypeAttribute(Type type) { - Type = type; + /// + /// Creates an instance of the , with the given subclass' type. + /// + /// + public KnownTypeAttribute(Type type) + { + Type = type; + } } - } } \ No newline at end of file diff --git a/src/YouTrackSharp/SerializationAttributes/VerboseAttribute.cs b/src/YouTrackSharp/SerializationAttributes/VerboseAttribute.cs index 6378eac..80b9432 100644 --- a/src/YouTrackSharp/SerializationAttributes/VerboseAttribute.cs +++ b/src/YouTrackSharp/SerializationAttributes/VerboseAttribute.cs @@ -1,9 +1,11 @@ using System; -namespace YouTrackSharp.SerializationAttributes { - /// - /// This attribute is used to mark a property as being only retrieved when verbose information was requested. - /// - [AttributeUsage(AttributeTargets.Property)] - public class VerboseAttribute : Attribute { } +namespace YouTrackSharp.SerializationAttributes +{ + /// + /// This attribute is used to mark a property as being only retrieved when verbose information was requested. + /// + [AttributeUsage(AttributeTargets.Property)] + public class VerboseAttribute : Attribute + { } } \ No newline at end of file diff --git a/tests/YouTrackSharp.Tests/Infrastructure/ConnectionStub.cs b/tests/YouTrackSharp.Tests/Infrastructure/ConnectionStub.cs index bd45a21..d231c97 100644 --- a/tests/YouTrackSharp.Tests/Infrastructure/ConnectionStub.cs +++ b/tests/YouTrackSharp.Tests/Infrastructure/ConnectionStub.cs @@ -1,63 +1,72 @@ using System; -using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; -namespace YouTrackSharp.Tests.Infrastructure { - /// - /// Connection mock used to return predefined HTTP responses, for testing purposes. - /// - public class ConnectionStub : Connection { - private Func ExecuteRequest { get; } - - private TimeSpan TimeOut => TimeSpan.FromSeconds(100); - - /// - /// Creates an instance of with give response delegate - /// - /// Request delegate - public ConnectionStub(Func executeRequest) : base("http://fake.connection.com/") { - ExecuteRequest = executeRequest; - } - +namespace YouTrackSharp.Tests.Infrastructure +{ /// - /// Creates an configured to return a predefined message and HTTP status - /// on request. + /// Connection mock used to return predefined HTTP responses, for testing purposes. /// - /// configured to return a predefined message and HTTP status - public override Task GetAuthenticatedHttpClient() { - return Task.FromResult(CreateClient()); - } + public class ConnectionStub : Connection + { + private Func ExecuteRequest { get; } - private HttpClient CreateClient() { - HttpClient httpClient = new HttpClient(new HttpClientHandlerStub(ExecuteRequest)); - httpClient.BaseAddress = ServerUri; - httpClient.Timeout = TimeOut; + private TimeSpan TimeOut => TimeSpan.FromSeconds(100); - return httpClient; + /// + /// Creates an instance of with give response delegate + /// + /// Request delegate + public ConnectionStub(Func executeRequest) : base( + "http://fake.connection.com/") + { + ExecuteRequest = executeRequest; + } + + /// + /// Creates an configured to return a predefined message and HTTP status + /// on request. + /// + /// configured to return a predefined message and HTTP status + public override Task GetAuthenticatedHttpClient() + { + return Task.FromResult(CreateClient()); + } + + private HttpClient CreateClient() + { + HttpClient httpClient = new HttpClient(new HttpClientHandlerStub(ExecuteRequest)); + httpClient.BaseAddress = ServerUri; + httpClient.Timeout = TimeOut; + + return httpClient; + } } - } - - /// - /// mock, that returns a predefined reply and HTTP status code. - /// - public class HttpClientHandlerStub : HttpClientHandler { - private Func ExecuteRequest { get; } - + /// - /// Creates an instance that delegates HttpRequestMessages + /// mock, that returns a predefined reply and HTTP status code. /// - /// Request delegate - public HttpClientHandlerStub(Func executeRequest) { - ExecuteRequest = executeRequest; - } + public class HttpClientHandlerStub : HttpClientHandler + { + private Func ExecuteRequest { get; } + + /// + /// Creates an instance that delegates HttpRequestMessages + /// + /// Request delegate + public HttpClientHandlerStub(Func executeRequest) + { + ExecuteRequest = executeRequest; + } - /// - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - HttpResponseMessage reply = ExecuteRequest?.Invoke(request); + /// + protected override Task SendAsync(HttpRequestMessage request, + CancellationToken cancellationToken) + { + HttpResponseMessage reply = ExecuteRequest?.Invoke(request); - return Task.FromResult(reply); + return Task.FromResult(reply); + } } - } } \ No newline at end of file diff --git a/tests/YouTrackSharp.Tests/Infrastructure/Connections.cs b/tests/YouTrackSharp.Tests/Infrastructure/Connections.cs index 8c56fb6..121d39c 100644 --- a/tests/YouTrackSharp.Tests/Infrastructure/Connections.cs +++ b/tests/YouTrackSharp.Tests/Infrastructure/Connections.cs @@ -24,7 +24,8 @@ public static string ServerUrl public static Connection Demo3Token => new BearerTokenConnection(ServerUrl, "perm:ZGVtbzM=.WW91VHJhY2tTaGFycA==.L04RdcCnjyW2UPCVg1qyb6dQflpzFy", ConfigureTestsHandler); - public static Connection ConnectionStub(string content, HttpStatusCode status = HttpStatusCode.OK) { + public static Connection ConnectionStub(string content, HttpStatusCode status = HttpStatusCode.OK) + { HttpResponseMessage response = new HttpResponseMessage(status); response.Content = new StringContent(content); diff --git a/tests/YouTrackSharp.Tests/Integration/Agiles/AgileServiceTest.cs b/tests/YouTrackSharp.Tests/Integration/Agiles/AgileServiceTest.cs index 5c0d589..2f2f61b 100644 --- a/tests/YouTrackSharp.Tests/Integration/Agiles/AgileServiceTest.cs +++ b/tests/YouTrackSharp.Tests/Integration/Agiles/AgileServiceTest.cs @@ -1,28 +1,31 @@ using System.IO; -using System.Net; -using System.Net.Http; using System.Reflection; -using YouTrackSharp.Tests.Infrastructure; -namespace YouTrackSharp.Tests.Integration.Agiles { - public partial class AgileServiceTest { - private static string DemoBoardId => "108-2"; - private static string DemoBoardNamePrefix => "Test Board597fb561-ea1f-4095-9636-859ae4439605"; +namespace YouTrackSharp.Tests.Integration.Agiles +{ + public partial class AgileServiceTest + { + private static string DemoBoardId => "108-2"; + private static string DemoBoardNamePrefix => "Test Board597fb561-ea1f-4095-9636-859ae4439605"; - private static string DemoSprintId => "109-2"; - private static string DemoSprintName => "First sprint"; + private static string DemoSprintId => "109-2"; + private static string DemoSprintName => "First sprint"; - private static string CompleteAgileJson => GetTextResource("YouTrackSharp.Tests.Resources.CompleteAgile.json"); + private static string CompleteAgileJson => GetTextResource("YouTrackSharp.Tests.Resources.CompleteAgile.json"); - private static string GetTextResource(string name) { - Assembly assembly = Assembly.GetExecutingAssembly(); + private static string GetTextResource(string name) + { + Assembly assembly = Assembly.GetExecutingAssembly(); - using Stream stream = assembly.GetManifestResourceStream(name); - if (stream == null) return string.Empty; - - using StreamReader reader = new StreamReader(stream); - - return reader.ReadToEnd(); + using Stream stream = assembly.GetManifestResourceStream(name); + if (stream == null) + { + return string.Empty; + } + + using StreamReader reader = new StreamReader(stream); + + return reader.ReadToEnd(); + } } - } } \ No newline at end of file diff --git a/tests/YouTrackSharp.Tests/Integration/Agiles/GetAgiles.cs b/tests/YouTrackSharp.Tests/Integration/Agiles/GetAgiles.cs index 45485fd..b540ee7 100644 --- a/tests/YouTrackSharp.Tests/Integration/Agiles/GetAgiles.cs +++ b/tests/YouTrackSharp.Tests/Integration/Agiles/GetAgiles.cs @@ -6,119 +6,127 @@ using YouTrackSharp.Agiles; using YouTrackSharp.Tests.Infrastructure; -namespace YouTrackSharp.Tests.Integration.Agiles { - [UsedImplicitly] - public partial class AgileServiceTest { - public class GetAgiles { - [Fact] - public async Task Valid_Connection_Return_Existing_Agile_Verbose() { - // Arrange - IAgileService agileService = Connections.Demo1Token.CreateAgileService(); - - // Act - ICollection result = await agileService.GetAgileBoards(true); - - // Assert - Assert.NotNull(result); - Assert.NotEmpty(result); - - Agile demoBoard = result.FirstOrDefault(); - Assert.NotNull(demoBoard); - Assert.Equal(DemoBoardId, demoBoard.Id); - Assert.Equal(DemoBoardNamePrefix, demoBoard.Name); - - Assert.NotNull(demoBoard.ColumnSettings); - Assert.NotNull(demoBoard.Projects); - Assert.NotNull(demoBoard.Sprints); - Assert.NotNull(demoBoard.Projects); - Assert.NotNull(demoBoard.Sprints); - Assert.NotNull(demoBoard.Status); - - Assert.NotNull(demoBoard.ColumnSettings); - Assert.NotNull(demoBoard.CurrentSprint); - Assert.NotNull(demoBoard.EstimationField); - Assert.NotNull(demoBoard.SprintsSettings); - Assert.NotNull(demoBoard.SwimlaneSettings); - - - // These fields are null in the agile test - Assert.Null(demoBoard.ColorCoding); - Assert.Null(demoBoard.UpdateableBy); - Assert.Null(demoBoard.VisibleFor); - Assert.Null(demoBoard.OriginalEstimationField); - - Sprint sprint = demoBoard.Sprints.FirstOrDefault(); - Assert.NotNull(sprint); - Assert.Equal(DemoSprintId, sprint.Id); - Assert.Equal(DemoSprintName, sprint.Name); - } - - [Fact] - public async Task Verbose_Disabled_Returns_Agile_Minimum_Info() { - // Arrange - IAgileService agileService = Connections.Demo1Token.CreateAgileService(); - - // Act - ICollection result = await agileService.GetAgileBoards(false); - - // Assert - Assert.NotNull(result); - Assert.NotEmpty(result); - - Agile demoBoard = result.FirstOrDefault(); - Assert.NotNull(demoBoard); - - Assert.Equal(DemoBoardId, demoBoard.Id); - Assert.Equal(DemoBoardNamePrefix, demoBoard.Name); - } - - [Fact] - public async Task Invalid_Connection_Throws_UnauthorizedConnectionException() { - // Arrange - IAgileService agileService = Connections.UnauthorizedConnection.CreateAgileService(); - - // Act & Assert - await Assert.ThrowsAsync(async () => await agileService.GetAgileBoards()); - } - - [Fact] - public async Task Full_Agile_Json_Gets_Deserialized_Successfully() { - // Arrange - IAgileService agileService = Connections.ConnectionStub(CompleteAgileJson).CreateAgileService(); - - // Act - ICollection result = await agileService.GetAgileBoards(true); - - // Assert - Assert.NotNull(result); - Assert.NotEmpty(result); - - Agile demoBoard = result.FirstOrDefault(); - Assert.NotNull(demoBoard); - Assert.Equal(DemoBoardId, demoBoard.Id); - Assert.Equal(DemoBoardNamePrefix, demoBoard.Name); - - Assert.NotNull(demoBoard.ColumnSettings); - Assert.NotNull(demoBoard.Projects); - Assert.NotNull(demoBoard.Sprints); - Assert.NotNull(demoBoard.Projects); - Assert.NotNull(demoBoard.Sprints); - Assert.NotNull(demoBoard.Status); - Assert.NotNull(demoBoard.ColumnSettings); - Assert.NotNull(demoBoard.CurrentSprint); - Assert.NotNull(demoBoard.EstimationField); - Assert.NotNull(demoBoard.SprintsSettings); - Assert.NotNull(demoBoard.SwimlaneSettings); - Assert.NotNull(demoBoard.ColorCoding); - Assert.NotNull(demoBoard.UpdateableBy); - Assert.NotNull(demoBoard.VisibleFor); - Assert.NotNull(demoBoard.OriginalEstimationField); - - Sprint sprint = demoBoard.Sprints.FirstOrDefault(); - Assert.NotNull(sprint); - Assert.Equal(DemoSprintId, sprint.Id); - Assert.Equal(DemoSprintName, sprint.Name); - } +namespace YouTrackSharp.Tests.Integration.Agiles +{ + [UsedImplicitly] + public partial class AgileServiceTest + { + public class GetAgiles + { + [Fact] + public async Task Valid_Connection_Return_Existing_Agile_Verbose() + { + // Arrange + IAgileService agileService = Connections.Demo1Token.CreateAgileService(); + + // Act + ICollection result = await agileService.GetAgileBoards(true); + + // Assert + Assert.NotNull(result); + Assert.NotEmpty(result); + + Agile demoBoard = result.FirstOrDefault(); + Assert.NotNull(demoBoard); + Assert.Equal(DemoBoardId, demoBoard.Id); + Assert.Equal(DemoBoardNamePrefix, demoBoard.Name); + + Assert.NotNull(demoBoard.ColumnSettings); + Assert.NotNull(demoBoard.Projects); + Assert.NotNull(demoBoard.Sprints); + Assert.NotNull(demoBoard.Projects); + Assert.NotNull(demoBoard.Sprints); + Assert.NotNull(demoBoard.Status); + + Assert.NotNull(demoBoard.ColumnSettings); + Assert.NotNull(demoBoard.CurrentSprint); + Assert.NotNull(demoBoard.EstimationField); + Assert.NotNull(demoBoard.SprintsSettings); + Assert.NotNull(demoBoard.SwimlaneSettings); + + + // These fields are null in the agile test + Assert.Null(demoBoard.ColorCoding); + Assert.Null(demoBoard.UpdateableBy); + Assert.Null(demoBoard.VisibleFor); + Assert.Null(demoBoard.OriginalEstimationField); + + Sprint sprint = demoBoard.Sprints.FirstOrDefault(); + Assert.NotNull(sprint); + Assert.Equal(DemoSprintId, sprint.Id); + Assert.Equal(DemoSprintName, sprint.Name); + } + + [Fact] + public async Task Verbose_Disabled_Returns_Agile_Minimum_Info() + { + // Arrange + IAgileService agileService = Connections.Demo1Token.CreateAgileService(); + + // Act + ICollection result = await agileService.GetAgileBoards(false); + + // Assert + Assert.NotNull(result); + Assert.NotEmpty(result); + + Agile demoBoard = result.FirstOrDefault(); + Assert.NotNull(demoBoard); + + Assert.Equal(DemoBoardId, demoBoard.Id); + Assert.Equal(DemoBoardNamePrefix, demoBoard.Name); + } + + [Fact] + public async Task Invalid_Connection_Throws_UnauthorizedConnectionException() + { + // Arrange + IAgileService agileService = Connections.UnauthorizedConnection.CreateAgileService(); + + // Act & Assert + await Assert.ThrowsAsync( + async () => await agileService.GetAgileBoards()); + } + + [Fact] + public async Task Full_Agile_Json_Gets_Deserialized_Successfully() + { + // Arrange + IAgileService agileService = Connections.ConnectionStub(CompleteAgileJson).CreateAgileService(); + + // Act + ICollection result = await agileService.GetAgileBoards(true); + + // Assert + Assert.NotNull(result); + Assert.NotEmpty(result); + + Agile demoBoard = result.FirstOrDefault(); + Assert.NotNull(demoBoard); + Assert.Equal(DemoBoardId, demoBoard.Id); + Assert.Equal(DemoBoardNamePrefix, demoBoard.Name); + + Assert.NotNull(demoBoard.ColumnSettings); + Assert.NotNull(demoBoard.Projects); + Assert.NotNull(demoBoard.Sprints); + Assert.NotNull(demoBoard.Projects); + Assert.NotNull(demoBoard.Sprints); + Assert.NotNull(demoBoard.Status); + Assert.NotNull(demoBoard.ColumnSettings); + Assert.NotNull(demoBoard.CurrentSprint); + Assert.NotNull(demoBoard.EstimationField); + Assert.NotNull(demoBoard.SprintsSettings); + Assert.NotNull(demoBoard.SwimlaneSettings); + Assert.NotNull(demoBoard.ColorCoding); + Assert.NotNull(demoBoard.UpdateableBy); + Assert.NotNull(demoBoard.VisibleFor); + Assert.NotNull(demoBoard.OriginalEstimationField); + + Sprint sprint = demoBoard.Sprints.FirstOrDefault(); + Assert.NotNull(sprint); + Assert.Equal(DemoSprintId, sprint.Id); + Assert.Equal(DemoSprintName, sprint.Name); + } + } } - } } \ No newline at end of file diff --git a/tests/YouTrackSharp.Tests/Internal/FieldSyntaxEncoderTestFixtures.cs b/tests/YouTrackSharp.Tests/Internal/FieldSyntaxEncoderTestFixtures.cs index 1ebf3d7..b95cfad 100644 --- a/tests/YouTrackSharp.Tests/Internal/FieldSyntaxEncoderTestFixtures.cs +++ b/tests/YouTrackSharp.Tests/Internal/FieldSyntaxEncoderTestFixtures.cs @@ -3,115 +3,127 @@ using Newtonsoft.Json; using YouTrackSharp.SerializationAttributes; -namespace YouTrackSharp.Tests.Internal { - [UsedImplicitly] - public class FieldSyntaxEncoderTestFixtures { - public class FlatType { - [JsonProperty("id")] - public int Id { get; set; } - - [JsonProperty("name")] - public string Name { get; set; } - - [Verbose] - [JsonProperty("description")] - public string Description { get; set; } - } - - public class NestedTypes { - [JsonProperty("id")] - public int Id { get; set; } - - [JsonProperty("name")] - public string Name { get; set; } - - [JsonProperty("info")] - public Info Info { get; set; } - } - - public class DeepNestedTypes { - [JsonProperty("id")] - public int Id { get; set; } - - [Verbose] - [JsonProperty("name")] - public string Name { get; set; } - - [JsonProperty("contact")] - public DetailedContact Contact { get; set; } - } - - public class DeepNestedTypesWithInheritance { - [JsonProperty("id")] - public int Id { get; set; } - - [JsonProperty("name")] - public string name { get; set; } - - [JsonProperty("contact")] - public Contact Contact { get; set; } - } - - public class DeepNestedTypesWithCollection { - [JsonProperty("id")] - public int Id { get; set; } - - [JsonProperty("name")] - public string name { get; set; } - - [JsonProperty("contacts")] - public IList Contacts { get; set; } - } - - public class CyclicReference { - [JsonProperty("id")] - public int Id { get; set; } - - [JsonProperty("name")] - public string name { get; set; } - - [Verbose] - [JsonProperty("cyclic")] - public CyclicReference Cyclic { get; set; } - } - - [KnownType(typeof(SimpleContact))] - [KnownType(typeof(DetailedContact))] - public class Contact { - [JsonProperty("id")] - public int Id { get; set; } - } - - public class SimpleContact : Contact { - [JsonProperty("fullName")] - public string FullName { get; set; } - - [Verbose] - [JsonProperty("info")] - public string Info { get; set; } - } - - public class DetailedContact : Contact { - [JsonProperty("firstName")] - public string FirstName { get; set; } - - [JsonProperty("lastName")] - public string LastName { get; set; } - - [Verbose] - [JsonProperty("info")] - public Info Info { get; set; } - } - - public class Info { - [JsonProperty("id")] - public int Id { get; set; } - - [JsonProperty("address")] - public string Address { get; set; } - - [JsonProperty("phoneNumber")] - public string PhoneNumber { get; set; } - } - } +namespace YouTrackSharp.Tests.Internal +{ + [UsedImplicitly] + public class FieldSyntaxEncoderTestFixtures + { + public class FlatType + { + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [Verbose] + [JsonProperty("description")] + public string Description { get; set; } + } + + public class NestedTypes + { + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("info")] + public Info Info { get; set; } + } + + public class DeepNestedTypes + { + [JsonProperty("id")] + public int Id { get; set; } + + [Verbose] + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("contact")] + public DetailedContact Contact { get; set; } + } + + public class DeepNestedTypesWithInheritance + { + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("name")] + public string name { get; set; } + + [JsonProperty("contact")] + public Contact Contact { get; set; } + } + + public class DeepNestedTypesWithCollection + { + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("name")] + public string name { get; set; } + + [JsonProperty("contacts")] + public IList Contacts { get; set; } + } + + public class CyclicReference + { + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("name")] + public string name { get; set; } + + [Verbose] + [JsonProperty("cyclic")] + public CyclicReference Cyclic { get; set; } + } + + [KnownType(typeof(SimpleContact))] + [KnownType(typeof(DetailedContact))] + public class Contact + { + [JsonProperty("id")] + public int Id { get; set; } + } + + public class SimpleContact : Contact + { + [JsonProperty("fullName")] + public string FullName { get; set; } + + [Verbose] + [JsonProperty("info")] + public string Info { get; set; } + } + + public class DetailedContact : Contact + { + [JsonProperty("firstName")] + public string FirstName { get; set; } + + [JsonProperty("lastName")] + public string LastName { get; set; } + + [Verbose] + [JsonProperty("info")] + public Info Info { get; set; } + } + + public class Info + { + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("address")] + public string Address { get; set; } + + [JsonProperty("phoneNumber")] + public string PhoneNumber { get; set; } + } + } } \ No newline at end of file diff --git a/tests/YouTrackSharp.Tests/Internal/FieldSyntaxEncoderTests.cs b/tests/YouTrackSharp.Tests/Internal/FieldSyntaxEncoderTests.cs index bf35adf..c326991 100644 --- a/tests/YouTrackSharp.Tests/Internal/FieldSyntaxEncoderTests.cs +++ b/tests/YouTrackSharp.Tests/Internal/FieldSyntaxEncoderTests.cs @@ -2,149 +2,166 @@ using Xunit; using YouTrackSharp.Internal; -namespace YouTrackSharp.Tests.Internal { - [UsedImplicitly] - public class FieldSyntaxEncoderTests { - public class UrlEncoding { - [Fact] - public void Encode_Flat_Type() { - FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); - string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.FlatType)); +namespace YouTrackSharp.Tests.Internal +{ + [UsedImplicitly] + public class FieldSyntaxEncoderTests + { + public class UrlEncoding + { + [Fact] + public void Encode_Flat_Type() + { + FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); + string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.FlatType)); - string expected = "id,name,description"; + string expected = "id,name,description"; - Assert.Equal(expected, encoded); - } + Assert.Equal(expected, encoded); + } - [Fact] - public void Encode_Flat_Type_Non_Verbose() { - FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); - string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.FlatType), false); + [Fact] + public void Encode_Flat_Type_Non_Verbose() + { + FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); + string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.FlatType), false); - string expected = "id,name"; + string expected = "id,name"; - Assert.Equal(expected, encoded); - } + Assert.Equal(expected, encoded); + } - [Fact] - public void Encode_Single_Nested_Types() { - FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); - string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.NestedTypes)); + [Fact] + public void Encode_Single_Nested_Types() + { + FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); + string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.NestedTypes)); - string expected = "id,name,info(id,address,phoneNumber)"; + string expected = "id,name,info(id,address,phoneNumber)"; - Assert.Equal(expected, encoded); - } + Assert.Equal(expected, encoded); + } - [Fact] - public void Encode_Deep_Nested_Types() { - FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); - string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.DeepNestedTypes)); + [Fact] + public void Encode_Deep_Nested_Types() + { + FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); + string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.DeepNestedTypes)); - string expected = "id,name,contact(firstName,lastName,info(id,address,phoneNumber),id)"; + string expected = "id,name,contact(firstName,lastName,info(id,address,phoneNumber),id)"; - Assert.Equal(expected, encoded); - } + Assert.Equal(expected, encoded); + } - [Fact] - public void Encode_Nested_Types_With_Inheritance_Merges_Common_Field_Info() { - FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); - string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.DeepNestedTypesWithInheritance)); + [Fact] + public void Encode_Nested_Types_With_Inheritance_Merges_Common_Field_Info() + { + FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); + string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.DeepNestedTypesWithInheritance)); - string expected = "id,name,contact(id,fullName,info(id,address,phoneNumber),firstName,lastName)"; + string expected = "id,name,contact(id,fullName,info(id,address,phoneNumber),firstName,lastName)"; - Assert.Equal(expected, encoded); - } + Assert.Equal(expected, encoded); + } - [Fact] - public void Encode_Nested_Types_With_Collection() { - FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); - string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.DeepNestedTypesWithCollection)); + [Fact] + public void Encode_Nested_Types_With_Collection() + { + FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); + string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.DeepNestedTypesWithCollection)); - string expected = "id,name,contacts(id,fullName,info(id,address,phoneNumber),firstName,lastName)"; + string expected = "id,name,contacts(id,fullName,info(id,address,phoneNumber),firstName,lastName)"; - Assert.Equal(expected, encoded); - } + Assert.Equal(expected, encoded); + } - [Fact] - public void Encode_Nested_Types_Max_Depth_0() { - FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); - string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.DeepNestedTypes), true, 0); + [Fact] + public void Encode_Nested_Types_Max_Depth_0() + { + FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); + string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.DeepNestedTypes), true, 0); - string expected = string.Empty; + string expected = string.Empty; - Assert.Equal(expected, encoded); - } + Assert.Equal(expected, encoded); + } - [Fact] - public void Encode_Nested_Types_Max_Depth_1() { - FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); - string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.DeepNestedTypes), true, 1); + [Fact] + public void Encode_Nested_Types_Max_Depth_1() + { + FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); + string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.DeepNestedTypes), true, 1); - string expected = "id,name,contact"; + string expected = "id,name,contact"; - Assert.Equal(expected, encoded); - } + Assert.Equal(expected, encoded); + } - [Fact] - public void Encode_Nested_Types_Max_Depth_2() { - FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); - string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.DeepNestedTypes), true, 2); + [Fact] + public void Encode_Nested_Types_Max_Depth_2() + { + FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); + string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.DeepNestedTypes), true, 2); - string expected = "id,name,contact(firstName,lastName,info,id)"; + string expected = "id,name,contact(firstName,lastName,info,id)"; - Assert.Equal(expected, encoded); - } + Assert.Equal(expected, encoded); + } - [Fact] - public void Encode_Nested_Types_Max_Depth_3() { - FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); - string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.DeepNestedTypes), true, 3); + [Fact] + public void Encode_Nested_Types_Max_Depth_3() + { + FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); + string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.DeepNestedTypes), true, 3); - string expected = "id,name,contact(firstName,lastName,info(id,address,phoneNumber),id)"; + string expected = "id,name,contact(firstName,lastName,info(id,address,phoneNumber),id)"; - Assert.Equal(expected, encoded); - } + Assert.Equal(expected, encoded); + } - [Fact] - public void Encode_Flat_Type_Not_Verbose() { - FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); - string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.FlatType), false); + [Fact] + public void Encode_Flat_Type_Not_Verbose() + { + FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); + string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.FlatType), false); - string expected = "id,name"; + string expected = "id,name"; - Assert.Equal(expected, encoded); - } + Assert.Equal(expected, encoded); + } - [Fact] - public void Encode_Nested_Type_Not_Verbose() { - FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); - string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.DeepNestedTypes), false); + [Fact] + public void Encode_Nested_Type_Not_Verbose() + { + FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); + string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.DeepNestedTypes), false); - string expected = "id,contact(firstName,lastName,id)"; + string expected = "id,contact(firstName,lastName,id)"; - Assert.Equal(expected, encoded); - } - - [Fact] - public void Control_Cyclic_References_With_Max_Depth() { - FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); - string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.CyclicReference), true, 3); + Assert.Equal(expected, encoded); + } - string expected = "id,name,cyclic(id,name,cyclic(id,name,cyclic))"; + [Fact] + public void Control_Cyclic_References_With_Max_Depth() + { + FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); + string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.CyclicReference), true, 3); - Assert.Equal(expected, encoded); - } - - [Fact] - public void Skip_Cyclic_References_With_Verbose_Disabled() { - FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); - string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.CyclicReference), false); + string expected = "id,name,cyclic(id,name,cyclic(id,name,cyclic))"; - string expected = "id,name"; + Assert.Equal(expected, encoded); + } - Assert.Equal(expected, encoded); - } + [Fact] + public void Skip_Cyclic_References_With_Verbose_Disabled() + { + FieldSyntaxEncoder encoder = new FieldSyntaxEncoder(); + string encoded = encoder.Encode(typeof(FieldSyntaxEncoderTestFixtures.CyclicReference), false); + + string expected = "id,name"; + + Assert.Equal(expected, encoded); + } + } } - } } \ No newline at end of file diff --git a/tests/YouTrackSharp.Tests/Json/KnownTypeConverterTestFixtures.cs b/tests/YouTrackSharp.Tests/Json/KnownTypeConverterTestFixtures.cs index 86a7b03..cb45aa3 100644 --- a/tests/YouTrackSharp.Tests/Json/KnownTypeConverterTestFixtures.cs +++ b/tests/YouTrackSharp.Tests/Json/KnownTypeConverterTestFixtures.cs @@ -6,187 +6,209 @@ using YouTrackSharp.Json; using YouTrackSharp.SerializationAttributes; -namespace YouTrackSharp.Tests.Json { - [UsedImplicitly] - public class KnownTypeConverterTestFixtures { - public class Json { - /// - /// Creates a Json string representation of a instance, with given id and name. - /// - /// Id - /// Name - /// Json representation of with given id and name - public string GetJsonForChildA(int id, string name) { - return $"{{ \"id\": {id}, \"name\": \"{name}\", \"$type\": \"ChildA\" }}"; - } - - /// - /// Creates a Json string representation of a instance, with given id and title. - /// - /// Id - /// Title - /// Json representation of with given id and title - public string GetJsonForChildB(int id, string title) { - return $"{{ \"id\": {id}, \"title\": \"{title}\", \"$type\": \"ChildB\" }}"; - } - - /// - /// Creates a Json string representation of an unknown sub-type of (unknown to - /// serialization), with given id and title. - /// - /// Id of object - /// Title of object - /// Json representation of an unknown sub-type of - public string GetJsonForUnknownType(int id, string title) { - return $"{{ \"id\": {id}, \"title\": \"{title}\", \"$type\": \"UnknownType\" }}"; - } - - /// - /// Creates a Json string representation of an object, with no "$type" field, with given id and title. - /// - /// Id of object - /// Title of object - /// Json representation of an untyped object - public string GetJsonForUnspecifiedType(int id, string title) { - return $"{{ \"id\": {id}, \"title\": \"{title}\" }}"; - } - - /// - /// Creates a Json representation of a , with given id, name.
- /// The instance is a json representation of concrete type , - /// with given child id and child name. - ///
- /// Id - /// Name - /// Id of child - /// Name of child - /// - /// Json representation of , with instance variable. - /// - public string GetJsonForCompoundType(int id, string name, int childId, string childName) { - string childJson = GetJsonForChildA(childId, childName); - - return $"{{ \"id\": {id}, \"name\": \"{name}\", \"child\": {childJson}, \"$type\": \"CompoundType\" }}"; - } - - /// - /// Creates a Json representation of a , with given id, name, but null child
- ///
- /// Id - /// Name - /// - /// Json representation of , with null child. - /// - public string GetJsonForCompoundTypeWithNullField(int id, string name) { - return $"{{ \"id\": {id}, \"name\": \"{name}\", \"child\": null, \"$type\": \"CompoundType\" }}"; - } - - /// - /// Creates Json representation of , with given id and name. - /// The is a list made of multiple and - /// instances, created from the given enumerable. - /// This enumerable contains a tuple per child to create, with the concrete type to use ( or - /// , the child id and its name). - /// - /// Id - /// Name - /// Children specifications - /// - /// Json representation of , with array of children of types - /// or . - /// - public string GetJsonForCompoundTypeWithList(int id, string name, IEnumerable> children) { - IEnumerable childrenJson = - children.Select(child => GetJsonForChild(child.Item1, child.Item2, child.Item3)); - - string childrenJsonArray = string.Join(", ", childrenJson); - - return $"{{ \"id\": {id}, \"name\": \"{name}\", \"children\": " + - $"[{childrenJsonArray}], \"$type\": \"CompoundTypeWithList\" }}"; - } - - /// - /// Creates Json representation of or , depending on the given - /// - /// - /// Concrete type ( or ) - /// Id of child - /// Name (for ) or Title (for ) - /// Json representation of given - private string GetJsonForChild(Type concreteType, int id, string nameOrTitle) { - if (concreteType == typeof(ChildA)) { - return GetJsonForChildA(id, nameOrTitle); +namespace YouTrackSharp.Tests.Json +{ + [UsedImplicitly] + public class KnownTypeConverterTestFixtures + { + public class Json + { + /// + /// Creates a Json string representation of a instance, with given id and name. + /// + /// Id + /// Name + /// Json representation of with given id and name + public string GetJsonForChildA(int id, string name) + { + return $"{{ \"id\": {id}, \"name\": \"{name}\", \"$type\": \"ChildA\" }}"; + } + + /// + /// Creates a Json string representation of a instance, with given id and title. + /// + /// Id + /// Title + /// Json representation of with given id and title + public string GetJsonForChildB(int id, string title) + { + return $"{{ \"id\": {id}, \"title\": \"{title}\", \"$type\": \"ChildB\" }}"; + } + + /// + /// Creates a Json string representation of an unknown sub-type of (unknown to + /// serialization), with given id and title. + /// + /// Id of object + /// Title of object + /// Json representation of an unknown sub-type of + public string GetJsonForUnknownType(int id, string title) + { + return $"{{ \"id\": {id}, \"title\": \"{title}\", \"$type\": \"UnknownType\" }}"; + } + + /// + /// Creates a Json string representation of an object, with no "$type" field, with given id and title. + /// + /// Id of object + /// Title of object + /// Json representation of an untyped object + public string GetJsonForUnspecifiedType(int id, string title) + { + return $"{{ \"id\": {id}, \"title\": \"{title}\" }}"; + } + + /// + /// Creates a Json representation of a , with given id, name.
+ /// The instance is a json representation of concrete type , + /// with given child id and child name. + ///
+ /// Id + /// Name + /// Id of child + /// Name of child + /// + /// Json representation of , with instance variable. + /// + public string GetJsonForCompoundType(int id, string name, int childId, string childName) + { + string childJson = GetJsonForChildA(childId, childName); + + return $"{{ \"id\": {id}, \"name\": \"{name}\", \"child\": {childJson}, \"$type\": \"CompoundType\" }}"; + } + + /// + /// Creates a Json representation of a , with given id, name, but null child
+ ///
+ /// Id + /// Name + /// + /// Json representation of , with null child. + /// + public string GetJsonForCompoundTypeWithNullField(int id, string name) + { + return $"{{ \"id\": {id}, \"name\": \"{name}\", \"child\": null, \"$type\": \"CompoundType\" }}"; + } + + /// + /// Creates Json representation of , with given id and name. + /// The is a list made of multiple and + /// instances, created from the given enumerable. + /// This enumerable contains a tuple per child to create, with the concrete type to use ( or + /// , the child id and its name). + /// + /// Id + /// Name + /// Children specifications + /// + /// Json representation of , with array of children of types + /// or . + /// + public string GetJsonForCompoundTypeWithList(int id, string name, + IEnumerable> children) + { + IEnumerable childrenJson = + children.Select(child => GetJsonForChild(child.Item1, child.Item2, child.Item3)); + + string childrenJsonArray = string.Join(", ", childrenJson); + + return $"{{ \"id\": {id}, \"name\": \"{name}\", \"children\": " + + $"[{childrenJsonArray}], \"$type\": \"CompoundTypeWithList\" }}"; + } + + /// + /// Creates json representation of , with given id and name, but + /// with children field set to null. + /// + /// Id + /// Name + /// + /// Json representation of with null children. + /// + public string GetJsonForCompoundTypeWithNullList(int id, string name) + { + return + $"{{ \"id\": {id}, \"name\": \"{name}\", \"children\": null, \"$type\": \"CompoundTypeWithList\" }}"; + } + + /// + /// Creates json representation of , with given id and name, but + /// with the children field set to an empty array. + /// + /// Id + /// Name + /// + /// Json representation of with empty children array. + /// + public string GetJsonForCompoundTypeWithEmptyList(int id, string name) + { + return + $"{{ \"id\": {id}, \"name\": \"{name}\", \"children\": [], \"$type\": \"CompoundTypeWithList\" }}"; + } + + /// + /// Creates Json representation of or , depending on the given + /// + /// + /// Concrete type ( or ) + /// Id of child + /// Name (for ) or Title (for ) + /// Json representation of given + private string GetJsonForChild(Type concreteType, int id, string nameOrTitle) + { + if (concreteType == typeof(ChildA)) + { + return GetJsonForChildA(id, nameOrTitle); + } + + return GetJsonForChildB(id, nameOrTitle); + } } - return GetJsonForChildB(id, nameOrTitle); - } - - /// - /// Creates json representation of , with given id and name, but - /// with children field set to null. - /// - /// Id - /// Name - /// - /// Json representation of with null children. - /// - public string GetJsonForCompoundTypeWithNullList(int id, string name) { - return $"{{ \"id\": {id}, \"name\": \"{name}\", \"children\": null, \"$type\": \"CompoundTypeWithList\" }}"; - } - - /// - /// Creates json representation of , with given id and name, but - /// with the children field set to an empty array. - /// - /// Id - /// Name - /// - /// Json representation of with empty children array. - /// - public string GetJsonForCompoundTypeWithEmptyList(int id, string name) { - return $"{{ \"id\": {id}, \"name\": \"{name}\", \"children\": [], \"$type\": \"CompoundTypeWithList\" }}"; - } - } - - public class CompoundTypeWithList { - [JsonProperty("id")] - public int Id { get; set; } + public class CompoundTypeWithList + { + [JsonProperty("id")] + public int Id { get; set; } - [JsonProperty("name")] - public string Name { get; set; } + [JsonProperty("name")] + public string Name { get; set; } - [JsonProperty("children")] - [JsonConverter(typeof(KnownTypeListConverter))] - public List Children { get; set; } - } + [JsonProperty("children")] + [JsonConverter(typeof(KnownTypeListConverter))] + public List Children { get; set; } + } - public class CompoundType { - [JsonProperty("id")] - public int Id { get; set; } + public class CompoundType + { + [JsonProperty("id")] + public int Id { get; set; } - [JsonProperty("name")] - public string Name { get; set; } + [JsonProperty("name")] + public string Name { get; set; } - [JsonProperty("child")] - [JsonConverter(typeof(KnownTypeConverter))] - public BaseType Child { get; set; } - } + [JsonProperty("child")] + [JsonConverter(typeof(KnownTypeConverter))] + public BaseType Child { get; set; } + } - [KnownType(typeof(ChildA))] - [KnownType(typeof(ChildB))] - public class BaseType { - [JsonProperty("id")] - public int Id { get; set; } - } + [KnownType(typeof(ChildA))] + [KnownType(typeof(ChildB))] + public class BaseType + { + [JsonProperty("id")] + public int Id { get; set; } + } - public class ChildA : BaseType { - [JsonProperty("name")] - public string Name { get; set; } - } + public class ChildA : BaseType + { + [JsonProperty("name")] + public string Name { get; set; } + } - public class ChildB : BaseType { - [JsonProperty("title")] - public string Title { get; set; } + public class ChildB : BaseType + { + [JsonProperty("title")] + public string Title { get; set; } + } } - } } \ No newline at end of file diff --git a/tests/YouTrackSharp.Tests/Json/KnownTypeConverterTests.cs b/tests/YouTrackSharp.Tests/Json/KnownTypeConverterTests.cs index b326ce2..40fd18c 100644 --- a/tests/YouTrackSharp.Tests/Json/KnownTypeConverterTests.cs +++ b/tests/YouTrackSharp.Tests/Json/KnownTypeConverterTests.cs @@ -5,138 +5,148 @@ using YouTrackSharp.Json; using Fixtures = YouTrackSharp.Tests.Json.KnownTypeConverterTestFixtures; -namespace YouTrackSharp.Tests.Json { - [UsedImplicitly] - public class KnownTypeConverterTests { - public class ReadJson { - [Fact] - public void Identifies_Correct_Subtype_ChildTypeA() { - KnownTypeConverter converter = new KnownTypeConverter(); - Fixtures.Json fixtures = new Fixtures.Json(); - - string expectedName = "Example Name"; - int expectedId = 123; - - using JsonTextReader reader = - new JsonTextReader(new StringReader(fixtures.GetJsonForChildA(expectedId, expectedName))); - reader.Read(); - - // Act - Fixtures.ChildA result = - converter.ReadJson(reader, typeof(Fixtures.BaseType), null, - new JsonSerializer()) as Fixtures.ChildA; - - Assert.NotNull(result); - Assert.Equal(expectedId, result.Id); - Assert.Equal(expectedName, result.Name); - } - - [Fact] - public void Identifies_Correct_Subtype_ChildTypeB() { - KnownTypeConverter converter = new KnownTypeConverter(); - Fixtures.Json fixtures = new Fixtures.Json(); - - string expectedTitle = "Example Title"; - int expectedId = 123; - - using JsonTextReader reader = - new JsonTextReader(new StringReader(fixtures.GetJsonForChildB(expectedId, expectedTitle))); - reader.Read(); - - // Act - Fixtures.ChildB result = - converter.ReadJson(reader, typeof(Fixtures.BaseType), null, - new JsonSerializer()) as Fixtures.ChildB; - - Assert.NotNull(result); - Assert.Equal(expectedId, result.Id); - Assert.Equal(expectedTitle, result.Title); - } - - [Fact] - public void Unknown_Subtype_Defaults_To_BaseType() { - KnownTypeConverter converter = new KnownTypeConverter(); - Fixtures.Json fixtures = new Fixtures.Json(); - - string expectedTitle = "Example Title"; - int expectedId = 123; - - using JsonTextReader reader = - new JsonTextReader(new StringReader(fixtures.GetJsonForUnknownType(expectedId, expectedTitle))); - reader.Read(); - - // Act - Fixtures.BaseType result = - converter.ReadJson(reader, typeof(Fixtures.BaseType), null, - new JsonSerializer()) as Fixtures.BaseType; - - Assert.NotNull(result); - Assert.Equal(expectedId, result.Id); - } - - [Fact] - public void Unspecified_Type_Defaults_To_BaseType() { - KnownTypeConverter converter = new KnownTypeConverter(); - Fixtures.Json fixtures = new Fixtures.Json(); - - string expectedTitle = "Example Title"; - int expectedId = 123; - - using JsonTextReader reader = - new JsonTextReader(new StringReader(fixtures.GetJsonForUnspecifiedType(expectedId, expectedTitle))); - reader.Read(); - - // Act - Fixtures.BaseType result = - converter.ReadJson(reader, typeof(Fixtures.BaseType), null, - new JsonSerializer()) as Fixtures.BaseType; - - Assert.NotNull(result); - Assert.Equal(expectedId, result.Id); - } - - [Fact] - public void Deserialize_Type_With_Polymorphic_Field() { - string expectedName = "Example Name"; - int expectedId = 123; - - string expectedChildName = "Child Name"; - int expectedChildId = 456; - - Fixtures.Json fixtures = new Fixtures.Json(); - string json = fixtures.GetJsonForCompoundType(expectedId, expectedName, expectedChildId, expectedChildName); - - Fixtures.CompoundType result = JsonConvert.DeserializeObject(json); - - - Assert.NotNull(result); - Assert.Equal(expectedId, result.Id); - Assert.Equal(expectedName, result.Name); - - Fixtures.ChildA child = result.Child as Fixtures.ChildA; - Assert.NotNull(child); - Assert.Equal(expectedChildId, child.Id); - Assert.Equal(expectedChildName, child.Name); - } - - [Fact] - public void Deserialize_Type_With_Null_Polymorphic_Field() { - string expectedName = "Example Name"; - int expectedId = 123; - - Fixtures.Json fixtures = new Fixtures.Json(); - string json = fixtures.GetJsonForCompoundTypeWithNullField(expectedId, expectedName); - - Fixtures.CompoundType result = JsonConvert.DeserializeObject(json); - - - Assert.NotNull(result); - Assert.Equal(expectedId, result.Id); - Assert.Equal(expectedName, result.Name); - - Fixtures.ChildA child = result.Child as Fixtures.ChildA; - Assert.Null(child); - } +namespace YouTrackSharp.Tests.Json +{ + [UsedImplicitly] + public class KnownTypeConverterTests + { + public class ReadJson + { + [Fact] + public void Identifies_Correct_Subtype_ChildTypeA() + { + KnownTypeConverter converter = new KnownTypeConverter(); + Fixtures.Json fixtures = new Fixtures.Json(); + + string expectedName = "Example Name"; + int expectedId = 123; + + using JsonTextReader reader = + new JsonTextReader(new StringReader(fixtures.GetJsonForChildA(expectedId, expectedName))); + reader.Read(); + + // Act + Fixtures.ChildA result = + converter.ReadJson(reader, typeof(Fixtures.BaseType), null, + new JsonSerializer()) as Fixtures.ChildA; + + Assert.NotNull(result); + Assert.Equal(expectedId, result.Id); + Assert.Equal(expectedName, result.Name); + } + + [Fact] + public void Identifies_Correct_Subtype_ChildTypeB() + { + KnownTypeConverter converter = new KnownTypeConverter(); + Fixtures.Json fixtures = new Fixtures.Json(); + + string expectedTitle = "Example Title"; + int expectedId = 123; + + using JsonTextReader reader = + new JsonTextReader(new StringReader(fixtures.GetJsonForChildB(expectedId, expectedTitle))); + reader.Read(); + + // Act + Fixtures.ChildB result = + converter.ReadJson(reader, typeof(Fixtures.BaseType), null, + new JsonSerializer()) as Fixtures.ChildB; + + Assert.NotNull(result); + Assert.Equal(expectedId, result.Id); + Assert.Equal(expectedTitle, result.Title); + } + + [Fact] + public void Unknown_Subtype_Defaults_To_BaseType() + { + KnownTypeConverter converter = new KnownTypeConverter(); + Fixtures.Json fixtures = new Fixtures.Json(); + + string expectedTitle = "Example Title"; + int expectedId = 123; + + using JsonTextReader reader = + new JsonTextReader(new StringReader(fixtures.GetJsonForUnknownType(expectedId, expectedTitle))); + reader.Read(); + + // Act + Fixtures.BaseType result = + converter.ReadJson(reader, typeof(Fixtures.BaseType), null, new JsonSerializer()) as + Fixtures.BaseType; + + Assert.NotNull(result); + Assert.Equal(expectedId, result.Id); + } + + [Fact] + public void Unspecified_Type_Defaults_To_BaseType() + { + KnownTypeConverter converter = new KnownTypeConverter(); + Fixtures.Json fixtures = new Fixtures.Json(); + + string expectedTitle = "Example Title"; + int expectedId = 123; + + using JsonTextReader reader = + new JsonTextReader(new StringReader(fixtures.GetJsonForUnspecifiedType(expectedId, expectedTitle))); + reader.Read(); + + // Act + Fixtures.BaseType result = + converter.ReadJson(reader, typeof(Fixtures.BaseType), null, new JsonSerializer()) as + Fixtures.BaseType; + + Assert.NotNull(result); + Assert.Equal(expectedId, result.Id); + } + + [Fact] + public void Deserialize_Type_With_Polymorphic_Field() + { + string expectedName = "Example Name"; + int expectedId = 123; + + string expectedChildName = "Child Name"; + int expectedChildId = 456; + + Fixtures.Json fixtures = new Fixtures.Json(); + string json = + fixtures.GetJsonForCompoundType(expectedId, expectedName, expectedChildId, expectedChildName); + + Fixtures.CompoundType result = JsonConvert.DeserializeObject(json); + + + Assert.NotNull(result); + Assert.Equal(expectedId, result.Id); + Assert.Equal(expectedName, result.Name); + + Fixtures.ChildA child = result.Child as Fixtures.ChildA; + Assert.NotNull(child); + Assert.Equal(expectedChildId, child.Id); + Assert.Equal(expectedChildName, child.Name); + } + + [Fact] + public void Deserialize_Type_With_Null_Polymorphic_Field() + { + string expectedName = "Example Name"; + int expectedId = 123; + + Fixtures.Json fixtures = new Fixtures.Json(); + string json = fixtures.GetJsonForCompoundTypeWithNullField(expectedId, expectedName); + + Fixtures.CompoundType result = JsonConvert.DeserializeObject(json); + + + Assert.NotNull(result); + Assert.Equal(expectedId, result.Id); + Assert.Equal(expectedName, result.Name); + + Fixtures.ChildA child = result.Child as Fixtures.ChildA; + Assert.Null(child); + } + } } - } } \ No newline at end of file diff --git a/tests/YouTrackSharp.Tests/Json/KnownTypeListConverterTests.cs b/tests/YouTrackSharp.Tests/Json/KnownTypeListConverterTests.cs index 18d59ac..21c6e9c 100644 --- a/tests/YouTrackSharp.Tests/Json/KnownTypeListConverterTests.cs +++ b/tests/YouTrackSharp.Tests/Json/KnownTypeListConverterTests.cs @@ -6,89 +6,101 @@ using Xunit; using Fixtures = YouTrackSharp.Tests.Json.KnownTypeConverterTestFixtures; -namespace YouTrackSharp.Tests.Json { - [UsedImplicitly] - public class KnownTypeListConverterTests { - public class ReadJson { - [Fact] - public void Deserialize_Type_With_Polymorphic_Collection() { - string expectedName = "Example Name"; - int expectedId = 123; - - IList> expectedChildren = new List>(); - - for (int i = 0; i < 10; i++) { - Type type = i % 2 == 0 ? typeof(Fixtures.ChildA) : typeof(Fixtures.ChildB); - expectedChildren.Add(new Tuple(type, i, $"Child {i}")); +namespace YouTrackSharp.Tests.Json +{ + [UsedImplicitly] + public class KnownTypeListConverterTests + { + public class ReadJson + { + [Fact] + public void Deserialize_Type_With_Polymorphic_Collection() + { + string expectedName = "Example Name"; + int expectedId = 123; + + IList> expectedChildren = new List>(); + + for (int i = 0; i < 10; i++) + { + Type type = i % 2 == 0 ? typeof(Fixtures.ChildA) : typeof(Fixtures.ChildB); + expectedChildren.Add(new Tuple(type, i, $"Child {i}")); + } + + Fixtures.Json fixtures = new Fixtures.Json(); + string json = fixtures.GetJsonForCompoundTypeWithList(expectedId, expectedName, expectedChildren); + + Fixtures.CompoundTypeWithList result = + JsonConvert.DeserializeObject(json); + + Assert.NotNull(result); + Assert.Equal(expectedId, result.Id); + Assert.Equal(expectedName, result.Name); + + Assert.NotNull(result.Children); + foreach (Fixtures.BaseType child in result.Children) + { + int id = child.Id; + Tuple expectedChild = expectedChildren.FirstOrDefault(c => c.Item2 == id); + + Assert.NotNull(expectedChild); + + Assert.Equal(expectedChild.Item1, child.GetType()); + + switch (child) + { + case Fixtures.ChildA childA: + Assert.Equal(expectedChild.Item3, childA.Name); + break; + + case Fixtures.ChildB childB: + Assert.Equal(expectedChild.Item3, childB.Title); + break; + + default: + Assert.True(false, "Child was not deserialized to recognizable type"); + break; + } + } + } + + [Fact] + public void Deserialize_Type_With_Polymorphic_Collection_Null() + { + string expectedName = "Example Name"; + int expectedId = 123; + + Fixtures.Json fixtures = new Fixtures.Json(); + string json = fixtures.GetJsonForCompoundTypeWithNullList(expectedId, expectedName); + + Fixtures.CompoundTypeWithList result = + JsonConvert.DeserializeObject(json); + + Assert.NotNull(result); + Assert.Equal(expectedId, result.Id); + Assert.Equal(expectedName, result.Name); + + Assert.Null(result.Children); + } + + [Fact] + public void Deserialize_Type_With_Polymorphic_Collection_Empty() + { + string expectedName = "Example Name"; + int expectedId = 123; + + Fixtures.Json fixtures = new Fixtures.Json(); + string json = fixtures.GetJsonForCompoundTypeWithEmptyList(expectedId, expectedName); + + Fixtures.CompoundTypeWithList result = + JsonConvert.DeserializeObject(json); + + Assert.NotNull(result); + Assert.Equal(expectedId, result.Id); + Assert.Equal(expectedName, result.Name); + Assert.NotNull(result.Children); + Assert.Empty(result.Children); + } } - - Fixtures.Json fixtures = new Fixtures.Json(); - string json = fixtures.GetJsonForCompoundTypeWithList(expectedId, expectedName, expectedChildren); - - Fixtures.CompoundTypeWithList result = JsonConvert.DeserializeObject(json); - - Assert.NotNull(result); - Assert.Equal(expectedId, result.Id); - Assert.Equal(expectedName, result.Name); - - Assert.NotNull(result.Children); - foreach (Fixtures.BaseType child in result.Children) { - int id = child.Id; - Tuple expectedChild = expectedChildren.FirstOrDefault(c => c.Item2 == id); - - Assert.NotNull(expectedChild); - - Assert.Equal(expectedChild.Item1, child.GetType()); - - switch (child) { - case Fixtures.ChildA childA: - Assert.Equal(expectedChild.Item3, childA.Name); - break; - - case Fixtures.ChildB childB: - Assert.Equal(expectedChild.Item3, childB.Title); - break; - - default: - Assert.True(false, "Child was not deserialized to recognizable type"); - break; - } - } - } - - [Fact] - public void Deserialize_Type_With_Polymorphic_Collection_Null() { - string expectedName = "Example Name"; - int expectedId = 123; - - Fixtures.Json fixtures = new Fixtures.Json(); - string json = fixtures.GetJsonForCompoundTypeWithNullList(expectedId, expectedName); - - Fixtures.CompoundTypeWithList result = JsonConvert.DeserializeObject(json); - - Assert.NotNull(result); - Assert.Equal(expectedId, result.Id); - Assert.Equal(expectedName, result.Name); - - Assert.Null(result.Children); - } - - [Fact] - public void Deserialize_Type_With_Polymorphic_Collection_Empty() { - string expectedName = "Example Name"; - int expectedId = 123; - - Fixtures.Json fixtures = new Fixtures.Json(); - string json = fixtures.GetJsonForCompoundTypeWithEmptyList(expectedId, expectedName); - - Fixtures.CompoundTypeWithList result = JsonConvert.DeserializeObject(json); - - Assert.NotNull(result); - Assert.Equal(expectedId, result.Id); - Assert.Equal(expectedName, result.Name); - Assert.NotNull(result.Children); - Assert.Empty(result.Children); - } } - } } \ No newline at end of file From 38d67fef0f8465ff420b75bbdfb315edf9b8ac21 Mon Sep 17 00:00:00 2001 From: zelodyc Date: Sun, 7 Mar 2021 04:36:33 -0800 Subject: [PATCH 13/14] Correct retrieval of boards in batches (of size reduced to 10), with 'top' and 'skip' properties in URL. Add tests to verify retrieval in batches --- src/YouTrackSharp/Agiles/AgileService.cs | 7 +- .../Infrastructure/Connections.cs | 10 +- .../Integration/Agiles/AgileServiceTest.cs | 12 +- .../Integration/Agiles/GetAgiles.cs | 32 +- .../Agiles/GetAgilesInMultipleBatches.cs | 74 ++++ .../Resources/CompleteAgile.json | 368 +++++++++--------- 6 files changed, 280 insertions(+), 223 deletions(-) create mode 100644 tests/YouTrackSharp.Tests/Integration/Agiles/GetAgilesInMultipleBatches.cs diff --git a/src/YouTrackSharp/Agiles/AgileService.cs b/src/YouTrackSharp/Agiles/AgileService.cs index 0f8ead3..89293de 100644 --- a/src/YouTrackSharp/Agiles/AgileService.cs +++ b/src/YouTrackSharp/Agiles/AgileService.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; @@ -35,15 +36,15 @@ public async Task> GetAgileBoards(bool verbose = false) { HttpClient client = await _connection.GetAuthenticatedHttpClient(); - const int batchSize = 50; + const int batchSize = 10; List agileBoards = new List(); List currentBatch; - + do { string fields = _fieldSyntaxEncoder.Encode(typeof(Agile), verbose); - HttpResponseMessage message = await client.GetAsync($"api/agiles?fields={fields}"); + HttpResponseMessage message = await client.GetAsync($"api/agiles?fields={fields}&$top={batchSize}&$skip={agileBoards.Count}"); string response = await message.Content.ReadAsStringAsync(); diff --git a/tests/YouTrackSharp.Tests/Infrastructure/Connections.cs b/tests/YouTrackSharp.Tests/Infrastructure/Connections.cs index 121d39c..e672294 100644 --- a/tests/YouTrackSharp.Tests/Infrastructure/Connections.cs +++ b/tests/YouTrackSharp.Tests/Infrastructure/Connections.cs @@ -26,12 +26,12 @@ public static string ServerUrl public static Connection ConnectionStub(string content, HttpStatusCode status = HttpStatusCode.OK) { - HttpResponseMessage response = new HttpResponseMessage(status); - response.Content = new StringContent(content); - - return new ConnectionStub(_ => response); + HttpResponseMessage response = new HttpResponseMessage(status); + response.Content = new StringContent(content); + + return new ConnectionStub(_ => response); } - + public static class TestData { public static readonly List ValidConnections diff --git a/tests/YouTrackSharp.Tests/Integration/Agiles/AgileServiceTest.cs b/tests/YouTrackSharp.Tests/Integration/Agiles/AgileServiceTest.cs index 2f2f61b..b0c04b4 100644 --- a/tests/YouTrackSharp.Tests/Integration/Agiles/AgileServiceTest.cs +++ b/tests/YouTrackSharp.Tests/Integration/Agiles/AgileServiceTest.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Linq; using System.Reflection; namespace YouTrackSharp.Tests.Integration.Agiles @@ -11,7 +12,16 @@ public partial class AgileServiceTest private static string DemoSprintId => "109-2"; private static string DemoSprintName => "First sprint"; - private static string CompleteAgileJson => GetTextResource("YouTrackSharp.Tests.Resources.CompleteAgile.json"); + private static string SingleAgileJson => GetTextResource("YouTrackSharp.Tests.Resources.CompleteAgile.json"); + + private static string GetAgileJsonArray(int count) + { + string agileJson = SingleAgileJson; + + string agilesJson = string.Join(",", Enumerable.Range(0, count).Select(_ => agileJson)); + + return $"[{agilesJson}]"; + } private static string GetTextResource(string name) { diff --git a/tests/YouTrackSharp.Tests/Integration/Agiles/GetAgiles.cs b/tests/YouTrackSharp.Tests/Integration/Agiles/GetAgiles.cs index b540ee7..4a2ad3a 100644 --- a/tests/YouTrackSharp.Tests/Integration/Agiles/GetAgiles.cs +++ b/tests/YouTrackSharp.Tests/Integration/Agiles/GetAgiles.cs @@ -14,7 +14,7 @@ public partial class AgileServiceTest public class GetAgiles { [Fact] - public async Task Valid_Connection_Return_Existing_Agile_Verbose() + public async Task Valid_Connection_Return_Existing_Agiles_Verbose() { // Arrange IAgileService agileService = Connections.Demo1Token.CreateAgileService(); @@ -30,35 +30,10 @@ public async Task Valid_Connection_Return_Existing_Agile_Verbose() Assert.NotNull(demoBoard); Assert.Equal(DemoBoardId, demoBoard.Id); Assert.Equal(DemoBoardNamePrefix, demoBoard.Name); - - Assert.NotNull(demoBoard.ColumnSettings); - Assert.NotNull(demoBoard.Projects); - Assert.NotNull(demoBoard.Sprints); - Assert.NotNull(demoBoard.Projects); - Assert.NotNull(demoBoard.Sprints); - Assert.NotNull(demoBoard.Status); - - Assert.NotNull(demoBoard.ColumnSettings); - Assert.NotNull(demoBoard.CurrentSprint); - Assert.NotNull(demoBoard.EstimationField); - Assert.NotNull(demoBoard.SprintsSettings); - Assert.NotNull(demoBoard.SwimlaneSettings); - - - // These fields are null in the agile test - Assert.Null(demoBoard.ColorCoding); - Assert.Null(demoBoard.UpdateableBy); - Assert.Null(demoBoard.VisibleFor); - Assert.Null(demoBoard.OriginalEstimationField); - - Sprint sprint = demoBoard.Sprints.FirstOrDefault(); - Assert.NotNull(sprint); - Assert.Equal(DemoSprintId, sprint.Id); - Assert.Equal(DemoSprintName, sprint.Name); } [Fact] - public async Task Verbose_Disabled_Returns_Agile_Minimum_Info() + public async Task Verbose_Disabled_Returns_Agiles_Non_Verbose() { // Arrange IAgileService agileService = Connections.Demo1Token.CreateAgileService(); @@ -72,7 +47,6 @@ public async Task Verbose_Disabled_Returns_Agile_Minimum_Info() Agile demoBoard = result.FirstOrDefault(); Assert.NotNull(demoBoard); - Assert.Equal(DemoBoardId, demoBoard.Id); Assert.Equal(DemoBoardNamePrefix, demoBoard.Name); } @@ -92,7 +66,7 @@ await Assert.ThrowsAsync( public async Task Full_Agile_Json_Gets_Deserialized_Successfully() { // Arrange - IAgileService agileService = Connections.ConnectionStub(CompleteAgileJson).CreateAgileService(); + IAgileService agileService = Connections.ConnectionStub(GetAgileJsonArray(1)).CreateAgileService(); // Act ICollection result = await agileService.GetAgileBoards(true); diff --git a/tests/YouTrackSharp.Tests/Integration/Agiles/GetAgilesInMultipleBatches.cs b/tests/YouTrackSharp.Tests/Integration/Agiles/GetAgilesInMultipleBatches.cs new file mode 100644 index 0000000..b6cbf11 --- /dev/null +++ b/tests/YouTrackSharp.Tests/Integration/Agiles/GetAgilesInMultipleBatches.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Xunit; +using YouTrackSharp.Agiles; +using YouTrackSharp.Tests.Infrastructure; + +namespace YouTrackSharp.Tests.Integration.Agiles +{ + public partial class AgileServiceTest + { + public class GetAgilesInMultipleBatches + { + /// + /// Creates a JSON array of Agile objects, whose size is determined by the "$top" and "$skip" parameters + /// of the request (with a maximum of - skipped).

+ /// This allows to simulate returning a total number of agiles, in batches (the size of the batch is + /// determined by the itself. + ///
+ /// REST request, with the $top parameter indicating the max number of results + /// Total number of agiles to simulate in the server + /// + /// Json array of agiles, whose size is hose size is determined by the "$top" and "$skip" parameters of the + /// request (with a maximum of - skipped) + /// + private HttpResponseMessage GetAgileBatch(HttpRequestMessage request, int totalAgiles) + { + string requestUri = request.RequestUri.ToString(); + + Match match = Regex.Match(requestUri, "(&\\$top=(?[0-9]+))?(&\\$skip=(?[0-9]+))?"); + + int top = totalAgiles; + if (match.Groups.ContainsKey("top") && match.Groups["top"].Success) + { + top = int.Parse(match.Groups["top"].Value); + } + + int skip = 0; + if (match.Groups.ContainsKey("skip") && match.Groups["skip"].Success) + { + skip = int.Parse(match.Groups["skip"].Value); + } + + int batchSize = Math.Min(top, totalAgiles - skip); + + string agileJsonArray = GetAgileJsonArray(batchSize); + HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.OK); + response.Content = new StringContent(agileJsonArray); + + return response; + } + + [Fact] + public async Task Many_Agiles_Are_Fetched_In_Batches() + { + // Arrange + const int totalAgileCount = 53; + Connection connection = new ConnectionStub(request => GetAgileBatch(request, totalAgileCount)); + IAgileService agileService = connection.CreateAgileService(); + + // Act + ICollection result = await agileService.GetAgileBoards(true); + + // Assert + Assert.NotNull(result); + Assert.NotEmpty(result); + Assert.Equal(totalAgileCount, result.Count); + } + } + } +} \ No newline at end of file diff --git a/tests/YouTrackSharp.Tests/Resources/CompleteAgile.json b/tests/YouTrackSharp.Tests/Resources/CompleteAgile.json index d2e2d97..0a65cff 100644 --- a/tests/YouTrackSharp.Tests/Resources/CompleteAgile.json +++ b/tests/YouTrackSharp.Tests/Resources/CompleteAgile.json @@ -1,202 +1,200 @@ -[ - { - "projects": [ - { - "shortName": "DP1", - "name": "DemoProject1", - "id": "0-1", - "$type": "Project" - } - ], - "swimlaneSettings": { - "field": { - "customField": { - "name": "Type", - "$type": "CustomField" - }, - "presentation": "Type", - "id": "51-2", +{ + "projects": [ + { + "shortName": "DP1", + "name": "DemoProject1", + "id": "0-1", + "$type": "Project" + } + ], + "swimlaneSettings": { + "field": { + "customField": { "name": "Type", - "$type": "CustomFilterField" - }, - "values": [ - { - "name": "Feature", - "id": "Feature", - "$type": "SwimlaneValue" - } - ], - "defaultCardType": { - "name": "Task", - "id": "Task", - "$type": "SwimlaneValue" + "$type": "CustomField" }, - "id": "105-3", - "$type": "IssueBasedSwimlaneSettings" - }, - "estimationField": { - "name": "Estimation", - "$type": "CustomField" + "presentation": "Type", + "id": "51-2", + "name": "Type", + "$type": "CustomFilterField" }, - "sprints": [ + "values": [ { - "name": "First sprint", - "id": "109-2", - "$type": "Sprint" + "name": "Feature", + "id": "Feature", + "$type": "SwimlaneValue" } ], - "hideOrphansSwimlane": false, - "orphansAtTheTop": false, - "visibleForProjectBased": true, - "updateableByProjectBased": true, - "columnSettings": { - "field": { - "name": "State", - "$type": "CustomField" - }, - "columns": [ - { - "fieldValues": [ - { - "name": "Open", - "isResolved": false, - "id": "111-12", - "$type": "AgileColumnFieldValue" - } - ], - "ordinal": 0, - "presentation": "Open", - "wipLimit": null, - "isResolved": false, - "id": "110-8", - "$type": "AgileColumn" - }, - { - "fieldValues": [ - { - "name": "In Progress", - "isResolved": false, - "id": "111-13", - "$type": "AgileColumnFieldValue" - } - ], - "ordinal": 1, - "presentation": "In Progress", - "wipLimit": null, - "isResolved": false, - "id": "110-9", - "$type": "AgileColumn" - }, - { - "fieldValues": [ - { - "name": "Fixed", - "isResolved": true, - "id": "111-14", - "$type": "AgileColumnFieldValue" - } - ], - "ordinal": 2, - "presentation": "Fixed", - "wipLimit": null, - "isResolved": true, - "id": "110-10", - "$type": "AgileColumn" - }, - { - "fieldValues": [ - { - "name": "Verified", - "isResolved": true, - "id": "111-15", - "$type": "AgileColumnFieldValue" - } - ], - "ordinal": 3, - "presentation": "Verified", - "wipLimit": null, - "isResolved": true, - "id": "110-11", - "$type": "AgileColumn" - } - ], - "id": "108-2", - "$type": "ColumnSettings" + "defaultCardType": { + "name": "Task", + "id": "Task", + "$type": "SwimlaneValue" }, - "colorCoding": { - "prototype": { - "name": "Type", - "$type": "CustomField" - }, - "id": "108-5", - "$type": "FieldBasedColorCoding" - }, - "currentSprint": { + "id": "105-3", + "$type": "IssueBasedSwimlaneSettings" + }, + "estimationField": { + "name": "Estimation", + "$type": "CustomField" + }, + "sprints": [ + { "name": "First sprint", "id": "109-2", "$type": "Sprint" - }, - "originalEstimationField": { - "name": "OriginalEstimationField", + } + ], + "hideOrphansSwimlane": false, + "orphansAtTheTop": false, + "visibleForProjectBased": true, + "updateableByProjectBased": true, + "columnSettings": { + "field": { + "name": "State", "$type": "CustomField" }, - "sprintsSettings": { - "explicitQuery": null, - "isExplicit": true, - "disableSprints": false, - "hideSubtasksOfCards": false, - "cardOnSeveralSprints": false, - "defaultSprint": null, - "sprintSyncField": null, - "id": "108-2", - "$type": "SprintsSettings" - }, - "visibleFor": { - "allUsersGroup" : false, - "icon" : "String", - "name" : "String", - "ringId" : "String", - "teamForProject" : { - "shortName": "DP1", - "name": "DemoProject1", - "id": "0-1", - "$type": "Project" + "columns": [ + { + "fieldValues": [ + { + "name": "Open", + "isResolved": false, + "id": "111-12", + "$type": "AgileColumnFieldValue" + } + ], + "ordinal": 0, + "presentation": "Open", + "wipLimit": null, + "isResolved": false, + "id": "110-8", + "$type": "AgileColumn" }, - "usersCount" : 2, - "id" : "String", - "$type" : "UserGroup" - }, - "updateableBy": { - "allUsersGroup" : false, - "icon" : "String", - "name" : "String", - "ringId" : "String", - "teamForProject" : { - "shortName": "DP1", - "name": "DemoProject1", - "id": "0-1", - "$type": "Project" + { + "fieldValues": [ + { + "name": "In Progress", + "isResolved": false, + "id": "111-13", + "$type": "AgileColumnFieldValue" + } + ], + "ordinal": 1, + "presentation": "In Progress", + "wipLimit": null, + "isResolved": false, + "id": "110-9", + "$type": "AgileColumn" }, - "usersCount" : 2, - "id" : "String", - "$type" : "UserGroup" + { + "fieldValues": [ + { + "name": "Fixed", + "isResolved": true, + "id": "111-14", + "$type": "AgileColumnFieldValue" + } + ], + "ordinal": 2, + "presentation": "Fixed", + "wipLimit": null, + "isResolved": true, + "id": "110-10", + "$type": "AgileColumn" + }, + { + "fieldValues": [ + { + "name": "Verified", + "isResolved": true, + "id": "111-15", + "$type": "AgileColumnFieldValue" + } + ], + "ordinal": 3, + "presentation": "Verified", + "wipLimit": null, + "isResolved": true, + "id": "110-11", + "$type": "AgileColumn" + } + ], + "id": "108-2", + "$type": "ColumnSettings" + }, + "colorCoding": { + "prototype": { + "name": "Type", + "$type": "CustomField" }, - "status": { - "errors": [], - "valid": true, - "hasJobs": false, - "warnings": [], - "id": "boardStatus", - "$type": "AgileStatus" + "id": "108-5", + "$type": "FieldBasedColorCoding" + }, + "currentSprint": { + "name": "First sprint", + "id": "109-2", + "$type": "Sprint" + }, + "originalEstimationField": { + "name": "OriginalEstimationField", + "$type": "CustomField" + }, + "sprintsSettings": { + "explicitQuery": null, + "isExplicit": true, + "disableSprints": false, + "hideSubtasksOfCards": false, + "cardOnSeveralSprints": false, + "defaultSprint": null, + "sprintSyncField": null, + "id": "108-2", + "$type": "SprintsSettings" + }, + "visibleFor": { + "allUsersGroup" : false, + "icon" : "String", + "name" : "String", + "ringId" : "String", + "teamForProject" : { + "shortName": "DP1", + "name": "DemoProject1", + "id": "0-1", + "$type": "Project" }, - "owner": { - "fullName": "Demo User 1", - "ringId": "0692a47b-3670-452e-8e73-8b166a774705", - "id": "1-2", - "$type": "User" + "usersCount" : 2, + "id" : "String", + "$type" : "UserGroup" + }, + "updateableBy": { + "allUsersGroup" : false, + "icon" : "String", + "name" : "String", + "ringId" : "String", + "teamForProject" : { + "shortName": "DP1", + "name": "DemoProject1", + "id": "0-1", + "$type": "Project" }, - "name": "Test Board597fb561-ea1f-4095-9636-859ae4439605", - "id": "108-2", - "$type": "Agile" - } -] \ No newline at end of file + "usersCount" : 2, + "id" : "String", + "$type" : "UserGroup" + }, + "status": { + "errors": [], + "valid": true, + "hasJobs": false, + "warnings": [], + "id": "boardStatus", + "$type": "AgileStatus" + }, + "owner": { + "fullName": "Demo User 1", + "ringId": "0692a47b-3670-452e-8e73-8b166a774705", + "id": "1-2", + "$type": "User" + }, + "name": "Test Board597fb561-ea1f-4095-9636-859ae4439605", + "id": "108-2", + "$type": "Agile" +} \ No newline at end of file From a550c528946493d5f0060aeef0e478af8fca4e34 Mon Sep 17 00:00:00 2001 From: zelodyc Date: Sun, 14 Mar 2021 22:37:17 -0700 Subject: [PATCH 14/14] Refactor tests. Add JsonArrayHandler / JsonHandler to simplify tests using ConnectionStub. Added check of requests number for multiple fetches in batches. Correct handling of and paramaters (support any order). Add multiple flavors of agile boards to tests. --- .../Infrastructure/ConnectionStub.cs | 51 +--- .../Infrastructure/Connections.cs | 8 - .../Infrastructure/JsonArrayHandler.cs | 91 +++++++ .../Infrastructure/JsonHandler.cs | 33 +++ .../Integration/Agiles/AgileServiceTest.cs | 14 +- .../Integration/Agiles/GetAgiles.cs | 80 ++++-- .../Agiles/GetAgilesInMultipleBatches.cs | 52 +--- .../Resources/FullAgile01.json | 232 ++++++++++++++++ .../Resources/FullAgile02.json | 251 ++++++++++++++++++ .../YouTrackSharp.Tests.csproj | 4 +- 10 files changed, 681 insertions(+), 135 deletions(-) create mode 100644 tests/YouTrackSharp.Tests/Infrastructure/JsonArrayHandler.cs create mode 100644 tests/YouTrackSharp.Tests/Infrastructure/JsonHandler.cs create mode 100644 tests/YouTrackSharp.Tests/Resources/FullAgile01.json create mode 100644 tests/YouTrackSharp.Tests/Resources/FullAgile02.json diff --git a/tests/YouTrackSharp.Tests/Infrastructure/ConnectionStub.cs b/tests/YouTrackSharp.Tests/Infrastructure/ConnectionStub.cs index d231c97..22696a1 100644 --- a/tests/YouTrackSharp.Tests/Infrastructure/ConnectionStub.cs +++ b/tests/YouTrackSharp.Tests/Infrastructure/ConnectionStub.cs @@ -1,6 +1,5 @@ using System; using System.Net.Http; -using System.Threading; using System.Threading.Tasks; namespace YouTrackSharp.Tests.Infrastructure @@ -10,18 +9,19 @@ namespace YouTrackSharp.Tests.Infrastructure ///
public class ConnectionStub : Connection { - private Func ExecuteRequest { get; } - + private readonly HttpClientHandler _handler; private TimeSpan TimeOut => TimeSpan.FromSeconds(100); /// /// Creates an instance of with give response delegate /// - /// Request delegate - public ConnectionStub(Func executeRequest) : base( - "http://fake.connection.com/") + /// + /// to associate to this connection. + /// This can be used to pass a stub handler for testing purposes. + /// + public ConnectionStub(HttpClientHandler handler) : base("http://fake.connection.com/") { - ExecuteRequest = executeRequest; + _handler = handler; } /// @@ -31,42 +31,11 @@ public ConnectionStub(Func executeReque /// configured to return a predefined message and HTTP status public override Task GetAuthenticatedHttpClient() { - return Task.FromResult(CreateClient()); - } - - private HttpClient CreateClient() - { - HttpClient httpClient = new HttpClient(new HttpClientHandlerStub(ExecuteRequest)); + HttpClient httpClient = new HttpClient(_handler); httpClient.BaseAddress = ServerUri; httpClient.Timeout = TimeOut; - - return httpClient; - } - } - - /// - /// mock, that returns a predefined reply and HTTP status code. - /// - public class HttpClientHandlerStub : HttpClientHandler - { - private Func ExecuteRequest { get; } - - /// - /// Creates an instance that delegates HttpRequestMessages - /// - /// Request delegate - public HttpClientHandlerStub(Func executeRequest) - { - ExecuteRequest = executeRequest; - } - - /// - protected override Task SendAsync(HttpRequestMessage request, - CancellationToken cancellationToken) - { - HttpResponseMessage reply = ExecuteRequest?.Invoke(request); - - return Task.FromResult(reply); + + return Task.FromResult(httpClient); } } } \ No newline at end of file diff --git a/tests/YouTrackSharp.Tests/Infrastructure/Connections.cs b/tests/YouTrackSharp.Tests/Infrastructure/Connections.cs index e672294..94d5a0a 100644 --- a/tests/YouTrackSharp.Tests/Infrastructure/Connections.cs +++ b/tests/YouTrackSharp.Tests/Infrastructure/Connections.cs @@ -23,14 +23,6 @@ public static string ServerUrl public static Connection Demo3Token => new BearerTokenConnection(ServerUrl, "perm:ZGVtbzM=.WW91VHJhY2tTaGFycA==.L04RdcCnjyW2UPCVg1qyb6dQflpzFy", ConfigureTestsHandler); - - public static Connection ConnectionStub(string content, HttpStatusCode status = HttpStatusCode.OK) - { - HttpResponseMessage response = new HttpResponseMessage(status); - response.Content = new StringContent(content); - - return new ConnectionStub(_ => response); - } public static class TestData { diff --git a/tests/YouTrackSharp.Tests/Infrastructure/JsonArrayHandler.cs b/tests/YouTrackSharp.Tests/Infrastructure/JsonArrayHandler.cs new file mode 100644 index 0000000..83765e1 --- /dev/null +++ b/tests/YouTrackSharp.Tests/Infrastructure/JsonArrayHandler.cs @@ -0,0 +1,91 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +namespace YouTrackSharp.Tests.Infrastructure +{ + /// + /// This handler is used to return a json array from a range of give json strings, created from the $top and $skip + /// parameters of the HTTP request. + /// This handler can be used to simulate a server returning json arrays in batches. + /// + public class JsonArrayHandler : HttpClientHandler + { + private readonly ICollection _jsonObjects; + public int RequestsReceived { get; private set; } + + /// + /// Creates an instance of + /// + /// List of json objects that this instance will pick from + public JsonArrayHandler(params string[] jsonObjects) + { + _jsonObjects = jsonObjects; + RequestsReceived = 0; + } + + /// + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + RequestsReceived++; + + GetRequestedRange(request, _jsonObjects.Count, out int skip, out int count); + string json = GetJsonArray(_jsonObjects, skip, count); + + HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.OK); + response.Content = new StringContent(json); + + return Task.FromResult(response); + } + + /// + /// Creates a JSON array from a range of the given json strings. + /// This allows to simulate returning a total number of elements, in batches. + /// + /// JSON objects from which the JSON array will be created + /// Number of items to skip + /// Number of items to return + /// + /// Json array + /// + private string GetJsonArray(ICollection jsonObjects, int skip, int count) + { + IEnumerable jsonObjectRange = jsonObjects.Skip(skip).Take(count); + string json = $"[{string.Join(",", jsonObjectRange)}]"; + + return json; + } + + /// + /// Parses the $skip and $top parameters from a Youtrack REST request URI, and computes the requested range + /// of objects to return (capped by ). + /// + /// HTTP request + /// Max index (range will not go beyond that index, even if $skip + $top is greater + /// Number of items to skip + /// Number of items to return + /// Range computed from request's $skip and $top + private void GetRequestedRange(HttpRequestMessage request, int maxIndex, out int skip, out int count) + { + string requestUri = request.RequestUri.ToString(); + + Match match = Regex.Match(requestUri, "&\\$top=(?[0-9]+)(&\\$skip=(?[0-9]+))?|&\\$skip=(?[0-9]+)(&\\$top=(?[0-9]+))?"); + + count = maxIndex; + if (match.Groups.ContainsKey("top") && match.Groups["top"].Success) + { + count = int.Parse(match.Groups["top"].Value); + } + + skip = 0; + if (match.Groups.ContainsKey("skip") && match.Groups["skip"].Success) + { + skip = int.Parse(match.Groups["skip"].Value); + } + } + } +} \ No newline at end of file diff --git a/tests/YouTrackSharp.Tests/Infrastructure/JsonHandler.cs b/tests/YouTrackSharp.Tests/Infrastructure/JsonHandler.cs new file mode 100644 index 0000000..bfb5360 --- /dev/null +++ b/tests/YouTrackSharp.Tests/Infrastructure/JsonHandler.cs @@ -0,0 +1,33 @@ +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace YouTrackSharp.Tests.Infrastructure +{ + /// + /// This handler returns a predefined json string on every request. + /// + public class JsonHandler : HttpClientHandler + { + private readonly string _json; + + /// + /// Creates an instance of + /// + /// Json string that will be returned upon each request + public JsonHandler(string json) + { + _json = json; + } + + /// + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.OK); + response.Content = new StringContent(_json); + + return Task.FromResult(response); + } + } +} \ No newline at end of file diff --git a/tests/YouTrackSharp.Tests/Integration/Agiles/AgileServiceTest.cs b/tests/YouTrackSharp.Tests/Integration/Agiles/AgileServiceTest.cs index b0c04b4..ee1b894 100644 --- a/tests/YouTrackSharp.Tests/Integration/Agiles/AgileServiceTest.cs +++ b/tests/YouTrackSharp.Tests/Integration/Agiles/AgileServiceTest.cs @@ -8,20 +8,12 @@ public partial class AgileServiceTest { private static string DemoBoardId => "108-2"; private static string DemoBoardNamePrefix => "Test Board597fb561-ea1f-4095-9636-859ae4439605"; - + private static string DemoSprintId => "109-2"; private static string DemoSprintName => "First sprint"; - private static string SingleAgileJson => GetTextResource("YouTrackSharp.Tests.Resources.CompleteAgile.json"); - - private static string GetAgileJsonArray(int count) - { - string agileJson = SingleAgileJson; - - string agilesJson = string.Join(",", Enumerable.Range(0, count).Select(_ => agileJson)); - - return $"[{agilesJson}]"; - } + private static string FullAgile01 => GetTextResource("YouTrackSharp.Tests.Resources.FullAgile01.json"); + private static string FullAgile02 => GetTextResource("YouTrackSharp.Tests.Resources.FullAgile02.json"); private static string GetTextResource(string name) { diff --git a/tests/YouTrackSharp.Tests/Integration/Agiles/GetAgiles.cs b/tests/YouTrackSharp.Tests/Integration/Agiles/GetAgiles.cs index 4a2ad3a..a9cea6d 100644 --- a/tests/YouTrackSharp.Tests/Integration/Agiles/GetAgiles.cs +++ b/tests/YouTrackSharp.Tests/Integration/Agiles/GetAgiles.cs @@ -63,43 +63,65 @@ await Assert.ThrowsAsync( } [Fact] - public async Task Full_Agile_Json_Gets_Deserialized_Successfully() + public async Task Mock_Connection_Returns_Full_Agiles() { // Arrange - IAgileService agileService = Connections.ConnectionStub(GetAgileJsonArray(1)).CreateAgileService(); + string[] strings = { FullAgile01, FullAgile02 }; + + JsonArrayHandler handler = new JsonArrayHandler(strings); + ConnectionStub connection = new ConnectionStub(handler); + + IAgileService agileService = connection.CreateAgileService(); // Act ICollection result = await agileService.GetAgileBoards(true); // Assert Assert.NotNull(result); - Assert.NotEmpty(result); - - Agile demoBoard = result.FirstOrDefault(); - Assert.NotNull(demoBoard); - Assert.Equal(DemoBoardId, demoBoard.Id); - Assert.Equal(DemoBoardNamePrefix, demoBoard.Name); - - Assert.NotNull(demoBoard.ColumnSettings); - Assert.NotNull(demoBoard.Projects); - Assert.NotNull(demoBoard.Sprints); - Assert.NotNull(demoBoard.Projects); - Assert.NotNull(demoBoard.Sprints); - Assert.NotNull(demoBoard.Status); - Assert.NotNull(demoBoard.ColumnSettings); - Assert.NotNull(demoBoard.CurrentSprint); - Assert.NotNull(demoBoard.EstimationField); - Assert.NotNull(demoBoard.SprintsSettings); - Assert.NotNull(demoBoard.SwimlaneSettings); - Assert.NotNull(demoBoard.ColorCoding); - Assert.NotNull(demoBoard.UpdateableBy); - Assert.NotNull(demoBoard.VisibleFor); - Assert.NotNull(demoBoard.OriginalEstimationField); - - Sprint sprint = demoBoard.Sprints.FirstOrDefault(); - Assert.NotNull(sprint); - Assert.Equal(DemoSprintId, sprint.Id); - Assert.Equal(DemoSprintName, sprint.Name); + Assert.Equal(2, result.Count); + + foreach (Agile agile in result) + { + Assert.NotNull(agile); + + Assert.True("109-1".Equals(agile.Id) || "109-2".Equals(agile.Id)); + + Assert.NotNull(agile.ColumnSettings); + Assert.NotNull(agile.Projects); + Assert.NotNull(agile.Sprints); + Assert.NotNull(agile.Projects); + Assert.NotNull(agile.Sprints); + Assert.NotNull(agile.Status); + Assert.NotNull(agile.ColumnSettings); + Assert.NotNull(agile.CurrentSprint); + Assert.NotNull(agile.EstimationField); + Assert.NotNull(agile.SprintsSettings); + Assert.NotNull(agile.SwimlaneSettings); + Assert.NotNull(agile.ColorCoding); + Assert.NotNull(agile.UpdateableBy); + Assert.NotNull(agile.VisibleFor); + Assert.NotNull(agile.OriginalEstimationField); + + Sprint sprint = agile.Sprints.FirstOrDefault(); + Assert.NotNull(sprint); + Assert.Equal(DemoSprintId, sprint.Id); + Assert.Equal(DemoSprintName, sprint.Name); + + if ("109-1".Equals(agile.Id)) + { + Assert.Equal("Full Board 01", agile.Name); + Assert.IsType(agile.ColorCoding); + Assert.IsType(agile.SwimlaneSettings); + Assert.IsType(((IssueBasedSwimlaneSettings)agile.SwimlaneSettings).Field); + } + else + { + Assert.Equal("Full Board 02", agile.Name); + Assert.IsType(agile.ColorCoding); + Assert.IsType(agile.SwimlaneSettings); + Assert.IsType(((AttributeBasedSwimlaneSettings)agile.SwimlaneSettings).Field); + } + } } } } diff --git a/tests/YouTrackSharp.Tests/Integration/Agiles/GetAgilesInMultipleBatches.cs b/tests/YouTrackSharp.Tests/Integration/Agiles/GetAgilesInMultipleBatches.cs index b6cbf11..ae3b640 100644 --- a/tests/YouTrackSharp.Tests/Integration/Agiles/GetAgilesInMultipleBatches.cs +++ b/tests/YouTrackSharp.Tests/Integration/Agiles/GetAgilesInMultipleBatches.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using System.Net; -using System.Net.Http; -using System.Text.RegularExpressions; +using System.Linq; using System.Threading.Tasks; using Xunit; using YouTrackSharp.Agiles; @@ -14,51 +12,16 @@ public partial class AgileServiceTest { public class GetAgilesInMultipleBatches { - /// - /// Creates a JSON array of Agile objects, whose size is determined by the "$top" and "$skip" parameters - /// of the request (with a maximum of - skipped).

- /// This allows to simulate returning a total number of agiles, in batches (the size of the batch is - /// determined by the itself. - ///
- /// REST request, with the $top parameter indicating the max number of results - /// Total number of agiles to simulate in the server - /// - /// Json array of agiles, whose size is hose size is determined by the "$top" and "$skip" parameters of the - /// request (with a maximum of - skipped) - /// - private HttpResponseMessage GetAgileBatch(HttpRequestMessage request, int totalAgiles) - { - string requestUri = request.RequestUri.ToString(); - - Match match = Regex.Match(requestUri, "(&\\$top=(?[0-9]+))?(&\\$skip=(?[0-9]+))?"); - - int top = totalAgiles; - if (match.Groups.ContainsKey("top") && match.Groups["top"].Success) - { - top = int.Parse(match.Groups["top"].Value); - } - - int skip = 0; - if (match.Groups.ContainsKey("skip") && match.Groups["skip"].Success) - { - skip = int.Parse(match.Groups["skip"].Value); - } - - int batchSize = Math.Min(top, totalAgiles - skip); - - string agileJsonArray = GetAgileJsonArray(batchSize); - HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.OK); - response.Content = new StringContent(agileJsonArray); - - return response; - } - [Fact] - public async Task Many_Agiles_Are_Fetched_In_Batches() + public async Task Mock_Connection_Return_Many_Agiles_In_Batches() { // Arrange const int totalAgileCount = 53; - Connection connection = new ConnectionStub(request => GetAgileBatch(request, totalAgileCount)); + int expectedRequests = (int)Math.Ceiling(totalAgileCount / 10.0); + + string[] jsonStrings = Enumerable.Range(0, totalAgileCount).Select(i => FullAgile01).ToArray(); + JsonArrayHandler handler = new JsonArrayHandler(jsonStrings); + ConnectionStub connection = new ConnectionStub(handler); IAgileService agileService = connection.CreateAgileService(); // Act @@ -68,6 +31,7 @@ public async Task Many_Agiles_Are_Fetched_In_Batches() Assert.NotNull(result); Assert.NotEmpty(result); Assert.Equal(totalAgileCount, result.Count); + Assert.Equal(expectedRequests, handler.RequestsReceived); } } } diff --git a/tests/YouTrackSharp.Tests/Resources/FullAgile01.json b/tests/YouTrackSharp.Tests/Resources/FullAgile01.json new file mode 100644 index 0000000..e12e627 --- /dev/null +++ b/tests/YouTrackSharp.Tests/Resources/FullAgile01.json @@ -0,0 +1,232 @@ +{ + "_comment": "Represents a full agile board (no fields are null), with FieldBasedColorCoding, IssueBasedSwimlaneSettings (and CustomFilterField)", + "projects": [ + { + "shortName": "DP1", + "name": "DemoProject1", + "id": "0-1", + "$type": "Project" + } + ], + "swimlaneSettings": { + "field": { + "customField": { + "name": "Type", + "$type": "CustomField" + }, + "presentation": "Type", + "id": "51-2", + "name": "Type", + "$type": "CustomFilterField" + }, + "values": [ + { + "name": "Feature", + "id": "Feature", + "$type": "SwimlaneValue" + } + ], + "defaultCardType": { + "name": "Task", + "id": "Task", + "$type": "SwimlaneValue" + }, + "id": "105-3", + "$type": "IssueBasedSwimlaneSettings" + }, + "estimationField": { + "name": "Estimation", + "$type": "CustomField" + }, + "sprints": [ + { + "name": "First sprint", + "id": "109-2", + "$type": "Sprint" + } + ], + "hideOrphansSwimlane": false, + "orphansAtTheTop": false, + "visibleForProjectBased": true, + "updateableByProjectBased": true, + "columnSettings": { + "field": { + "name": "State", + "$type": "CustomField" + }, + "columns": [ + { + "fieldValues": [ + { + "name": "Open", + "isResolved": true, + "id": "111-12", + "$type": "AgileColumnFieldValue" + } + ], + "ordinal": 0, + "presentation": "Open", + "wipLimit": { + "id": "205-0", + "max": "10", + "min": "3", + "column": "3", + "$type": "WIPLimit" + }, + "isResolved": true, + "id": "110-8", + "$type": "AgileColumn" + }, + { + "fieldValues": [ + { + "name": "In Progress", + "isResolved": true, + "id": "111-13", + "$type": "AgileColumnFieldValue" + } + ], + "ordinal": 1, + "presentation": "In Progress", + "wipLimit": { + "id": "205-1", + "max": "10", + "min": "3", + "column": "3", + "$type": "WIPLimit" + }, + "isResolved": true, + "id": "110-9", + "$type": "AgileColumn" + }, + { + "fieldValues": [ + { + "name": "Fixed", + "isResolved": true, + "id": "111-14", + "$type": "AgileColumnFieldValue" + } + ], + "ordinal": 2, + "presentation": "Fixed", + "wipLimit": { + "id": "205-2", + "max": "10", + "min": "3", + "column": "3", + "$type": "WIPLimit" + }, + "isResolved": true, + "id": "110-10", + "$type": "AgileColumn" + }, + { + "fieldValues": [ + { + "name": "Verified", + "isResolved": true, + "id": "111-15", + "$type": "AgileColumnFieldValue" + } + ], + "ordinal": 3, + "presentation": "Verified", + "wipLimit": { + "id": "205-3", + "max": "10", + "min": "3", + "column": "3", + "$type": "WIPLimit" + }, + "isResolved": true, + "id": "110-11", + "$type": "AgileColumn" + } + ], + "id": "108-2", + "$type": "ColumnSettings" + }, + "colorCoding": { + "prototype": { + "name": "Type", + "$type": "CustomField" + }, + "id": "108-5", + "$type": "FieldBasedColorCoding" + }, + "currentSprint": { + "name": "First sprint", + "id": "109-2", + "$type": "Sprint" + }, + "originalEstimationField": { + "name": "OriginalEstimationField", + "$type": "CustomField" + }, + "sprintsSettings": { + "explicitQuery": "project: DemoProject1", + "isExplicit": false, + "disableSprints": false, + "hideSubtasksOfCards": false, + "cardOnSeveralSprints": false, + "defaultSprint": { + "name": "First sprint", + "id": "109-2", + "$type": "Sprint" + }, + "sprintSyncField": { + "name": "State", + "$type": "CustomField" + }, + "id": "108-2", + "$type": "SprintsSettings" + }, + "visibleFor": { + "allUsersGroup" : false, + "icon" : "String", + "name" : "String", + "ringId" : "String", + "teamForProject" : { + "shortName": "DP1", + "name": "DemoProject1", + "id": "0-1", + "$type": "Project" + }, + "usersCount" : 2, + "id" : "String", + "$type" : "UserGroup" + }, + "updateableBy": { + "allUsersGroup" : false, + "icon" : "String", + "name" : "String", + "ringId" : "String", + "teamForProject" : { + "shortName": "DP1", + "name": "DemoProject1", + "id": "0-1", + "$type": "Project" + }, + "usersCount" : 2, + "id" : "String", + "$type" : "UserGroup" + }, + "status": { + "errors": [], + "valid": true, + "hasJobs": false, + "warnings": [], + "id": "boardStatus", + "$type": "AgileStatus" + }, + "owner": { + "fullName": "Demo User 1", + "ringId": "0692a47b-3670-452e-8e73-8b166a774705", + "id": "1-2", + "$type": "User" + }, + "name": "Full Board 01", + "id": "109-1", + "$type": "Agile" +} \ No newline at end of file diff --git a/tests/YouTrackSharp.Tests/Resources/FullAgile02.json b/tests/YouTrackSharp.Tests/Resources/FullAgile02.json new file mode 100644 index 0000000..6531da2 --- /dev/null +++ b/tests/YouTrackSharp.Tests/Resources/FullAgile02.json @@ -0,0 +1,251 @@ +{ + "_comment": "Represents a full agile board (no fields are null), with ProjectBasedColorCoding, AttributeBasedSwimlaneSettings (and PredefinedFilterField)", + "projects": [ + { + "shortName": "DP1", + "name": "DemoProject1", + "id": "0-1", + "$type": "Project" + } + ], + "swimlaneSettings": { + "field": { + "presentation": "Type", + "id": "51-3", + "name": "Type", + "$type": "PredefinedFilterField" + }, + "values": [ + { + "name": "Feature 01", + "isResolved": true, + "id": "61-3", + "$type": "SwimlaneEntityAttributeValue" + }, + { + "name": "Feature 02", + "isResolved": true, + "id": "61-4", + "$type": "SwimlaneEntityAttributeValue" + }, + { + "name": "Feature 03", + "isResolved": false, + "id": "61-5", + "$type": "SwimlaneEntityAttributeValue" + } + ], + "id": "105-3", + "name": "Swimlane Settings 01", + "$type": "AttributeBasedSwimlaneSettings" + }, + "estimationField": { + "name": "Estimation", + "$type": "CustomField" + }, + "sprints": [ + { + "name": "First sprint", + "id": "109-2", + "$type": "Sprint" + } + ], + "hideOrphansSwimlane": true, + "orphansAtTheTop": true, + "visibleForProjectBased": true, + "updateableByProjectBased": true, + "columnSettings": { + "field": { + "name": "State", + "$type": "CustomField" + }, + "columns": [ + { + "fieldValues": [ + { + "name": "Open", + "isResolved": true, + "id": "111-12", + "$type": "AgileColumnFieldValue" + } + ], + "ordinal": 0, + "presentation": "Open", + "wipLimit": { + "id": "205-0", + "max": "10", + "min": "3", + "column": "3", + "$type": "WIPLimit" + }, + "isResolved": true, + "id": "110-8", + "$type": "AgileColumn" + }, + { + "fieldValues": [ + { + "name": "In Progress", + "isResolved": true, + "id": "111-13", + "$type": "AgileColumnFieldValue" + } + ], + "ordinal": 1, + "presentation": "In Progress", + "wipLimit": { + "id": "205-1", + "max": "10", + "min": "3", + "column": "3", + "$type": "WIPLimit" + }, + "isResolved": true, + "id": "110-9", + "$type": "AgileColumn" + }, + { + "fieldValues": [ + { + "name": "Fixed", + "isResolved": true, + "id": "111-14", + "$type": "AgileColumnFieldValue" + } + ], + "ordinal": 2, + "presentation": "Fixed", + "wipLimit": { + "id": "205-2", + "max": "10", + "min": "3", + "column": "3", + "$type": "WIPLimit" + }, + "isResolved": true, + "id": "110-10", + "$type": "AgileColumn" + }, + { + "fieldValues": [ + { + "name": "Verified", + "isResolved": true, + "id": "111-15", + "$type": "AgileColumnFieldValue" + } + ], + "ordinal": 3, + "presentation": "Verified", + "wipLimit": { + "id": "205-3", + "max": "10", + "min": "3", + "column": "3", + "$type": "WIPLimit" + }, + "isResolved": true, + "id": "110-11", + "$type": "AgileColumn" + } + ], + "id": "108-2", + "$type": "ColumnSettings" + }, + "colorCoding": { + "id": "108-5", + "projectColors": [ + { + "id": "120-1", + "project": { + "shortName": "DP1", + "name": "DemoProject1", + "id": "0-1", + "$type": "Project" + }, + "color": { + "id": "130-1", + "background": "White", + "foreground": "Black", + "$type": "FieldStyle" + }, + "$type": "ProjectColor" + } + ], + "$type": "ProjectBasedColorCoding" + }, + "currentSprint": { + "name": "First sprint", + "id": "109-2", + "$type": "Sprint" + }, + "originalEstimationField": { + "name": "OriginalEstimationField", + "$type": "CustomField" + }, + "sprintsSettings": { + "explicitQuery": "project: DemoProject1", + "isExplicit": false, + "disableSprints": false, + "hideSubtasksOfCards": false, + "cardOnSeveralSprints": false, + "defaultSprint": { + "name": "First sprint", + "id": "109-2", + "$type": "Sprint" + }, + "sprintSyncField": { + "name": "State", + "$type": "CustomField" + }, + "id": "108-2", + "$type": "SprintsSettings" + }, + "visibleFor": { + "allUsersGroup" : false, + "icon" : "String", + "name" : "String", + "ringId" : "String", + "teamForProject" : { + "shortName": "DP1", + "name": "DemoProject1", + "id": "0-1", + "$type": "Project" + }, + "usersCount" : 2, + "id" : "String", + "$type" : "UserGroup" + }, + "updateableBy": { + "allUsersGroup" : false, + "icon" : "String", + "name" : "String", + "ringId" : "String", + "teamForProject" : { + "shortName": "DP1", + "name": "DemoProject1", + "id": "0-1", + "$type": "Project" + }, + "usersCount" : 2, + "id" : "String", + "$type" : "UserGroup" + }, + "status": { + "errors": [], + "valid": true, + "hasJobs": false, + "warnings": [], + "id": "boardStatus", + "$type": "AgileStatus" + }, + "owner": { + "fullName": "Demo User 1", + "ringId": "0692a47b-3670-452e-8e73-8b166a774705", + "id": "1-2", + "$type": "User" + }, + "name": "Full Board 02", + "id": "109-2", + "$type": "Agile" +} \ No newline at end of file diff --git a/tests/YouTrackSharp.Tests/YouTrackSharp.Tests.csproj b/tests/YouTrackSharp.Tests/YouTrackSharp.Tests.csproj index 26b6aff..3802e6f 100644 --- a/tests/YouTrackSharp.Tests/YouTrackSharp.Tests.csproj +++ b/tests/YouTrackSharp.Tests/YouTrackSharp.Tests.csproj @@ -19,7 +19,7 @@ - - + + \ No newline at end of file