diff --git a/src/main/java/com/crowdin/client/Client.java b/src/main/java/com/crowdin/client/Client.java
index a2776b5b..1011ba27 100644
--- a/src/main/java/com/crowdin/client/Client.java
+++ b/src/main/java/com/crowdin/client/Client.java
@@ -1,5 +1,6 @@
package com.crowdin.client;
+import com.crowdin.client.advisors.AdvisorsApi;
import com.crowdin.client.ai.AIApi;
import com.crowdin.client.applications.ApplicationsApi;
import com.crowdin.client.branches.BranchesApi;
@@ -17,6 +18,8 @@
import com.crowdin.client.languages.LanguagesApi;
import com.crowdin.client.machinetranslationengines.MachineTranslationEnginesApi;
import com.crowdin.client.notifications.NotificationsApi;
+import com.crowdin.client.organization.OrganizationApi;
+import com.crowdin.client.placeholders.PlaceholdersApi;
import com.crowdin.client.projectsgroups.ProjectsGroupsApi;
import com.crowdin.client.reports.ReportsApi;
import com.crowdin.client.screenshots.ScreenshotsApi;
@@ -78,6 +81,9 @@ public class Client extends CrowdinApi {
private final AIApi aiApi;
private final StringCorrectionsApi stringCorrectionsApi;
private final StyleGuidesApi styleGuidesApi;
+ private final AdvisorsApi advisorsApi;
+ private final PlaceholdersApi placeholdersApi;
+ private final OrganizationApi organizationApi;
public Client(Credentials credentials) {
super(credentials);
@@ -116,6 +122,9 @@ public Client(Credentials credentials) {
this.aiApi = new AIApi(credentials);
this.stringCorrectionsApi = new StringCorrectionsApi(credentials);
this.styleGuidesApi = new StyleGuidesApi(credentials);
+ this.advisorsApi = new AdvisorsApi(credentials);
+ this.placeholdersApi = new PlaceholdersApi(credentials);
+ this.organizationApi = new OrganizationApi(credentials);
}
public Client(Credentials credentials, ClientConfig clientConfig) {
@@ -155,6 +164,9 @@ public Client(Credentials credentials, ClientConfig clientConfig) {
this.aiApi = new AIApi(credentials, clientConfig);
this.stringCorrectionsApi = new StringCorrectionsApi(credentials, clientConfig);
this.styleGuidesApi = new StyleGuidesApi(credentials, clientConfig);
+ this.advisorsApi = new AdvisorsApi(credentials, clientConfig);
+ this.placeholdersApi = new PlaceholdersApi(credentials, clientConfig);
+ this.organizationApi = new OrganizationApi(credentials, clientConfig);
}
}
diff --git a/src/main/java/com/crowdin/client/advisors/AdvisorsApi.java b/src/main/java/com/crowdin/client/advisors/AdvisorsApi.java
new file mode 100644
index 00000000..05f01cbb
--- /dev/null
+++ b/src/main/java/com/crowdin/client/advisors/AdvisorsApi.java
@@ -0,0 +1,124 @@
+package com.crowdin.client.advisors;
+
+import com.crowdin.client.advisors.model.AdvisorCheck;
+import com.crowdin.client.advisors.model.AdvisorCheckResponseObject;
+import com.crowdin.client.advisors.model.AdvisorInsight;
+import com.crowdin.client.advisors.model.AdvisorInsightResponseList;
+import com.crowdin.client.advisors.model.AdvisorInsightResponseObject;
+import com.crowdin.client.advisors.model.ListAdvisorInsightsParams;
+import com.crowdin.client.advisors.model.RunAdvisorCheckRequest;
+import com.crowdin.client.advisors.model.UpsertAdvisorInsightRequest;
+import com.crowdin.client.core.CrowdinApi;
+import com.crowdin.client.core.http.HttpRequestConfig;
+import com.crowdin.client.core.http.exceptions.HttpBadRequestException;
+import com.crowdin.client.core.http.exceptions.HttpException;
+import com.crowdin.client.core.model.ClientConfig;
+import com.crowdin.client.core.model.Credentials;
+import com.crowdin.client.core.model.PatchRequest;
+import com.crowdin.client.core.model.ResponseList;
+import com.crowdin.client.core.model.ResponseObject;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+public class AdvisorsApi extends CrowdinApi {
+ public AdvisorsApi(Credentials credentials) {
+ super(credentials);
+ }
+
+ public AdvisorsApi(Credentials credentials, ClientConfig clientConfig) {
+ super(credentials, clientConfig);
+ }
+
+ /**
+ * @param projectId project identifier
+ * @param request request object
+ * @return advisor check job descriptor
+ * @see
+ */
+ public ResponseObject runAdvisorCheck(Long projectId, RunAdvisorCheckRequest request) throws HttpException, HttpBadRequestException {
+ AdvisorCheckResponseObject advisorCheckResponseObject = this.httpClient.post(this.url + "/projects/" + projectId + "/advisors/checks", request, new HttpRequestConfig(), AdvisorCheckResponseObject.class);
+ return ResponseObject.of(advisorCheckResponseObject.getData());
+ }
+
+ /**
+ * @param projectId project identifier
+ * @param checkId advisor check identifier
+ * @return advisor check job descriptor
+ * @see
+ */
+ public ResponseObject getAdvisorCheck(Long projectId, String checkId) throws HttpException, HttpBadRequestException {
+ AdvisorCheckResponseObject advisorCheckResponseObject = this.httpClient.get(this.url + "/projects/" + projectId + "/advisors/checks/" + checkId, new HttpRequestConfig(), AdvisorCheckResponseObject.class);
+ return ResponseObject.of(advisorCheckResponseObject.getData());
+ }
+
+ /**
+ * @param projectId project identifier
+ * @param params query params
+ * @return list of advisor insights; the pagination is an {@link com.crowdin.client.advisors.model.AdvisorInsightPagination} which also carries the total count
+ * @see
+ */
+ public ResponseList listAdvisorInsights(Long projectId, ListAdvisorInsightsParams params) throws HttpException, HttpBadRequestException {
+ ListAdvisorInsightsParams query = Optional.ofNullable(params).orElse(new ListAdvisorInsightsParams());
+ Map> queryParams = HttpRequestConfig.buildUrlParams(
+ "limit", Optional.ofNullable(query.getLimit()),
+ "offset", Optional.ofNullable(query.getOffset()),
+ "isDismissed", Optional.ofNullable(query.getIsDismissed()),
+ "status", Optional.ofNullable(
+ query.getStatus() == null ? null : query.getStatus().stream()
+ .map(status -> status.to(status))
+ .collect(Collectors.joining(","))
+ ),
+ "outcome", Optional.ofNullable(
+ query.getOutcome() == null ? null : query.getOutcome().stream()
+ .map(outcome -> outcome.to(outcome))
+ .collect(Collectors.joining(","))
+ )
+ );
+ AdvisorInsightResponseList advisorInsightResponseList = this.httpClient.get(this.url + "/projects/" + projectId + "/advisors/insights", new HttpRequestConfig(queryParams), AdvisorInsightResponseList.class);
+ return AdvisorInsightResponseList.to(advisorInsightResponseList);
+ }
+
+ /**
+ * @param projectId project identifier
+ * @param insightId insight identifier
+ * @param request request object (only the {@code /isDismissed} path is supported)
+ * @return updated advisor insight
+ * @see
+ */
+ public ResponseObject editAdvisorInsight(Long projectId, Long insightId, List request) throws HttpException, HttpBadRequestException {
+ AdvisorInsightResponseObject advisorInsightResponseObject = this.httpClient.patch(this.url + "/projects/" + projectId + "/advisors/insights/" + insightId, request, new HttpRequestConfig(), AdvisorInsightResponseObject.class);
+ return ResponseObject.of(advisorInsightResponseObject.getData());
+ }
+
+ /**
+ * Publishes the result of an application's {@code advisor-inspector} module check.
+ * Subsequent calls for the same module overwrite the previous insight.
+ *
+ * @param projectId project identifier
+ * @param applicationIdentifier application identifier
+ * @param moduleKey application module key
+ * @param request request object
+ * @see
+ */
+ public void upsertApplicationAdvisorInsight(Long projectId, String applicationIdentifier, String moduleKey, UpsertAdvisorInsightRequest request) throws HttpException, HttpBadRequestException {
+ this.httpClient.put(this.url + "/projects/" + projectId + "/applications/" + applicationIdentifier + "/modules/" + moduleKey + "/advisors/insights", request, new HttpRequestConfig(), Void.class);
+ }
+}
diff --git a/src/main/java/com/crowdin/client/advisors/model/AdvisorAiMode.java b/src/main/java/com/crowdin/client/advisors/model/AdvisorAiMode.java
new file mode 100644
index 00000000..3bd90e0d
--- /dev/null
+++ b/src/main/java/com/crowdin/client/advisors/model/AdvisorAiMode.java
@@ -0,0 +1,21 @@
+package com.crowdin.client.advisors.model;
+
+import com.crowdin.client.core.model.EnumConverter;
+
+public enum AdvisorAiMode implements EnumConverter {
+ AUTO, ALL;
+
+ public static AdvisorAiMode from(String value) {
+ for (AdvisorAiMode v : AdvisorAiMode.values()) {
+ if (v.name().equalsIgnoreCase(value)) {
+ return v;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String to(AdvisorAiMode value) {
+ return value.name().toLowerCase();
+ }
+}
diff --git a/src/main/java/com/crowdin/client/advisors/model/AdvisorAiRun.java b/src/main/java/com/crowdin/client/advisors/model/AdvisorAiRun.java
new file mode 100644
index 00000000..b68ebb74
--- /dev/null
+++ b/src/main/java/com/crowdin/client/advisors/model/AdvisorAiRun.java
@@ -0,0 +1,13 @@
+package com.crowdin.client.advisors.model;
+
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+public class AdvisorAiRun {
+
+ private AdvisorAiMode mode;
+ private Long promptId;
+ private Date at;
+}
diff --git a/src/main/java/com/crowdin/client/advisors/model/AdvisorCheck.java b/src/main/java/com/crowdin/client/advisors/model/AdvisorCheck.java
new file mode 100644
index 00000000..3f895512
--- /dev/null
+++ b/src/main/java/com/crowdin/client/advisors/model/AdvisorCheck.java
@@ -0,0 +1,21 @@
+package com.crowdin.client.advisors.model;
+
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+public class AdvisorCheck {
+
+ private String identifier;
+ /**
+ * Status of the underlying job (e.g. created, in_progress, done, failed)
+ */
+ private String status;
+ private Integer progress;
+ private AdvisorCheckAttributes attributes;
+ private Date createdAt;
+ private Date updatedAt;
+ private Date startedAt;
+ private Date finishedAt;
+}
diff --git a/src/main/java/com/crowdin/client/advisors/model/AdvisorCheckAttributes.java b/src/main/java/com/crowdin/client/advisors/model/AdvisorCheckAttributes.java
new file mode 100644
index 00000000..739a007b
--- /dev/null
+++ b/src/main/java/com/crowdin/client/advisors/model/AdvisorCheckAttributes.java
@@ -0,0 +1,12 @@
+package com.crowdin.client.advisors.model;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class AdvisorCheckAttributes {
+
+ private String category;
+ private List inspectors;
+}
diff --git a/src/main/java/com/crowdin/client/advisors/model/AdvisorCheckInspector.java b/src/main/java/com/crowdin/client/advisors/model/AdvisorCheckInspector.java
new file mode 100644
index 00000000..ba02890f
--- /dev/null
+++ b/src/main/java/com/crowdin/client/advisors/model/AdvisorCheckInspector.java
@@ -0,0 +1,9 @@
+package com.crowdin.client.advisors.model;
+
+import lombok.Data;
+
+@Data
+public class AdvisorCheckInspector {
+
+ private String key;
+}
diff --git a/src/main/java/com/crowdin/client/advisors/model/AdvisorCheckResponseObject.java b/src/main/java/com/crowdin/client/advisors/model/AdvisorCheckResponseObject.java
new file mode 100644
index 00000000..608e2daa
--- /dev/null
+++ b/src/main/java/com/crowdin/client/advisors/model/AdvisorCheckResponseObject.java
@@ -0,0 +1,9 @@
+package com.crowdin.client.advisors.model;
+
+import lombok.Data;
+
+@Data
+public class AdvisorCheckResponseObject {
+
+ private AdvisorCheck data;
+}
diff --git a/src/main/java/com/crowdin/client/advisors/model/AdvisorInsight.java b/src/main/java/com/crowdin/client/advisors/model/AdvisorInsight.java
new file mode 100644
index 00000000..f6bdefc4
--- /dev/null
+++ b/src/main/java/com/crowdin/client/advisors/model/AdvisorInsight.java
@@ -0,0 +1,28 @@
+package com.crowdin.client.advisors.model;
+
+import com.crowdin.client.core.http.impl.json.EmptyArrayToNullDeserializer;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import lombok.Data;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+@Data
+public class AdvisorInsight {
+
+ private Long id;
+ private String inspectorKey;
+ private String category;
+ private Boolean isDismissed;
+ private AdvisorInsightStatus status;
+ private AdvisorInsightOutcome outcome;
+ private AdvisorInsightSeverity severity;
+ private AdvisorRefreshPolicy refreshPolicy;
+ private List metrics;
+ private List recommendations;
+ private Date checkedAt;
+ private AdvisorAiRun lastAiRun;
+ @JsonDeserialize(using = EmptyArrayToNullDeserializer.class)
+ private Map payload;
+}
diff --git a/src/main/java/com/crowdin/client/advisors/model/AdvisorInsightMetric.java b/src/main/java/com/crowdin/client/advisors/model/AdvisorInsightMetric.java
new file mode 100644
index 00000000..73550cbc
--- /dev/null
+++ b/src/main/java/com/crowdin/client/advisors/model/AdvisorInsightMetric.java
@@ -0,0 +1,17 @@
+package com.crowdin.client.advisors.model;
+
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+public class AdvisorInsightMetric {
+
+ private String key;
+ private Double value;
+ private AdvisorMetricUnit unit;
+ private Double threshold;
+ private AdvisorMetricTone tone;
+ private AdvisorMetricSource source;
+ private Date checkedAt;
+}
diff --git a/src/main/java/com/crowdin/client/advisors/model/AdvisorInsightOutcome.java b/src/main/java/com/crowdin/client/advisors/model/AdvisorInsightOutcome.java
new file mode 100644
index 00000000..90fcc548
--- /dev/null
+++ b/src/main/java/com/crowdin/client/advisors/model/AdvisorInsightOutcome.java
@@ -0,0 +1,21 @@
+package com.crowdin.client.advisors.model;
+
+import com.crowdin.client.core.model.EnumConverter;
+
+public enum AdvisorInsightOutcome implements EnumConverter {
+ FLAGGED, CLEAR, NOT_APPLICABLE;
+
+ public static AdvisorInsightOutcome from(String value) {
+ for (AdvisorInsightOutcome v : AdvisorInsightOutcome.values()) {
+ if (v.name().equalsIgnoreCase(value)) {
+ return v;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String to(AdvisorInsightOutcome value) {
+ return value.name().toLowerCase();
+ }
+}
diff --git a/src/main/java/com/crowdin/client/advisors/model/AdvisorInsightPagination.java b/src/main/java/com/crowdin/client/advisors/model/AdvisorInsightPagination.java
new file mode 100644
index 00000000..55e2576e
--- /dev/null
+++ b/src/main/java/com/crowdin/client/advisors/model/AdvisorInsightPagination.java
@@ -0,0 +1,18 @@
+package com.crowdin.client.advisors.model;
+
+import com.crowdin.client.core.model.Pagination;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+/**
+ * Pagination returned by List Advisor Insights, which additionally carries the {@link #total} number of insights.
+ * {@link com.crowdin.client.core.model.ResponseList} exposes it as {@link Pagination}; cast it to this class to read the total.
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class AdvisorInsightPagination extends Pagination {
+
+ private Integer total;
+}
diff --git a/src/main/java/com/crowdin/client/advisors/model/AdvisorInsightRecommendation.java b/src/main/java/com/crowdin/client/advisors/model/AdvisorInsightRecommendation.java
new file mode 100644
index 00000000..85ceb8fc
--- /dev/null
+++ b/src/main/java/com/crowdin/client/advisors/model/AdvisorInsightRecommendation.java
@@ -0,0 +1,16 @@
+package com.crowdin.client.advisors.model;
+
+import com.crowdin.client.core.http.impl.json.EmptyArrayToNullDeserializer;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import lombok.Data;
+
+import java.util.Map;
+
+@Data
+public class AdvisorInsightRecommendation {
+
+ private String id;
+ private Boolean primary;
+ @JsonDeserialize(using = EmptyArrayToNullDeserializer.class)
+ private Map params;
+}
diff --git a/src/main/java/com/crowdin/client/advisors/model/AdvisorInsightResponseList.java b/src/main/java/com/crowdin/client/advisors/model/AdvisorInsightResponseList.java
new file mode 100644
index 00000000..38b19453
--- /dev/null
+++ b/src/main/java/com/crowdin/client/advisors/model/AdvisorInsightResponseList.java
@@ -0,0 +1,25 @@
+package com.crowdin.client.advisors.model;
+
+import com.crowdin.client.core.model.ResponseList;
+import com.crowdin.client.core.model.ResponseObject;
+import lombok.Data;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Data
+public class AdvisorInsightResponseList {
+
+ private List data;
+ private AdvisorInsightPagination pagination;
+
+ public static ResponseList to(AdvisorInsightResponseList advisorInsightResponseList) {
+ return ResponseList.of(
+ advisorInsightResponseList.getData().stream()
+ .map(AdvisorInsightResponseObject::getData)
+ .map(ResponseObject::of)
+ .collect(Collectors.toList()),
+ advisorInsightResponseList.getPagination()
+ );
+ }
+}
diff --git a/src/main/java/com/crowdin/client/advisors/model/AdvisorInsightResponseObject.java b/src/main/java/com/crowdin/client/advisors/model/AdvisorInsightResponseObject.java
new file mode 100644
index 00000000..19d9cc27
--- /dev/null
+++ b/src/main/java/com/crowdin/client/advisors/model/AdvisorInsightResponseObject.java
@@ -0,0 +1,9 @@
+package com.crowdin.client.advisors.model;
+
+import lombok.Data;
+
+@Data
+public class AdvisorInsightResponseObject {
+
+ private AdvisorInsight data;
+}
diff --git a/src/main/java/com/crowdin/client/advisors/model/AdvisorInsightSeverity.java b/src/main/java/com/crowdin/client/advisors/model/AdvisorInsightSeverity.java
new file mode 100644
index 00000000..86121620
--- /dev/null
+++ b/src/main/java/com/crowdin/client/advisors/model/AdvisorInsightSeverity.java
@@ -0,0 +1,21 @@
+package com.crowdin.client.advisors.model;
+
+import com.crowdin.client.core.model.EnumConverter;
+
+public enum AdvisorInsightSeverity implements EnumConverter {
+ HIGH, MEDIUM, LOW;
+
+ public static AdvisorInsightSeverity from(String value) {
+ for (AdvisorInsightSeverity v : AdvisorInsightSeverity.values()) {
+ if (v.name().equalsIgnoreCase(value)) {
+ return v;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String to(AdvisorInsightSeverity value) {
+ return value.name().toLowerCase();
+ }
+}
diff --git a/src/main/java/com/crowdin/client/advisors/model/AdvisorInsightStatus.java b/src/main/java/com/crowdin/client/advisors/model/AdvisorInsightStatus.java
new file mode 100644
index 00000000..196c6265
--- /dev/null
+++ b/src/main/java/com/crowdin/client/advisors/model/AdvisorInsightStatus.java
@@ -0,0 +1,21 @@
+package com.crowdin.client.advisors.model;
+
+import com.crowdin.client.core.model.EnumConverter;
+
+public enum AdvisorInsightStatus implements EnumConverter {
+ PENDING, CHECKING, OUTDATED, DONE;
+
+ public static AdvisorInsightStatus from(String value) {
+ for (AdvisorInsightStatus v : AdvisorInsightStatus.values()) {
+ if (v.name().equalsIgnoreCase(value)) {
+ return v;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String to(AdvisorInsightStatus value) {
+ return value.name().toLowerCase();
+ }
+}
diff --git a/src/main/java/com/crowdin/client/advisors/model/AdvisorInspectorOptions.java b/src/main/java/com/crowdin/client/advisors/model/AdvisorInspectorOptions.java
new file mode 100644
index 00000000..9fb94562
--- /dev/null
+++ b/src/main/java/com/crowdin/client/advisors/model/AdvisorInspectorOptions.java
@@ -0,0 +1,10 @@
+package com.crowdin.client.advisors.model;
+
+import lombok.Data;
+
+@Data
+public class AdvisorInspectorOptions {
+
+ private AdvisorAiMode mode;
+ private Long promptId;
+}
diff --git a/src/main/java/com/crowdin/client/advisors/model/AdvisorMetricSource.java b/src/main/java/com/crowdin/client/advisors/model/AdvisorMetricSource.java
new file mode 100644
index 00000000..acebf846
--- /dev/null
+++ b/src/main/java/com/crowdin/client/advisors/model/AdvisorMetricSource.java
@@ -0,0 +1,21 @@
+package com.crowdin.client.advisors.model;
+
+import com.crowdin.client.core.model.EnumConverter;
+
+public enum AdvisorMetricSource implements EnumConverter {
+ DETERMINISTIC, AI, APP;
+
+ public static AdvisorMetricSource from(String value) {
+ for (AdvisorMetricSource v : AdvisorMetricSource.values()) {
+ if (v.name().equalsIgnoreCase(value)) {
+ return v;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String to(AdvisorMetricSource value) {
+ return value.name().toLowerCase();
+ }
+}
diff --git a/src/main/java/com/crowdin/client/advisors/model/AdvisorMetricTone.java b/src/main/java/com/crowdin/client/advisors/model/AdvisorMetricTone.java
new file mode 100644
index 00000000..0ebdf7fd
--- /dev/null
+++ b/src/main/java/com/crowdin/client/advisors/model/AdvisorMetricTone.java
@@ -0,0 +1,21 @@
+package com.crowdin.client.advisors.model;
+
+import com.crowdin.client.core.model.EnumConverter;
+
+public enum AdvisorMetricTone implements EnumConverter {
+ DEFAULT, SUCCESS, DANGER;
+
+ public static AdvisorMetricTone from(String value) {
+ for (AdvisorMetricTone v : AdvisorMetricTone.values()) {
+ if (v.name().equalsIgnoreCase(value)) {
+ return v;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String to(AdvisorMetricTone value) {
+ return value.name().toLowerCase();
+ }
+}
diff --git a/src/main/java/com/crowdin/client/advisors/model/AdvisorMetricUnit.java b/src/main/java/com/crowdin/client/advisors/model/AdvisorMetricUnit.java
new file mode 100644
index 00000000..9b0aa48a
--- /dev/null
+++ b/src/main/java/com/crowdin/client/advisors/model/AdvisorMetricUnit.java
@@ -0,0 +1,21 @@
+package com.crowdin.client.advisors.model;
+
+import com.crowdin.client.core.model.EnumConverter;
+
+public enum AdvisorMetricUnit implements EnumConverter {
+ PERCENT, COUNT;
+
+ public static AdvisorMetricUnit from(String value) {
+ for (AdvisorMetricUnit v : AdvisorMetricUnit.values()) {
+ if (v.name().equalsIgnoreCase(value)) {
+ return v;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String to(AdvisorMetricUnit value) {
+ return value.name().toLowerCase();
+ }
+}
diff --git a/src/main/java/com/crowdin/client/advisors/model/AdvisorRefreshPolicy.java b/src/main/java/com/crowdin/client/advisors/model/AdvisorRefreshPolicy.java
new file mode 100644
index 00000000..0f00b0a0
--- /dev/null
+++ b/src/main/java/com/crowdin/client/advisors/model/AdvisorRefreshPolicy.java
@@ -0,0 +1,21 @@
+package com.crowdin.client.advisors.model;
+
+import com.crowdin.client.core.model.EnumConverter;
+
+public enum AdvisorRefreshPolicy implements EnumConverter {
+ HOURLY, DAILY;
+
+ public static AdvisorRefreshPolicy from(String value) {
+ for (AdvisorRefreshPolicy v : AdvisorRefreshPolicy.values()) {
+ if (v.name().equalsIgnoreCase(value)) {
+ return v;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String to(AdvisorRefreshPolicy value) {
+ return value.name().toLowerCase();
+ }
+}
diff --git a/src/main/java/com/crowdin/client/advisors/model/ListAdvisorInsightsParams.java b/src/main/java/com/crowdin/client/advisors/model/ListAdvisorInsightsParams.java
new file mode 100644
index 00000000..63fdbbcc
--- /dev/null
+++ b/src/main/java/com/crowdin/client/advisors/model/ListAdvisorInsightsParams.java
@@ -0,0 +1,16 @@
+package com.crowdin.client.advisors.model;
+
+import com.crowdin.client.core.model.Pagination;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.List;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ListAdvisorInsightsParams extends Pagination {
+
+ private Boolean isDismissed;
+ private List status;
+ private List outcome;
+}
diff --git a/src/main/java/com/crowdin/client/advisors/model/RunAdvisorCheckInspector.java b/src/main/java/com/crowdin/client/advisors/model/RunAdvisorCheckInspector.java
new file mode 100644
index 00000000..bb144300
--- /dev/null
+++ b/src/main/java/com/crowdin/client/advisors/model/RunAdvisorCheckInspector.java
@@ -0,0 +1,10 @@
+package com.crowdin.client.advisors.model;
+
+import lombok.Data;
+
+@Data
+public class RunAdvisorCheckInspector {
+
+ private String key;
+ private AdvisorInspectorOptions options;
+}
diff --git a/src/main/java/com/crowdin/client/advisors/model/RunAdvisorCheckRequest.java b/src/main/java/com/crowdin/client/advisors/model/RunAdvisorCheckRequest.java
new file mode 100644
index 00000000..691c14cb
--- /dev/null
+++ b/src/main/java/com/crowdin/client/advisors/model/RunAdvisorCheckRequest.java
@@ -0,0 +1,15 @@
+package com.crowdin.client.advisors.model;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * At most one of {@link #category} or {@link #inspectors} may be set; if neither is set, every inspector is re-checked.
+ */
+@Data
+public class RunAdvisorCheckRequest {
+
+ private String category;
+ private List inspectors;
+}
diff --git a/src/main/java/com/crowdin/client/advisors/model/UpsertAdvisorInsightRequest.java b/src/main/java/com/crowdin/client/advisors/model/UpsertAdvisorInsightRequest.java
new file mode 100644
index 00000000..2f7fc4eb
--- /dev/null
+++ b/src/main/java/com/crowdin/client/advisors/model/UpsertAdvisorInsightRequest.java
@@ -0,0 +1,17 @@
+package com.crowdin.client.advisors.model;
+
+import lombok.Data;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+@Data
+public class UpsertAdvisorInsightRequest {
+
+ private AdvisorInsightOutcome outcome;
+ private Date checkedAt;
+ private List metrics;
+ private List recommendations;
+ private Map payload;
+}
diff --git a/src/main/java/com/crowdin/client/core/http/impl/json/LanguageTranslationsDeserializer.java b/src/main/java/com/crowdin/client/core/http/impl/json/LanguageTranslationsDeserializer.java
index 6429d6a6..39eef6be 100644
--- a/src/main/java/com/crowdin/client/core/http/impl/json/LanguageTranslationsDeserializer.java
+++ b/src/main/java/com/crowdin/client/core/http/impl/json/LanguageTranslationsDeserializer.java
@@ -7,9 +7,11 @@
import com.crowdin.client.stringtranslations.model.PluralLanguageTranslations;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
@@ -19,6 +21,8 @@
public class LanguageTranslationsDeserializer extends JsonDeserializer {
+ private static final String CROWDIN_CONTENT_TYPE_PREFIX = "application/vnd.crowdin.";
+
private final ObjectMapper objectMapper;
public LanguageTranslationsDeserializer(ObjectMapper objectMapper) {
@@ -39,7 +43,7 @@ public LanguageTranslations deserialize(JsonParser p, DeserializationContext ctx
case "\"application/vnd.crowdin.text+plural\"": return this.objectMapper.readValue(treeNode.toString(), PluralLanguageTranslations.class);
case "\"application/vnd.crowdin.text+icu\"": return this.objectMapper.readValue(treeNode.toString(), ICULanguageTranslations.class);
default:
- if (isAsset(fields)) {
+ if (isAsset(fields, treeNode.get("contentType"))) {
return this.objectMapper.readValue(treeNode.toString(), AssetLanguageTranslations.class);
}
throw new RuntimeException("Wrong type: " + contentType);
@@ -51,10 +55,18 @@ public LanguageTranslations deserialize(JsonParser p, DeserializationContext ctx
/**
* Asset translations report the MIME type of the asset (e.g. {@code image/jpeg}) as {@code contentType}, so they
- * are recognized by their shape: a {@code url} and neither {@code text} nor {@code plurals}.
+ * are recognized by their shape: a non-empty string {@code contentType} that is not one of Crowdin's own
+ * {@code application/vnd.crowdin.*} text types, a {@code url}, and neither {@code text} nor {@code plurals}.
* Any other unknown content type is rejected rather than read as one of the known text types.
*/
- private static boolean isAsset(List fields) {
+ private static boolean isAsset(List fields, TreeNode contentTypeNode) {
+ if (contentTypeNode == null || contentTypeNode.asToken() != JsonToken.VALUE_STRING) {
+ return false;
+ }
+ String contentType = ((JsonNode) contentTypeNode).textValue();
+ if (contentType == null || contentType.trim().isEmpty() || contentType.startsWith(CROWDIN_CONTENT_TYPE_PREFIX)) {
+ return false;
+ }
return fields.contains("url") && !fields.contains("text") && !fields.contains("plurals");
}
}
diff --git a/src/main/java/com/crowdin/client/organization/OrganizationApi.java b/src/main/java/com/crowdin/client/organization/OrganizationApi.java
new file mode 100644
index 00000000..6c9e4e25
--- /dev/null
+++ b/src/main/java/com/crowdin/client/organization/OrganizationApi.java
@@ -0,0 +1,102 @@
+package com.crowdin.client.organization;
+
+import com.crowdin.client.core.CrowdinApi;
+import com.crowdin.client.core.http.HttpRequestConfig;
+import com.crowdin.client.core.http.exceptions.HttpBadRequestException;
+import com.crowdin.client.core.http.exceptions.HttpException;
+import com.crowdin.client.core.model.ClientConfig;
+import com.crowdin.client.core.model.Credentials;
+import com.crowdin.client.core.model.ResponseList;
+import com.crowdin.client.core.model.ResponseObject;
+import com.crowdin.client.organization.model.CustomSpellchecker;
+import com.crowdin.client.organization.model.CustomSpellcheckerResponseList;
+import com.crowdin.client.organization.model.CustomSpellcheckerResponseObject;
+import com.crowdin.client.organization.model.ExternalQaCheck;
+import com.crowdin.client.organization.model.ExternalQaCheckResponseList;
+import com.crowdin.client.organization.model.Organization;
+import com.crowdin.client.organization.model.OrganizationAuthSettings;
+import com.crowdin.client.organization.model.OrganizationAuthSettingsResponseObject;
+import com.crowdin.client.organization.model.OrganizationResponseObject;
+
+import java.util.Map;
+import java.util.Optional;
+
+public class OrganizationApi extends CrowdinApi {
+ public OrganizationApi(Credentials credentials) {
+ super(credentials);
+ }
+
+ public OrganizationApi(Credentials credentials, ClientConfig clientConfig) {
+ super(credentials, clientConfig);
+ }
+
+ /**
+ * @return organization info
+ * @see
+ */
+ public ResponseObject getOrganization() throws HttpException, HttpBadRequestException {
+ OrganizationResponseObject organizationResponseObject = this.httpClient.get(this.url + "/organization", new HttpRequestConfig(), OrganizationResponseObject.class);
+ return ResponseObject.of(organizationResponseObject.getData());
+ }
+
+ /**
+ * @return organization authentication settings
+ * @see
+ */
+ public ResponseObject getOrganizationAuthSettings() throws HttpException, HttpBadRequestException {
+ OrganizationAuthSettingsResponseObject organizationAuthSettingsResponseObject = this.httpClient.get(this.url + "/organization/auth-settings", new HttpRequestConfig(), OrganizationAuthSettingsResponseObject.class);
+ return ResponseObject.of(organizationAuthSettingsResponseObject.getData());
+ }
+
+ /**
+ * @param limit maximum number of items to retrieve (default 25)
+ * @param offset starting offset in the collection (default 0)
+ * @return list of custom spellcheckers
+ * @see
+ */
+ public ResponseList listCustomSpellcheckers(Integer limit, Integer offset) throws HttpException, HttpBadRequestException {
+ Map> queryParams = HttpRequestConfig.buildUrlParams(
+ "limit", Optional.ofNullable(limit),
+ "offset", Optional.ofNullable(offset)
+ );
+ CustomSpellcheckerResponseList customSpellcheckerResponseList = this.httpClient.get(this.url + "/custom-spellcheckers", new HttpRequestConfig(queryParams), CustomSpellcheckerResponseList.class);
+ return CustomSpellcheckerResponseList.to(customSpellcheckerResponseList);
+ }
+
+ /**
+ * @param customSpellcheckerId custom spellchecker identifier
+ * @return custom spellchecker
+ * @see
+ */
+ public ResponseObject getCustomSpellchecker(Long customSpellcheckerId) throws HttpException, HttpBadRequestException {
+ CustomSpellcheckerResponseObject customSpellcheckerResponseObject = this.httpClient.get(this.url + "/custom-spellcheckers/" + customSpellcheckerId, new HttpRequestConfig(), CustomSpellcheckerResponseObject.class);
+ return ResponseObject.of(customSpellcheckerResponseObject.getData());
+ }
+
+ /**
+ * @param projectId project identifier (optional filter)
+ * @param limit maximum number of items to retrieve (default 25)
+ * @param offset starting offset in the collection (default 0)
+ * @return list of external QA checks
+ * @see
+ */
+ public ResponseList listExternalQaChecks(Long projectId, Integer limit, Integer offset) throws HttpException, HttpBadRequestException {
+ Map> queryParams = HttpRequestConfig.buildUrlParams(
+ "projectId", Optional.ofNullable(projectId),
+ "limit", Optional.ofNullable(limit),
+ "offset", Optional.ofNullable(offset)
+ );
+ ExternalQaCheckResponseList externalQaCheckResponseList = this.httpClient.get(this.url + "/external-qa-checks", new HttpRequestConfig(queryParams), ExternalQaCheckResponseList.class);
+ return ExternalQaCheckResponseList.to(externalQaCheckResponseList);
+ }
+}
diff --git a/src/main/java/com/crowdin/client/organization/model/AuthMethod.java b/src/main/java/com/crowdin/client/organization/model/AuthMethod.java
new file mode 100644
index 00000000..a79e7a6c
--- /dev/null
+++ b/src/main/java/com/crowdin/client/organization/model/AuthMethod.java
@@ -0,0 +1,11 @@
+package com.crowdin.client.organization.model;
+
+import lombok.Data;
+
+@Data
+public class AuthMethod {
+
+ private String name;
+ private Boolean isEnabled;
+ private Boolean isDefault;
+}
diff --git a/src/main/java/com/crowdin/client/organization/model/CustomSpellchecker.java b/src/main/java/com/crowdin/client/organization/model/CustomSpellchecker.java
new file mode 100644
index 00000000..ef1f0d06
--- /dev/null
+++ b/src/main/java/com/crowdin/client/organization/model/CustomSpellchecker.java
@@ -0,0 +1,15 @@
+package com.crowdin.client.organization.model;
+
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+public class CustomSpellchecker {
+
+ private Long id;
+ private String name;
+ private CustomSpellcheckerConfig config;
+ private Date createdAt;
+ private Date updatedAt;
+}
diff --git a/src/main/java/com/crowdin/client/organization/model/CustomSpellcheckerConfig.java b/src/main/java/com/crowdin/client/organization/model/CustomSpellcheckerConfig.java
new file mode 100644
index 00000000..d62bc199
--- /dev/null
+++ b/src/main/java/com/crowdin/client/organization/model/CustomSpellcheckerConfig.java
@@ -0,0 +1,14 @@
+package com.crowdin.client.organization.model;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class CustomSpellcheckerConfig {
+
+ private String identifier;
+ private String key;
+ private Boolean realTimeCheckEnabled;
+ private List enabledLanguageIds;
+}
diff --git a/src/main/java/com/crowdin/client/organization/model/CustomSpellcheckerResponseList.java b/src/main/java/com/crowdin/client/organization/model/CustomSpellcheckerResponseList.java
new file mode 100644
index 00000000..efc4d831
--- /dev/null
+++ b/src/main/java/com/crowdin/client/organization/model/CustomSpellcheckerResponseList.java
@@ -0,0 +1,26 @@
+package com.crowdin.client.organization.model;
+
+import com.crowdin.client.core.model.Pagination;
+import com.crowdin.client.core.model.ResponseList;
+import com.crowdin.client.core.model.ResponseObject;
+import lombok.Data;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Data
+public class CustomSpellcheckerResponseList {
+
+ private List data;
+ private Pagination pagination;
+
+ public static ResponseList to(CustomSpellcheckerResponseList customSpellcheckerResponseList) {
+ return ResponseList.of(
+ customSpellcheckerResponseList.getData().stream()
+ .map(CustomSpellcheckerResponseObject::getData)
+ .map(ResponseObject::of)
+ .collect(Collectors.toList()),
+ customSpellcheckerResponseList.getPagination()
+ );
+ }
+}
diff --git a/src/main/java/com/crowdin/client/organization/model/CustomSpellcheckerResponseObject.java b/src/main/java/com/crowdin/client/organization/model/CustomSpellcheckerResponseObject.java
new file mode 100644
index 00000000..54f3c68b
--- /dev/null
+++ b/src/main/java/com/crowdin/client/organization/model/CustomSpellcheckerResponseObject.java
@@ -0,0 +1,9 @@
+package com.crowdin.client.organization.model;
+
+import lombok.Data;
+
+@Data
+public class CustomSpellcheckerResponseObject {
+
+ private CustomSpellchecker data;
+}
diff --git a/src/main/java/com/crowdin/client/organization/model/ExternalQaCheck.java b/src/main/java/com/crowdin/client/organization/model/ExternalQaCheck.java
new file mode 100644
index 00000000..75a028cb
--- /dev/null
+++ b/src/main/java/com/crowdin/client/organization/model/ExternalQaCheck.java
@@ -0,0 +1,20 @@
+package com.crowdin.client.organization.model;
+
+import com.crowdin.client.core.http.impl.json.EmptyArrayToNullDeserializer;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import lombok.Data;
+
+import java.util.Date;
+import java.util.Map;
+
+@Data
+public class ExternalQaCheck {
+
+ private Long id;
+ private String name;
+ private String description;
+ @JsonDeserialize(using = EmptyArrayToNullDeserializer.class)
+ private Map config;
+ private Date createdAt;
+ private Date updatedAt;
+}
diff --git a/src/main/java/com/crowdin/client/organization/model/ExternalQaCheckResponseList.java b/src/main/java/com/crowdin/client/organization/model/ExternalQaCheckResponseList.java
new file mode 100644
index 00000000..f785c41e
--- /dev/null
+++ b/src/main/java/com/crowdin/client/organization/model/ExternalQaCheckResponseList.java
@@ -0,0 +1,26 @@
+package com.crowdin.client.organization.model;
+
+import com.crowdin.client.core.model.Pagination;
+import com.crowdin.client.core.model.ResponseList;
+import com.crowdin.client.core.model.ResponseObject;
+import lombok.Data;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Data
+public class ExternalQaCheckResponseList {
+
+ private List data;
+ private Pagination pagination;
+
+ public static ResponseList to(ExternalQaCheckResponseList externalQaCheckResponseList) {
+ return ResponseList.of(
+ externalQaCheckResponseList.getData().stream()
+ .map(ExternalQaCheckResponseObject::getData)
+ .map(ResponseObject::of)
+ .collect(Collectors.toList()),
+ externalQaCheckResponseList.getPagination()
+ );
+ }
+}
diff --git a/src/main/java/com/crowdin/client/organization/model/ExternalQaCheckResponseObject.java b/src/main/java/com/crowdin/client/organization/model/ExternalQaCheckResponseObject.java
new file mode 100644
index 00000000..48dfba1b
--- /dev/null
+++ b/src/main/java/com/crowdin/client/organization/model/ExternalQaCheckResponseObject.java
@@ -0,0 +1,9 @@
+package com.crowdin.client.organization.model;
+
+import lombok.Data;
+
+@Data
+public class ExternalQaCheckResponseObject {
+
+ private ExternalQaCheck data;
+}
diff --git a/src/main/java/com/crowdin/client/organization/model/Organization.java b/src/main/java/com/crowdin/client/organization/model/Organization.java
new file mode 100644
index 00000000..67d61ea5
--- /dev/null
+++ b/src/main/java/com/crowdin/client/organization/model/Organization.java
@@ -0,0 +1,89 @@
+package com.crowdin.client.organization.model;
+
+import com.fasterxml.jackson.annotation.JsonGetter;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonSetter;
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class Organization {
+
+ private Long id;
+ private String domain;
+ private String name;
+ private String logo;
+ private String defaultLogo;
+ private String description;
+ private String internalDescription;
+ private String cname;
+ /**
+ * Whether the organization is a vendor. Set when the API returns a JSON boolean or the string {@code "true"} /
+ * {@code "false"}; {@code null} for any other value, which is kept in {@link #isVendorRaw}.
+ */
+ @JsonIgnore
+ private Boolean isVendor;
+ /**
+ * Raw {@code isVendor} value as text (e.g. {@code "false"}). The API documents {@code isVendor} as a string
+ * while its example shows a boolean, so the original value is always kept here.
+ */
+ @JsonIgnore
+ private String isVendorRaw;
+ private PublicProjectsView defaultPublicProjectsView;
+ private OrganizationPlan plan;
+ private List defaults;
+
+ /**
+ * Sets {@code isVendor} and keeps {@link #isVendorRaw} in sync ({@code "true"} / {@code "false"});
+ * {@code null} clears both fields
+ *
+ * @param isVendor whether the organization is a vendor
+ */
+ public void setIsVendor(Boolean isVendor) {
+ this.isVendor = isVendor;
+ this.isVendorRaw = isVendor == null ? null : isVendor.toString();
+ }
+
+ /**
+ * Sets the raw {@code isVendor} value exactly as given and recomputes {@link #isVendor} with the same rules as
+ * the JSON reader: {@code "true"} / {@code "false"} (case-insensitive) become a boolean, any other value leaves
+ * it {@code null}; {@code null} clears both fields
+ *
+ * @param isVendorRaw raw {@code isVendor} value
+ */
+ public void setIsVendorRaw(String isVendorRaw) {
+ this.isVendorRaw = isVendorRaw;
+ this.isVendor = parseBoolean(isVendorRaw);
+ }
+
+ /**
+ * Reads {@code isVendor} returned either as a JSON boolean or as a string
+ */
+ @JsonSetter("isVendor")
+ private void readIsVendor(JsonNode value) {
+ if (value == null || value.isNull()) {
+ this.setIsVendorRaw(null);
+ } else if (value.isBoolean()) {
+ this.setIsVendor(value.booleanValue());
+ } else {
+ this.setIsVendorRaw(value.isTextual() ? value.textValue() : value.toString());
+ }
+ }
+
+ private static Boolean parseBoolean(String value) {
+ if ("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value)) {
+ return Boolean.valueOf(value);
+ }
+ return null;
+ }
+
+ /**
+ * Writes {@code isVendor}: the boolean if known, otherwise the raw value
+ */
+ @JsonGetter("isVendor")
+ private Object writeIsVendor() {
+ return this.isVendor != null ? this.isVendor : this.isVendorRaw;
+ }
+}
diff --git a/src/main/java/com/crowdin/client/organization/model/OrganizationAuthSettings.java b/src/main/java/com/crowdin/client/organization/model/OrganizationAuthSettings.java
new file mode 100644
index 00000000..7323337f
--- /dev/null
+++ b/src/main/java/com/crowdin/client/organization/model/OrganizationAuthSettings.java
@@ -0,0 +1,13 @@
+package com.crowdin.client.organization.model;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class OrganizationAuthSettings {
+
+ private Boolean allowSignUp;
+ private Boolean twoFactorAuthentication;
+ private List authMethods;
+}
diff --git a/src/main/java/com/crowdin/client/organization/model/OrganizationAuthSettingsResponseObject.java b/src/main/java/com/crowdin/client/organization/model/OrganizationAuthSettingsResponseObject.java
new file mode 100644
index 00000000..06f6fd20
--- /dev/null
+++ b/src/main/java/com/crowdin/client/organization/model/OrganizationAuthSettingsResponseObject.java
@@ -0,0 +1,9 @@
+package com.crowdin.client.organization.model;
+
+import lombok.Data;
+
+@Data
+public class OrganizationAuthSettingsResponseObject {
+
+ private OrganizationAuthSettings data;
+}
diff --git a/src/main/java/com/crowdin/client/organization/model/OrganizationDefault.java b/src/main/java/com/crowdin/client/organization/model/OrganizationDefault.java
new file mode 100644
index 00000000..240dc6ba
--- /dev/null
+++ b/src/main/java/com/crowdin/client/organization/model/OrganizationDefault.java
@@ -0,0 +1,11 @@
+package com.crowdin.client.organization.model;
+
+import lombok.Data;
+
+@Data
+public class OrganizationDefault {
+
+ private String name;
+ private Boolean isLocked;
+ private String defaultValue;
+}
diff --git a/src/main/java/com/crowdin/client/organization/model/OrganizationPlan.java b/src/main/java/com/crowdin/client/organization/model/OrganizationPlan.java
new file mode 100644
index 00000000..4313335d
--- /dev/null
+++ b/src/main/java/com/crowdin/client/organization/model/OrganizationPlan.java
@@ -0,0 +1,11 @@
+package com.crowdin.client.organization.model;
+
+import lombok.Data;
+
+@Data
+public class OrganizationPlan {
+
+ private String name;
+ private Long wordsLimit;
+ private Long managersLimit;
+}
diff --git a/src/main/java/com/crowdin/client/organization/model/OrganizationResponseObject.java b/src/main/java/com/crowdin/client/organization/model/OrganizationResponseObject.java
new file mode 100644
index 00000000..2785e3b6
--- /dev/null
+++ b/src/main/java/com/crowdin/client/organization/model/OrganizationResponseObject.java
@@ -0,0 +1,9 @@
+package com.crowdin.client.organization.model;
+
+import lombok.Data;
+
+@Data
+public class OrganizationResponseObject {
+
+ private Organization data;
+}
diff --git a/src/main/java/com/crowdin/client/organization/model/PublicProjectsView.java b/src/main/java/com/crowdin/client/organization/model/PublicProjectsView.java
new file mode 100644
index 00000000..4fc185fb
--- /dev/null
+++ b/src/main/java/com/crowdin/client/organization/model/PublicProjectsView.java
@@ -0,0 +1,21 @@
+package com.crowdin.client.organization.model;
+
+import com.crowdin.client.core.model.EnumConverter;
+
+public enum PublicProjectsView implements EnumConverter {
+ GRID, LIST;
+
+ public static PublicProjectsView from(String value) {
+ for (PublicProjectsView v : PublicProjectsView.values()) {
+ if (v.name().equalsIgnoreCase(value)) {
+ return v;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String to(PublicProjectsView value) {
+ return value.name().toLowerCase();
+ }
+}
diff --git a/src/main/java/com/crowdin/client/placeholders/PlaceholdersApi.java b/src/main/java/com/crowdin/client/placeholders/PlaceholdersApi.java
new file mode 100644
index 00000000..7c7ffb6d
--- /dev/null
+++ b/src/main/java/com/crowdin/client/placeholders/PlaceholdersApi.java
@@ -0,0 +1,203 @@
+package com.crowdin.client.placeholders;
+
+import com.crowdin.client.core.CrowdinApi;
+import com.crowdin.client.core.http.HttpRequestConfig;
+import com.crowdin.client.core.http.exceptions.HttpBadRequestException;
+import com.crowdin.client.core.http.exceptions.HttpException;
+import com.crowdin.client.core.model.ClientConfig;
+import com.crowdin.client.core.model.Credentials;
+import com.crowdin.client.core.model.PatchRequest;
+import com.crowdin.client.core.model.ResponseList;
+import com.crowdin.client.core.model.ResponseObject;
+import com.crowdin.client.placeholders.model.AddOrganizationPlaceholderRequest;
+import com.crowdin.client.placeholders.model.AddProjectPlaceholderRequest;
+import com.crowdin.client.placeholders.model.OrganizationPlaceholder;
+import com.crowdin.client.placeholders.model.OrganizationPlaceholderResponseList;
+import com.crowdin.client.placeholders.model.OrganizationPlaceholderResponseObject;
+import com.crowdin.client.placeholders.model.ProjectPlaceholder;
+import com.crowdin.client.placeholders.model.ProjectPlaceholderResponseList;
+import com.crowdin.client.placeholders.model.ProjectPlaceholderResponseObject;
+import com.crowdin.client.placeholders.model.SystemPlaceholder;
+import com.crowdin.client.placeholders.model.SystemPlaceholderResponseList;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+public class PlaceholdersApi extends CrowdinApi {
+ public PlaceholdersApi(Credentials credentials) {
+ super(credentials);
+ }
+
+ public PlaceholdersApi(Credentials credentials, ClientConfig clientConfig) {
+ super(credentials, clientConfig);
+ }
+
+ /**
+ * @param projectId project identifier
+ * @param limit maximum number of items to retrieve (default 25)
+ * @param offset starting offset in the collection (default 0)
+ * @return list of system placeholders with their state in the project
+ * @see
+ */
+ public ResponseList listSystemPlaceholders(Long projectId, Integer limit, Integer offset) throws HttpException, HttpBadRequestException {
+ Map> queryParams = HttpRequestConfig.buildUrlParams(
+ "limit", Optional.ofNullable(limit),
+ "offset", Optional.ofNullable(offset)
+ );
+ SystemPlaceholderResponseList systemPlaceholderResponseList = this.httpClient.get(this.url + "/projects/" + projectId + "/system-placeholders", new HttpRequestConfig(queryParams), SystemPlaceholderResponseList.class);
+ return SystemPlaceholderResponseList.to(systemPlaceholderResponseList);
+ }
+
+ /**
+ * Turns system placeholders on and off. Each operation path has the form {@code /{systemPlaceholderId}/isEnabled}.
+ *
+ * @param projectId project identifier
+ * @param request request object
+ * @return changed system placeholders (no pagination)
+ * @see
+ */
+ public ResponseList editSystemPlaceholders(Long projectId, List request) throws HttpException, HttpBadRequestException {
+ SystemPlaceholderResponseList systemPlaceholderResponseList = this.httpClient.patch(this.url + "/projects/" + projectId + "/system-placeholders", request, new HttpRequestConfig(), SystemPlaceholderResponseList.class);
+ return SystemPlaceholderResponseList.to(systemPlaceholderResponseList);
+ }
+
+ /**
+ * @param limit maximum number of items to retrieve (default 25)
+ * @param offset starting offset in the collection (default 0)
+ * @return list of custom placeholders of the organization
+ * @see
+ */
+ public ResponseList listCustomPlaceholders(Integer limit, Integer offset) throws HttpException, HttpBadRequestException {
+ Map> queryParams = HttpRequestConfig.buildUrlParams(
+ "limit", Optional.ofNullable(limit),
+ "offset", Optional.ofNullable(offset)
+ );
+ OrganizationPlaceholderResponseList organizationPlaceholderResponseList = this.httpClient.get(this.url + "/custom-placeholders", new HttpRequestConfig(queryParams), OrganizationPlaceholderResponseList.class);
+ return OrganizationPlaceholderResponseList.to(organizationPlaceholderResponseList);
+ }
+
+ /**
+ * @param request request object
+ * @return newly created custom placeholder
+ * @see
+ */
+ public ResponseObject addCustomPlaceholder(AddOrganizationPlaceholderRequest request) throws HttpException, HttpBadRequestException {
+ OrganizationPlaceholderResponseObject organizationPlaceholderResponseObject = this.httpClient.post(this.url + "/custom-placeholders", request, new HttpRequestConfig(), OrganizationPlaceholderResponseObject.class);
+ return ResponseObject.of(organizationPlaceholderResponseObject.getData());
+ }
+
+ /**
+ * @param customPlaceholderId custom placeholder identifier
+ * @return custom placeholder
+ * @see
+ */
+ public ResponseObject getCustomPlaceholder(Long customPlaceholderId) throws HttpException, HttpBadRequestException {
+ OrganizationPlaceholderResponseObject organizationPlaceholderResponseObject = this.httpClient.get(this.url + "/custom-placeholders/" + customPlaceholderId, new HttpRequestConfig(), OrganizationPlaceholderResponseObject.class);
+ return ResponseObject.of(organizationPlaceholderResponseObject.getData());
+ }
+
+ /**
+ * @param customPlaceholderId custom placeholder identifier
+ * @param request request object
+ * @return updated custom placeholder
+ * @see
+ */
+ public ResponseObject editCustomPlaceholder(Long customPlaceholderId, List request) throws HttpException, HttpBadRequestException {
+ OrganizationPlaceholderResponseObject organizationPlaceholderResponseObject = this.httpClient.patch(this.url + "/custom-placeholders/" + customPlaceholderId, request, new HttpRequestConfig(), OrganizationPlaceholderResponseObject.class);
+ return ResponseObject.of(organizationPlaceholderResponseObject.getData());
+ }
+
+ /**
+ * @param customPlaceholderId custom placeholder identifier
+ * @see
+ */
+ public void deleteCustomPlaceholder(Long customPlaceholderId) throws HttpException, HttpBadRequestException {
+ this.httpClient.delete(this.url + "/custom-placeholders/" + customPlaceholderId, new HttpRequestConfig(), Void.class);
+ }
+
+ /**
+ * @param projectId project identifier
+ * @param limit maximum number of items to retrieve (default 25)
+ * @param offset starting offset in the collection (default 0)
+ * @return list of custom placeholders assigned to the project
+ * @see
+ */
+ public ResponseList listProjectPlaceholders(Long projectId, Integer limit, Integer offset) throws HttpException, HttpBadRequestException {
+ Map> queryParams = HttpRequestConfig.buildUrlParams(
+ "limit", Optional.ofNullable(limit),
+ "offset", Optional.ofNullable(offset)
+ );
+ ProjectPlaceholderResponseList projectPlaceholderResponseList = this.httpClient.get(this.url + "/projects/" + projectId + "/placeholders", new HttpRequestConfig(queryParams), ProjectPlaceholderResponseList.class);
+ return ProjectPlaceholderResponseList.to(projectPlaceholderResponseList);
+ }
+
+ /**
+ * @param projectId project identifier
+ * @param request request object
+ * @return newly assigned project placeholder
+ * @see
+ */
+ public ResponseObject addProjectPlaceholder(Long projectId, AddProjectPlaceholderRequest request) throws HttpException, HttpBadRequestException {
+ ProjectPlaceholderResponseObject projectPlaceholderResponseObject = this.httpClient.post(this.url + "/projects/" + projectId + "/placeholders", request, new HttpRequestConfig(), ProjectPlaceholderResponseObject.class);
+ return ResponseObject.of(projectPlaceholderResponseObject.getData());
+ }
+
+ /**
+ * @param projectId project identifier
+ * @param projectPlaceholderId project placeholder identifier
+ * @return project placeholder
+ * @see
+ */
+ public ResponseObject getProjectPlaceholder(Long projectId, Long projectPlaceholderId) throws HttpException, HttpBadRequestException {
+ ProjectPlaceholderResponseObject projectPlaceholderResponseObject = this.httpClient.get(this.url + "/projects/" + projectId + "/placeholders/" + projectPlaceholderId, new HttpRequestConfig(), ProjectPlaceholderResponseObject.class);
+ return ResponseObject.of(projectPlaceholderResponseObject.getData());
+ }
+
+ /**
+ * @param projectId project identifier
+ * @param projectPlaceholderId project placeholder identifier
+ * @param request request object
+ * @return updated project placeholder
+ * @see
+ */
+ public ResponseObject editProjectPlaceholder(Long projectId, Long projectPlaceholderId, List request) throws HttpException, HttpBadRequestException {
+ ProjectPlaceholderResponseObject projectPlaceholderResponseObject = this.httpClient.patch(this.url + "/projects/" + projectId + "/placeholders/" + projectPlaceholderId, request, new HttpRequestConfig(), ProjectPlaceholderResponseObject.class);
+ return ResponseObject.of(projectPlaceholderResponseObject.getData());
+ }
+
+ /**
+ * @param projectId project identifier
+ * @param projectPlaceholderId project placeholder identifier
+ * @see
+ */
+ public void deleteProjectPlaceholder(Long projectId, Long projectPlaceholderId) throws HttpException, HttpBadRequestException {
+ this.httpClient.delete(this.url + "/projects/" + projectId + "/placeholders/" + projectPlaceholderId, new HttpRequestConfig(), Void.class);
+ }
+}
diff --git a/src/main/java/com/crowdin/client/placeholders/model/AddOrganizationPlaceholderRequest.java b/src/main/java/com/crowdin/client/placeholders/model/AddOrganizationPlaceholderRequest.java
new file mode 100644
index 00000000..19ce7208
--- /dev/null
+++ b/src/main/java/com/crowdin/client/placeholders/model/AddOrganizationPlaceholderRequest.java
@@ -0,0 +1,11 @@
+package com.crowdin.client.placeholders.model;
+
+import lombok.Data;
+
+@Data
+public class AddOrganizationPlaceholderRequest {
+
+ private String definition;
+ private String description;
+ private String argumentDelimiter;
+}
diff --git a/src/main/java/com/crowdin/client/placeholders/model/AddProjectPlaceholderRequest.java b/src/main/java/com/crowdin/client/placeholders/model/AddProjectPlaceholderRequest.java
new file mode 100644
index 00000000..63a423cc
--- /dev/null
+++ b/src/main/java/com/crowdin/client/placeholders/model/AddProjectPlaceholderRequest.java
@@ -0,0 +1,15 @@
+package com.crowdin.client.placeholders.model;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class AddProjectPlaceholderRequest {
+
+ private Long customPlaceholderId;
+ private ProjectPlaceholderType type;
+ private Integer index;
+ private Boolean isBlocking;
+ private List formats;
+}
diff --git a/src/main/java/com/crowdin/client/placeholders/model/OrganizationPlaceholder.java b/src/main/java/com/crowdin/client/placeholders/model/OrganizationPlaceholder.java
new file mode 100644
index 00000000..7b151e49
--- /dev/null
+++ b/src/main/java/com/crowdin/client/placeholders/model/OrganizationPlaceholder.java
@@ -0,0 +1,16 @@
+package com.crowdin.client.placeholders.model;
+
+import lombok.Data;
+
+/**
+ * Custom placeholder defined by the organization (Enterprise "Custom Placeholder" resource).
+ * Not to be confused with {@link com.crowdin.client.ai.model.CustomPlaceholder} used by AI prompts.
+ */
+@Data
+public class OrganizationPlaceholder {
+
+ private Long id;
+ private String definition;
+ private String description;
+ private String argumentDelimiter;
+}
diff --git a/src/main/java/com/crowdin/client/placeholders/model/OrganizationPlaceholderResponseList.java b/src/main/java/com/crowdin/client/placeholders/model/OrganizationPlaceholderResponseList.java
new file mode 100644
index 00000000..99114b7e
--- /dev/null
+++ b/src/main/java/com/crowdin/client/placeholders/model/OrganizationPlaceholderResponseList.java
@@ -0,0 +1,26 @@
+package com.crowdin.client.placeholders.model;
+
+import com.crowdin.client.core.model.Pagination;
+import com.crowdin.client.core.model.ResponseList;
+import com.crowdin.client.core.model.ResponseObject;
+import lombok.Data;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Data
+public class OrganizationPlaceholderResponseList {
+
+ private List data;
+ private Pagination pagination;
+
+ public static ResponseList to(OrganizationPlaceholderResponseList organizationPlaceholderResponseList) {
+ return ResponseList.of(
+ organizationPlaceholderResponseList.getData().stream()
+ .map(OrganizationPlaceholderResponseObject::getData)
+ .map(ResponseObject::of)
+ .collect(Collectors.toList()),
+ organizationPlaceholderResponseList.getPagination()
+ );
+ }
+}
diff --git a/src/main/java/com/crowdin/client/placeholders/model/OrganizationPlaceholderResponseObject.java b/src/main/java/com/crowdin/client/placeholders/model/OrganizationPlaceholderResponseObject.java
new file mode 100644
index 00000000..4c9ce41a
--- /dev/null
+++ b/src/main/java/com/crowdin/client/placeholders/model/OrganizationPlaceholderResponseObject.java
@@ -0,0 +1,9 @@
+package com.crowdin.client.placeholders.model;
+
+import lombok.Data;
+
+@Data
+public class OrganizationPlaceholderResponseObject {
+
+ private OrganizationPlaceholder data;
+}
diff --git a/src/main/java/com/crowdin/client/placeholders/model/ProjectPlaceholder.java b/src/main/java/com/crowdin/client/placeholders/model/ProjectPlaceholder.java
new file mode 100644
index 00000000..0a3e1a98
--- /dev/null
+++ b/src/main/java/com/crowdin/client/placeholders/model/ProjectPlaceholder.java
@@ -0,0 +1,19 @@
+package com.crowdin.client.placeholders.model;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * A custom placeholder of the organization, with the settings it has in the project.
+ */
+@Data
+public class ProjectPlaceholder {
+
+ private Long id;
+ private Long customPlaceholderId;
+ private ProjectPlaceholderType type;
+ private Integer index;
+ private Boolean isBlocking;
+ private List formats;
+}
diff --git a/src/main/java/com/crowdin/client/placeholders/model/ProjectPlaceholderResponseList.java b/src/main/java/com/crowdin/client/placeholders/model/ProjectPlaceholderResponseList.java
new file mode 100644
index 00000000..779613ba
--- /dev/null
+++ b/src/main/java/com/crowdin/client/placeholders/model/ProjectPlaceholderResponseList.java
@@ -0,0 +1,26 @@
+package com.crowdin.client.placeholders.model;
+
+import com.crowdin.client.core.model.Pagination;
+import com.crowdin.client.core.model.ResponseList;
+import com.crowdin.client.core.model.ResponseObject;
+import lombok.Data;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Data
+public class ProjectPlaceholderResponseList {
+
+ private List data;
+ private Pagination pagination;
+
+ public static ResponseList to(ProjectPlaceholderResponseList projectPlaceholderResponseList) {
+ return ResponseList.of(
+ projectPlaceholderResponseList.getData().stream()
+ .map(ProjectPlaceholderResponseObject::getData)
+ .map(ResponseObject::of)
+ .collect(Collectors.toList()),
+ projectPlaceholderResponseList.getPagination()
+ );
+ }
+}
diff --git a/src/main/java/com/crowdin/client/placeholders/model/ProjectPlaceholderResponseObject.java b/src/main/java/com/crowdin/client/placeholders/model/ProjectPlaceholderResponseObject.java
new file mode 100644
index 00000000..c7a595d7
--- /dev/null
+++ b/src/main/java/com/crowdin/client/placeholders/model/ProjectPlaceholderResponseObject.java
@@ -0,0 +1,9 @@
+package com.crowdin.client.placeholders.model;
+
+import lombok.Data;
+
+@Data
+public class ProjectPlaceholderResponseObject {
+
+ private ProjectPlaceholder data;
+}
diff --git a/src/main/java/com/crowdin/client/placeholders/model/ProjectPlaceholderType.java b/src/main/java/com/crowdin/client/placeholders/model/ProjectPlaceholderType.java
new file mode 100644
index 00000000..33b4f265
--- /dev/null
+++ b/src/main/java/com/crowdin/client/placeholders/model/ProjectPlaceholderType.java
@@ -0,0 +1,21 @@
+package com.crowdin.client.placeholders.model;
+
+import com.crowdin.client.core.model.EnumConverter;
+
+public enum ProjectPlaceholderType implements EnumConverter {
+ HIGH, LOW;
+
+ public static ProjectPlaceholderType from(String value) {
+ for (ProjectPlaceholderType v : ProjectPlaceholderType.values()) {
+ if (v.name().equalsIgnoreCase(value)) {
+ return v;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String to(ProjectPlaceholderType value) {
+ return value.name().toLowerCase();
+ }
+}
diff --git a/src/main/java/com/crowdin/client/placeholders/model/SystemPlaceholder.java b/src/main/java/com/crowdin/client/placeholders/model/SystemPlaceholder.java
new file mode 100644
index 00000000..df65ec95
--- /dev/null
+++ b/src/main/java/com/crowdin/client/placeholders/model/SystemPlaceholder.java
@@ -0,0 +1,21 @@
+package com.crowdin.client.placeholders.model;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * A placeholder Crowdin ships, turned on or off per project.
+ */
+@Data
+public class SystemPlaceholder {
+
+ /**
+ * Key of the system placeholder (e.g. printfSpecifier). Kept as a string because the catalogue grows over time.
+ */
+ private String id;
+ private Boolean isEnabled;
+ private String label;
+ private List examples;
+ private String description;
+}
diff --git a/src/main/java/com/crowdin/client/placeholders/model/SystemPlaceholderResponseList.java b/src/main/java/com/crowdin/client/placeholders/model/SystemPlaceholderResponseList.java
new file mode 100644
index 00000000..2b0df68b
--- /dev/null
+++ b/src/main/java/com/crowdin/client/placeholders/model/SystemPlaceholderResponseList.java
@@ -0,0 +1,26 @@
+package com.crowdin.client.placeholders.model;
+
+import com.crowdin.client.core.model.Pagination;
+import com.crowdin.client.core.model.ResponseList;
+import com.crowdin.client.core.model.ResponseObject;
+import lombok.Data;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Data
+public class SystemPlaceholderResponseList {
+
+ private List data;
+ private Pagination pagination;
+
+ public static ResponseList to(SystemPlaceholderResponseList systemPlaceholderResponseList) {
+ return ResponseList.of(
+ systemPlaceholderResponseList.getData().stream()
+ .map(SystemPlaceholderResponseObject::getData)
+ .map(ResponseObject::of)
+ .collect(Collectors.toList()),
+ systemPlaceholderResponseList.getPagination()
+ );
+ }
+}
diff --git a/src/main/java/com/crowdin/client/placeholders/model/SystemPlaceholderResponseObject.java b/src/main/java/com/crowdin/client/placeholders/model/SystemPlaceholderResponseObject.java
new file mode 100644
index 00000000..6a1c9a24
--- /dev/null
+++ b/src/main/java/com/crowdin/client/placeholders/model/SystemPlaceholderResponseObject.java
@@ -0,0 +1,9 @@
+package com.crowdin.client.placeholders.model;
+
+import lombok.Data;
+
+@Data
+public class SystemPlaceholderResponseObject {
+
+ private SystemPlaceholder data;
+}
diff --git a/src/test/java/com/crowdin/client/advisors/AdvisorsApiTest.java b/src/test/java/com/crowdin/client/advisors/AdvisorsApiTest.java
new file mode 100644
index 00000000..4f2d2090
--- /dev/null
+++ b/src/test/java/com/crowdin/client/advisors/AdvisorsApiTest.java
@@ -0,0 +1,216 @@
+package com.crowdin.client.advisors;
+
+import com.crowdin.client.advisors.model.AdvisorAiMode;
+import com.crowdin.client.advisors.model.AdvisorCheck;
+import com.crowdin.client.advisors.model.AdvisorInsight;
+import com.crowdin.client.advisors.model.AdvisorInsightMetric;
+import com.crowdin.client.advisors.model.AdvisorInsightOutcome;
+import com.crowdin.client.advisors.model.AdvisorInsightPagination;
+import com.crowdin.client.advisors.model.AdvisorInsightRecommendation;
+import com.crowdin.client.advisors.model.AdvisorInsightSeverity;
+import com.crowdin.client.advisors.model.AdvisorInsightStatus;
+import com.crowdin.client.advisors.model.AdvisorInspectorOptions;
+import com.crowdin.client.advisors.model.AdvisorMetricSource;
+import com.crowdin.client.advisors.model.AdvisorMetricTone;
+import com.crowdin.client.advisors.model.AdvisorMetricUnit;
+import com.crowdin.client.advisors.model.AdvisorRefreshPolicy;
+import com.crowdin.client.advisors.model.ListAdvisorInsightsParams;
+import com.crowdin.client.advisors.model.RunAdvisorCheckInspector;
+import com.crowdin.client.advisors.model.RunAdvisorCheckRequest;
+import com.crowdin.client.advisors.model.UpsertAdvisorInsightRequest;
+import com.crowdin.client.core.model.PatchOperation;
+import com.crowdin.client.core.model.PatchRequest;
+import com.crowdin.client.core.model.ResponseList;
+import com.crowdin.client.core.model.ResponseObject;
+import com.crowdin.client.framework.RequestMock;
+import com.crowdin.client.framework.TestClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPatch;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
+import org.junit.jupiter.api.Test;
+
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static java.util.Collections.singletonList;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class AdvisorsApiTest extends TestClient {
+
+ private final Long projectId = 8L;
+ private final String checkId = "550e8400-e29b-41d4-a716-446655440000";
+ private final Long insightId = 10L;
+ private final String applicationIdentifier = "my-app";
+ private final String moduleKey = "my-inspector";
+ private final String inspectorKey = "string_context_relevance";
+
+ @Override
+ public List getMocks() {
+ Map listParams = new HashMap<>();
+ listParams.put("limit", 25);
+ listParams.put("offset", 0);
+ listParams.put("isDismissed", false);
+ listParams.put("status", "done,outdated");
+ listParams.put("outcome", "flagged,not_applicable");
+
+ String projectUrl = this.url + "/projects/" + projectId;
+ return Arrays.asList(
+ RequestMock.build(projectUrl + "/advisors/checks", HttpPost.METHOD_NAME, "api/advisors/runAdvisorCheckRequest.json", "api/advisors/advisorCheck.json"),
+ RequestMock.build(projectUrl + "/advisors/checks/" + checkId, HttpGet.METHOD_NAME, "api/advisors/advisorCheck.json"),
+ RequestMock.build(projectUrl + "/advisors/insights", HttpGet.METHOD_NAME, "api/advisors/listAdvisorInsights.json", listParams),
+ RequestMock.build(projectUrl + "/advisors/insights/" + insightId, HttpPatch.METHOD_NAME, "api/advisors/editAdvisorInsightRequest.json", "api/advisors/advisorInsight.json"),
+ RequestMock.build(projectUrl + "/applications/" + applicationIdentifier + "/modules/" + moduleKey + "/advisors/insights", HttpPut.METHOD_NAME, "api/advisors/upsertAdvisorInsightRequest.json", (String) null)
+ );
+ }
+
+ @Test
+ public void runAdvisorCheckTest() {
+ AdvisorInspectorOptions options = new AdvisorInspectorOptions();
+ options.setMode(AdvisorAiMode.AUTO);
+ options.setPromptId(42L);
+ RunAdvisorCheckInspector inspector = new RunAdvisorCheckInspector();
+ inspector.setKey(inspectorKey);
+ inspector.setOptions(options);
+ RunAdvisorCheckRequest request = new RunAdvisorCheckRequest();
+ request.setInspectors(singletonList(inspector));
+
+ ResponseObject response = this.getAdvisorsApi().runAdvisorCheck(projectId, request);
+ assertAdvisorCheck(response.getData());
+ }
+
+ @Test
+ public void getAdvisorCheckTest() {
+ ResponseObject response = this.getAdvisorsApi().getAdvisorCheck(projectId, checkId);
+ assertAdvisorCheck(response.getData());
+ }
+
+ @Test
+ public void listAdvisorInsightsTest() {
+ ListAdvisorInsightsParams params = new ListAdvisorInsightsParams();
+ params.setLimit(25);
+ params.setOffset(0);
+ params.setIsDismissed(false);
+ params.setStatus(Arrays.asList(AdvisorInsightStatus.DONE, AdvisorInsightStatus.OUTDATED));
+ params.setOutcome(Arrays.asList(AdvisorInsightOutcome.FLAGGED, AdvisorInsightOutcome.NOT_APPLICABLE));
+
+ ResponseList response = this.getAdvisorsApi().listAdvisorInsights(projectId, params);
+ assertEquals(2, response.getData().size());
+ assertInsight(response.getData().get(0).getData());
+
+ AdvisorInsight removed = response.getData().get(1).getData();
+ assertEquals(11L, removed.getId());
+ assertTrue(removed.getIsDismissed());
+ assertNull(removed.getStatus());
+ assertNull(removed.getOutcome());
+ assertNull(removed.getSeverity());
+ assertNull(removed.getRefreshPolicy());
+ assertNull(removed.getCategory());
+ assertNull(removed.getLastAiRun());
+ assertNull(removed.getPayload());
+ assertNull(removed.getRecommendations().get(0).getParams());
+
+ assertTrue(response.getPagination() instanceof AdvisorInsightPagination);
+ AdvisorInsightPagination pagination = (AdvisorInsightPagination) response.getPagination();
+ assertEquals(0, pagination.getOffset());
+ assertEquals(25, pagination.getLimit());
+ assertEquals(2, pagination.getTotal());
+ }
+
+ @Test
+ public void editAdvisorInsightTest() {
+ PatchRequest request = new PatchRequest();
+ request.setOp(PatchOperation.REPLACE);
+ request.setPath("/isDismissed");
+ request.setValue(true);
+
+ ResponseObject response = this.getAdvisorsApi().editAdvisorInsight(projectId, insightId, singletonList(request));
+ assertInsight(response.getData());
+ }
+
+ @Test
+ public void upsertApplicationAdvisorInsightTest() {
+ AdvisorInsightMetric metric = new AdvisorInsightMetric();
+ metric.setKey("quality_score");
+ metric.setValue(12.0);
+ metric.setUnit(AdvisorMetricUnit.COUNT);
+ metric.setThreshold(50.0);
+ metric.setTone(AdvisorMetricTone.DEFAULT);
+ metric.setSource(AdvisorMetricSource.APP);
+
+ AdvisorInsightRecommendation recommendation = new AdvisorInsightRecommendation();
+ recommendation.setId("fix_strings");
+ recommendation.setPrimary(true);
+ recommendation.setParams(Collections.singletonMap("projectId", 8));
+
+ UpsertAdvisorInsightRequest request = new UpsertAdvisorInsightRequest();
+ request.setOutcome(AdvisorInsightOutcome.FLAGGED);
+ request.setCheckedAt(Date.from(Instant.parse("2026-05-21T14:30:00Z")));
+ request.setMetrics(singletonList(metric));
+ request.setRecommendations(singletonList(recommendation));
+ request.setPayload(Collections.singletonMap("foo", "bar"));
+
+ this.getAdvisorsApi().upsertApplicationAdvisorInsight(projectId, applicationIdentifier, moduleKey, request);
+ }
+
+ /**
+ * The core DateDeserializer parses timestamps in the JVM default time zone (offset ignored).
+ */
+ private static Date localDate(String dateTime) {
+ return Date.from(LocalDateTime.parse(dateTime).atZone(ZoneId.systemDefault()).toInstant());
+ }
+
+ private void assertAdvisorCheck(AdvisorCheck check) {
+ assertEquals(checkId, check.getIdentifier());
+ assertEquals("created", check.getStatus());
+ assertEquals(0, check.getProgress());
+ assertNull(check.getAttributes().getCategory());
+ assertEquals(inspectorKey, check.getAttributes().getInspectors().get(0).getKey());
+ assertEquals(localDate("2026-04-16T12:00:00"), check.getCreatedAt());
+ assertEquals(localDate("2026-04-16T12:00:00"), check.getUpdatedAt());
+ assertNull(check.getStartedAt());
+ assertNull(check.getFinishedAt());
+ }
+
+ private void assertInsight(AdvisorInsight insight) {
+ Date checkedAt = localDate("2026-04-06T10:30:00");
+ assertEquals(insightId, insight.getId());
+ assertEquals(inspectorKey, insight.getInspectorKey());
+ assertEquals("context", insight.getCategory());
+ assertFalse(insight.getIsDismissed());
+ assertEquals(AdvisorInsightStatus.DONE, insight.getStatus());
+ assertEquals(AdvisorInsightOutcome.NOT_APPLICABLE, insight.getOutcome());
+ assertEquals(AdvisorInsightSeverity.HIGH, insight.getSeverity());
+ assertEquals(AdvisorRefreshPolicy.HOURLY, insight.getRefreshPolicy());
+
+ AdvisorInsightMetric metric = insight.getMetrics().get(0);
+ assertEquals("quality_score", metric.getKey());
+ assertEquals(12.5, metric.getValue());
+ assertEquals(AdvisorMetricUnit.PERCENT, metric.getUnit());
+ assertEquals(50.0, metric.getThreshold());
+ assertEquals(AdvisorMetricTone.DANGER, metric.getTone());
+ assertEquals(AdvisorMetricSource.DETERMINISTIC, metric.getSource());
+ assertEquals(checkedAt, metric.getCheckedAt());
+
+ AdvisorInsightRecommendation recommendation = insight.getRecommendations().get(0);
+ assertEquals("run_ai_context_validation", recommendation.getId());
+ assertTrue(recommendation.getPrimary());
+ assertEquals(8, recommendation.getParams().get("projectId"));
+
+ assertEquals(checkedAt, insight.getCheckedAt());
+ assertEquals(AdvisorAiMode.AUTO, insight.getLastAiRun().getMode());
+ assertEquals(42L, insight.getLastAiRun().getPromptId());
+ assertEquals(checkedAt, insight.getLastAiRun().getAt());
+ assertEquals(Arrays.asList(1, 2), insight.getPayload().get("stringIds"));
+ }
+}
diff --git a/src/test/java/com/crowdin/client/organization/OrganizationApiTest.java b/src/test/java/com/crowdin/client/organization/OrganizationApiTest.java
new file mode 100644
index 00000000..d64763ec
--- /dev/null
+++ b/src/test/java/com/crowdin/client/organization/OrganizationApiTest.java
@@ -0,0 +1,149 @@
+package com.crowdin.client.organization;
+
+import com.crowdin.client.core.http.impl.json.JacksonJsonTransformer;
+import com.crowdin.client.core.model.ResponseList;
+import com.crowdin.client.core.model.ResponseObject;
+import com.crowdin.client.framework.RequestMock;
+import com.crowdin.client.framework.TestClient;
+import com.crowdin.client.organization.model.AuthMethod;
+import com.crowdin.client.organization.model.CustomSpellchecker;
+import com.crowdin.client.organization.model.ExternalQaCheck;
+import com.crowdin.client.organization.model.Organization;
+import com.crowdin.client.organization.model.OrganizationAuthSettings;
+import com.crowdin.client.organization.model.OrganizationResponseObject;
+import com.crowdin.client.organization.model.PublicProjectsView;
+import org.apache.http.client.methods.HttpGet;
+import org.junit.jupiter.api.Test;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class OrganizationApiTest extends TestClient {
+
+ private final Long projectId = 8L;
+ private final Long customSpellcheckerId = 2L;
+ private final Long externalQaCheckId = 1L;
+
+ @Override
+ public List getMocks() {
+ Map pageParams = new HashMap<>();
+ pageParams.put("limit", 25);
+ pageParams.put("offset", 0);
+
+ Map qaParams = new HashMap<>(pageParams);
+ qaParams.put("projectId", projectId);
+
+ return Arrays.asList(
+ RequestMock.build(this.url + "/organization", HttpGet.METHOD_NAME, "api/organization/organization.json"),
+ RequestMock.build(this.url + "/organization/auth-settings", HttpGet.METHOD_NAME, "api/organization/authSettings.json"),
+ RequestMock.build(this.url + "/custom-spellcheckers", HttpGet.METHOD_NAME, "api/organization/listCustomSpellcheckers.json", pageParams),
+ RequestMock.build(this.url + "/custom-spellcheckers/" + customSpellcheckerId, HttpGet.METHOD_NAME, "api/organization/customSpellchecker.json"),
+ RequestMock.build(this.url + "/external-qa-checks", HttpGet.METHOD_NAME, "api/organization/listExternalQaChecks.json", qaParams)
+ );
+ }
+
+ @Test
+ public void getOrganizationTest() {
+ ResponseObject response = this.getOrganizationApi().getOrganization();
+ Organization organization = response.getData();
+ assertEquals(1L, organization.getId());
+ assertEquals("crowdin", organization.getDomain());
+ assertEquals("Crowdin", organization.getName());
+ assertNull(organization.getLogo());
+ assertEquals("", organization.getDefaultLogo());
+ assertNull(organization.getDescription());
+ assertEquals("Internal", organization.getInternalDescription());
+ assertNull(organization.getCname());
+ assertFalse(organization.getIsVendor());
+ assertEquals(PublicProjectsView.GRID, organization.getDefaultPublicProjectsView());
+ assertEquals("Team+", organization.getPlan().getName());
+ assertEquals(7500000L, organization.getPlan().getWordsLimit());
+ assertEquals(25L, organization.getPlan().getManagersLimit());
+ assertEquals(1, organization.getDefaults().size());
+ assertEquals("editorView", organization.getDefaults().get(0).getName());
+ assertTrue(organization.getDefaults().get(0).getIsLocked());
+ assertEquals("side-by-side", organization.getDefaults().get(0).getDefaultValue());
+ }
+
+ @Test
+ public void organizationIsVendorAsStringTest() {
+ String json = new BufferedReader(new InputStreamReader(
+ this.getClass().getClassLoader().getResourceAsStream("api/organization/organizationStringIsVendor.json")))
+ .lines().collect(Collectors.joining("\n"));
+ OrganizationResponseObject response = new JacksonJsonTransformer().parse(json, OrganizationResponseObject.class);
+ assertTrue(response.getData().getIsVendor());
+ }
+
+ @Test
+ public void getOrganizationAuthSettingsTest() {
+ ResponseObject response = this.getOrganizationApi().getOrganizationAuthSettings();
+ OrganizationAuthSettings settings = response.getData();
+ assertTrue(settings.getAllowSignUp());
+ assertFalse(settings.getTwoFactorAuthentication());
+ AuthMethod authMethod = settings.getAuthMethods().get(0);
+ assertEquals("Password", authMethod.getName());
+ assertTrue(authMethod.getIsEnabled());
+ assertTrue(authMethod.getIsDefault());
+ }
+
+ @Test
+ public void listCustomSpellcheckersTest() {
+ ResponseList response = this.getOrganizationApi().listCustomSpellcheckers(25, 0);
+ assertEquals(1, response.getData().size());
+ assertCustomSpellchecker(response.getData().get(0).getData());
+ }
+
+ @Test
+ public void getCustomSpellcheckerTest() {
+ ResponseObject response = this.getOrganizationApi().getCustomSpellchecker(customSpellcheckerId);
+ assertCustomSpellchecker(response.getData());
+ }
+
+ @Test
+ public void listExternalQaChecksTest() {
+ ResponseList response = this.getOrganizationApi().listExternalQaChecks(projectId, 25, 0);
+ assertEquals(1, response.getData().size());
+ assertExternalQaCheck(response.getData().get(0).getData());
+ }
+
+ /**
+ * The core DateDeserializer parses timestamps in the JVM default time zone (offset ignored).
+ */
+ private static Date localDate(String dateTime) {
+ return Date.from(LocalDateTime.parse(dateTime).atZone(ZoneId.systemDefault()).toInstant());
+ }
+
+ private void assertCustomSpellchecker(CustomSpellchecker spellchecker) {
+ assertEquals(customSpellcheckerId, spellchecker.getId());
+ assertEquals("Custom Spellchecker", spellchecker.getName());
+ assertEquals("app-identifier", spellchecker.getConfig().getIdentifier());
+ assertEquals("app-module-key", spellchecker.getConfig().getKey());
+ assertTrue(spellchecker.getConfig().getRealTimeCheckEnabled());
+ assertEquals(Arrays.asList("en", "es"), spellchecker.getConfig().getEnabledLanguageIds());
+ assertEquals(localDate("2019-09-23T07:19:47"), spellchecker.getCreatedAt());
+ assertNull(spellchecker.getUpdatedAt());
+ }
+
+ private void assertExternalQaCheck(ExternalQaCheck check) {
+ assertEquals(externalQaCheckId, check.getId());
+ assertEquals("Test QA check", check.getName());
+ assertEquals("Do some QA checks", check.getDescription());
+ assertEquals("app-identifier", check.getConfig().get("identifier"));
+ assertEquals(true, check.getConfig().get("enabled"));
+ assertEquals(localDate("2024-09-19T14:14:00"), check.getCreatedAt());
+ assertEquals(localDate("2024-09-19T14:14:00"), check.getUpdatedAt());
+ }
+}
diff --git a/src/test/java/com/crowdin/client/organization/OrganizationIsVendorTest.java b/src/test/java/com/crowdin/client/organization/OrganizationIsVendorTest.java
new file mode 100644
index 00000000..2bc9fd9c
--- /dev/null
+++ b/src/test/java/com/crowdin/client/organization/OrganizationIsVendorTest.java
@@ -0,0 +1,151 @@
+package com.crowdin.client.organization;
+
+import com.crowdin.client.core.http.impl.json.JacksonJsonTransformer;
+import com.crowdin.client.organization.model.Organization;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+/**
+ * The API documents {@code isVendor} as a string while its example is a boolean; both are accepted and the raw value
+ * is always kept.
+ */
+class OrganizationIsVendorTest {
+
+ private final JacksonJsonTransformer jsonTransformer = new JacksonJsonTransformer();
+
+ @Test
+ void booleanRepresentationTest() {
+ assertIsVendor("true", Boolean.TRUE, "true");
+ assertIsVendor("false", Boolean.FALSE, "false");
+ }
+
+ @Test
+ void stringRepresentationTest() {
+ assertIsVendor("\"true\"", Boolean.TRUE, "true");
+ assertIsVendor("\"false\"", Boolean.FALSE, "false");
+ assertIsVendor("\"TRUE\"", Boolean.TRUE, "TRUE");
+ }
+
+ @Test
+ void unrecognizedValuesAreKeptRawTest() {
+ assertIsVendor("\"1\"", null, "1");
+ assertIsVendor("\"vendor\"", null, "vendor");
+ assertIsVendor("\"\"", null, "");
+ assertIsVendor("1", null, "1");
+ }
+
+ @Test
+ void nullAndMissingTest() {
+ assertIsVendor("null", null, null);
+ Organization missing = jsonTransformer.parse("{\"id\":1}", Organization.class);
+ assertNull(missing.getIsVendor());
+ assertNull(missing.getIsVendorRaw());
+ }
+
+ @Test
+ void serializationTest() {
+ Organization organization = new Organization();
+ assertEquals("{}", jsonTransformer.convert(organization));
+ organization.setIsVendor(true);
+ assertEquals("{\"isVendor\":true}", jsonTransformer.convert(organization));
+ organization.setIsVendor(null);
+ organization.setIsVendorRaw("vendor");
+ assertEquals("{\"isVendor\":\"vendor\"}", jsonTransformer.convert(organization));
+ }
+
+ @Test
+ void setIsVendorKeepsRawInSyncTest() {
+ Organization organization = new Organization();
+ organization.setIsVendor(true);
+ assertState(organization, Boolean.TRUE, "true", "{\"isVendor\":true}");
+ organization.setIsVendor(false);
+ assertState(organization, Boolean.FALSE, "false", "{\"isVendor\":false}");
+ organization.setIsVendor(null);
+ assertState(organization, null, null, "{}");
+ }
+
+ @Test
+ void setIsVendorRawRecomputesBooleanTest() {
+ Organization organization = new Organization();
+ organization.setIsVendorRaw("true");
+ assertState(organization, Boolean.TRUE, "true", "{\"isVendor\":true}");
+ organization.setIsVendorRaw("false");
+ assertState(organization, Boolean.FALSE, "false", "{\"isVendor\":false}");
+ organization.setIsVendorRaw("TRUE");
+ assertState(organization, Boolean.TRUE, "TRUE", "{\"isVendor\":true}");
+ organization.setIsVendorRaw("vendor");
+ assertState(organization, null, "vendor", "{\"isVendor\":\"vendor\"}");
+ organization.setIsVendorRaw(null);
+ assertState(organization, null, null, "{}");
+ }
+
+ @Test
+ void lastSetterWinsInBothOrdersTest() {
+ Organization booleanLast = new Organization();
+ booleanLast.setIsVendorRaw("vendor");
+ booleanLast.setIsVendor(false);
+ assertState(booleanLast, Boolean.FALSE, "false", "{\"isVendor\":false}");
+
+ Organization rawLast = new Organization();
+ rawLast.setIsVendor(true);
+ rawLast.setIsVendorRaw("false");
+ assertState(rawLast, Boolean.FALSE, "false", "{\"isVendor\":false}");
+
+ Organization rawUnknownLast = new Organization();
+ rawUnknownLast.setIsVendor(true);
+ rawUnknownLast.setIsVendorRaw("vendor");
+ assertState(rawUnknownLast, null, "vendor", "{\"isVendor\":\"vendor\"}");
+
+ Organization clearedByBoolean = new Organization();
+ clearedByBoolean.setIsVendorRaw("vendor");
+ clearedByBoolean.setIsVendor(null);
+ assertState(clearedByBoolean, null, null, "{}");
+ }
+
+ @Test
+ void settersAfterDeserializationStayConsistentTest() {
+ Organization parsedFalse = jsonTransformer.parse("{\"isVendor\":false}", Organization.class);
+ parsedFalse.setIsVendor(true);
+ assertState(parsedFalse, Boolean.TRUE, "true", "{\"isVendor\":true}");
+
+ Organization parsedTrue = jsonTransformer.parse("{\"isVendor\":true}", Organization.class);
+ parsedTrue.setIsVendorRaw("false");
+ assertState(parsedTrue, Boolean.FALSE, "false", "{\"isVendor\":false}");
+
+ Organization parsedString = jsonTransformer.parse("{\"isVendor\":\"vendor\"}", Organization.class);
+ parsedString.setIsVendor(false);
+ assertState(parsedString, Boolean.FALSE, "false", "{\"isVendor\":false}");
+ }
+
+ @Test
+ void setterAndJsonReaderAgreeTest() {
+ for (String raw : new String[]{"true", "false", "TRUE", "False", "vendor", "1", "", " true "}) {
+ Organization viaSetter = new Organization();
+ viaSetter.setIsVendorRaw(raw);
+ Organization viaJson = jsonTransformer.parse("{\"isVendor\":\"" + raw + "\"}", Organization.class);
+ assertEquals(viaJson.getIsVendor(), viaSetter.getIsVendor(), raw);
+ assertEquals(viaJson.getIsVendorRaw(), viaSetter.getIsVendorRaw(), raw);
+ assertEquals(jsonTransformer.convert(viaJson), jsonTransformer.convert(viaSetter), raw);
+ }
+ }
+
+ private void assertState(Organization organization, Boolean expected, String expectedRaw, String expectedJson) {
+ assertEquals(expected, organization.getIsVendor());
+ assertEquals(expectedRaw, organization.getIsVendorRaw());
+ String json = jsonTransformer.convert(organization);
+ assertEquals(expectedJson, json);
+ assertEquals(expectedJson.contains("isVendor") ? 1 : 0, json.split("\"isVendor\"", -1).length - 1);
+ if (expected != null) {
+ assertEquals(expected.toString(), expectedRaw.toLowerCase());
+ }
+ }
+
+ private void assertIsVendor(String jsonValue, Boolean expected, String expectedRaw) {
+ Organization organization = jsonTransformer.parse("{\"id\":1,\"isVendor\":" + jsonValue + "}", Organization.class);
+ assertEquals(expected, organization.getIsVendor(), jsonValue);
+ assertEquals(expectedRaw, organization.getIsVendorRaw(), jsonValue);
+ assertEquals(Long.valueOf(1), organization.getId());
+ }
+}
diff --git a/src/test/java/com/crowdin/client/placeholders/PlaceholdersApiTest.java b/src/test/java/com/crowdin/client/placeholders/PlaceholdersApiTest.java
new file mode 100644
index 00000000..bfd3243e
--- /dev/null
+++ b/src/test/java/com/crowdin/client/placeholders/PlaceholdersApiTest.java
@@ -0,0 +1,185 @@
+package com.crowdin.client.placeholders;
+
+import com.crowdin.client.core.model.PatchOperation;
+import com.crowdin.client.core.model.PatchRequest;
+import com.crowdin.client.core.model.ResponseList;
+import com.crowdin.client.core.model.ResponseObject;
+import com.crowdin.client.framework.RequestMock;
+import com.crowdin.client.framework.TestClient;
+import com.crowdin.client.placeholders.model.AddOrganizationPlaceholderRequest;
+import com.crowdin.client.placeholders.model.AddProjectPlaceholderRequest;
+import com.crowdin.client.placeholders.model.OrganizationPlaceholder;
+import com.crowdin.client.placeholders.model.ProjectPlaceholder;
+import com.crowdin.client.placeholders.model.ProjectPlaceholderType;
+import com.crowdin.client.placeholders.model.SystemPlaceholder;
+import org.apache.http.client.methods.HttpDelete;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPatch;
+import org.apache.http.client.methods.HttpPost;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static java.util.Collections.singletonList;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class PlaceholdersApiTest extends TestClient {
+
+ private final Long projectId = 8L;
+ private final Long customPlaceholderId = 1L;
+ private final Long projectPlaceholderId = 2L;
+ private final String definition = "start, then \"http\", maybe \"s\", then \"://\", end";
+ private final String description = "URL validation";
+ private final String argumentDelimiter = "\"";
+ private final List formats = Arrays.asList("docbook", "adoc");
+
+ @Override
+ public List getMocks() {
+ Map pageParams = new HashMap<>();
+ pageParams.put("limit", 25);
+ pageParams.put("offset", 0);
+
+ String projectUrl = this.url + "/projects/" + projectId;
+ return Arrays.asList(
+ RequestMock.build(projectUrl + "/system-placeholders", HttpGet.METHOD_NAME, "api/placeholders/listSystemPlaceholders.json", pageParams),
+ RequestMock.build(projectUrl + "/system-placeholders", HttpPatch.METHOD_NAME, "api/placeholders/editSystemPlaceholdersRequest.json", "api/placeholders/editSystemPlaceholders.json"),
+ RequestMock.build(this.url + "/custom-placeholders", HttpGet.METHOD_NAME, "api/placeholders/listCustomPlaceholders.json"),
+ RequestMock.build(this.url + "/custom-placeholders", HttpPost.METHOD_NAME, "api/placeholders/addCustomPlaceholderRequest.json", "api/placeholders/customPlaceholder.json"),
+ RequestMock.build(this.url + "/custom-placeholders/" + customPlaceholderId, HttpGet.METHOD_NAME, "api/placeholders/customPlaceholder.json"),
+ RequestMock.build(this.url + "/custom-placeholders/" + customPlaceholderId, HttpPatch.METHOD_NAME, "api/placeholders/editCustomPlaceholderRequest.json", "api/placeholders/customPlaceholder.json"),
+ RequestMock.build(this.url + "/custom-placeholders/" + customPlaceholderId, HttpDelete.METHOD_NAME),
+ RequestMock.build(projectUrl + "/placeholders", HttpGet.METHOD_NAME, "api/placeholders/listProjectPlaceholders.json", pageParams),
+ RequestMock.build(projectUrl + "/placeholders", HttpPost.METHOD_NAME, "api/placeholders/addProjectPlaceholderRequest.json", "api/placeholders/projectPlaceholder.json"),
+ RequestMock.build(projectUrl + "/placeholders/" + projectPlaceholderId, HttpGet.METHOD_NAME, "api/placeholders/projectPlaceholder.json"),
+ RequestMock.build(projectUrl + "/placeholders/" + projectPlaceholderId, HttpPatch.METHOD_NAME, "api/placeholders/editProjectPlaceholderRequest.json", "api/placeholders/projectPlaceholder.json"),
+ RequestMock.build(projectUrl + "/placeholders/" + projectPlaceholderId, HttpDelete.METHOD_NAME)
+ );
+ }
+
+ @Test
+ public void listSystemPlaceholdersTest() {
+ ResponseList response = this.getPlaceholdersApi().listSystemPlaceholders(projectId, 25, 0);
+ assertEquals(1, response.getData().size());
+ SystemPlaceholder placeholder = response.getData().get(0).getData();
+ assertEquals("printfSpecifier", placeholder.getId());
+ assertTrue(placeholder.getIsEnabled());
+ assertEquals("C-style format specifiers", placeholder.getLabel());
+ assertEquals(singletonList("%s"), placeholder.getExamples());
+ assertEquals("C, PHP, Java, Objective-C, Android, and Qt", placeholder.getDescription());
+ assertEquals(25, response.getPagination().getLimit());
+ }
+
+ @Test
+ public void editSystemPlaceholdersTest() {
+ PatchRequest request = new PatchRequest();
+ request.setOp(PatchOperation.REPLACE);
+ request.setPath("/printfSpecifier/isEnabled");
+ request.setValue(false);
+
+ ResponseList response = this.getPlaceholdersApi().editSystemPlaceholders(projectId, singletonList(request));
+ assertEquals(1, response.getData().size());
+ assertEquals("printfSpecifier", response.getData().get(0).getData().getId());
+ assertFalse(response.getData().get(0).getData().getIsEnabled());
+ assertNull(response.getPagination());
+ }
+
+ @Test
+ public void listCustomPlaceholdersTest() {
+ ResponseList response = this.getPlaceholdersApi().listCustomPlaceholders(null, null);
+ assertEquals(1, response.getData().size());
+ assertCustomPlaceholder(response.getData().get(0).getData());
+ }
+
+ @Test
+ public void addCustomPlaceholderTest() {
+ AddOrganizationPlaceholderRequest request = new AddOrganizationPlaceholderRequest();
+ request.setDefinition(definition);
+ request.setDescription(description);
+ request.setArgumentDelimiter(argumentDelimiter);
+ ResponseObject response = this.getPlaceholdersApi().addCustomPlaceholder(request);
+ assertCustomPlaceholder(response.getData());
+ }
+
+ @Test
+ public void getCustomPlaceholderTest() {
+ ResponseObject response = this.getPlaceholdersApi().getCustomPlaceholder(customPlaceholderId);
+ assertCustomPlaceholder(response.getData());
+ }
+
+ @Test
+ public void editCustomPlaceholderTest() {
+ PatchRequest request = new PatchRequest();
+ request.setOp(PatchOperation.REPLACE);
+ request.setPath("/description");
+ request.setValue(description);
+ ResponseObject response = this.getPlaceholdersApi().editCustomPlaceholder(customPlaceholderId, singletonList(request));
+ assertCustomPlaceholder(response.getData());
+ }
+
+ @Test
+ public void deleteCustomPlaceholderTest() {
+ this.getPlaceholdersApi().deleteCustomPlaceholder(customPlaceholderId);
+ }
+
+ @Test
+ public void listProjectPlaceholdersTest() {
+ ResponseList response = this.getPlaceholdersApi().listProjectPlaceholders(projectId, 25, 0);
+ assertEquals(1, response.getData().size());
+ assertProjectPlaceholder(response.getData().get(0).getData());
+ }
+
+ @Test
+ public void addProjectPlaceholderTest() {
+ AddProjectPlaceholderRequest request = new AddProjectPlaceholderRequest();
+ request.setCustomPlaceholderId(customPlaceholderId);
+ request.setType(ProjectPlaceholderType.HIGH);
+ request.setIndex(1);
+ request.setIsBlocking(false);
+ request.setFormats(formats);
+ ResponseObject response = this.getPlaceholdersApi().addProjectPlaceholder(projectId, request);
+ assertProjectPlaceholder(response.getData());
+ }
+
+ @Test
+ public void getProjectPlaceholderTest() {
+ ResponseObject response = this.getPlaceholdersApi().getProjectPlaceholder(projectId, projectPlaceholderId);
+ assertProjectPlaceholder(response.getData());
+ }
+
+ @Test
+ public void editProjectPlaceholderTest() {
+ PatchRequest request = new PatchRequest();
+ request.setOp(PatchOperation.REPLACE);
+ request.setPath("/isBlocking");
+ request.setValue(true);
+ ResponseObject response = this.getPlaceholdersApi().editProjectPlaceholder(projectId, projectPlaceholderId, singletonList(request));
+ assertProjectPlaceholder(response.getData());
+ }
+
+ @Test
+ public void deleteProjectPlaceholderTest() {
+ this.getPlaceholdersApi().deleteProjectPlaceholder(projectId, projectPlaceholderId);
+ }
+
+ private void assertCustomPlaceholder(OrganizationPlaceholder placeholder) {
+ assertEquals(customPlaceholderId, placeholder.getId());
+ assertEquals(definition, placeholder.getDefinition());
+ assertEquals(description, placeholder.getDescription());
+ assertEquals(argumentDelimiter, placeholder.getArgumentDelimiter());
+ }
+
+ private void assertProjectPlaceholder(ProjectPlaceholder placeholder) {
+ assertEquals(projectPlaceholderId, placeholder.getId());
+ assertEquals(customPlaceholderId, placeholder.getCustomPlaceholderId());
+ assertEquals(ProjectPlaceholderType.HIGH, placeholder.getType());
+ assertEquals(1, placeholder.getIndex());
+ assertFalse(placeholder.getIsBlocking());
+ assertEquals(formats, placeholder.getFormats());
+ }
+}
diff --git a/src/test/java/com/crowdin/client/stringtranslations/LanguageTranslationsDeserializationTest.java b/src/test/java/com/crowdin/client/stringtranslations/LanguageTranslationsDeserializationTest.java
index 036c9445..144f5b9a 100644
--- a/src/test/java/com/crowdin/client/stringtranslations/LanguageTranslationsDeserializationTest.java
+++ b/src/test/java/com/crowdin/client/stringtranslations/LanguageTranslationsDeserializationTest.java
@@ -108,6 +108,78 @@ void unknownContentTypeIsRejectedTest() {
assertEquals("Couldn't get class", rootMessage(missing));
}
+ @Test
+ void imageAssetsAreRecognizedTest() {
+ for (String mime : new String[]{"image/jpeg", "image/png", "image/svg+xml"}) {
+ AssetLanguageTranslations asset = assertInstanceOf(AssetLanguageTranslations.class,
+ parse("{\"stringId\":1,\"contentType\":\"" + mime + "\",\"url\":\"https://example.com/a\"}"), mime);
+ assertEquals(mime, asset.getContentType());
+ }
+ }
+
+ @Test
+ void assetFallbackRequiresNonEmptyStringContentTypeTest() {
+ String[] rejected = {
+ "{\"stringId\":1,\"contentType\":\"\",\"url\":\"u\"}",
+ "{\"stringId\":1,\"contentType\":\" \",\"url\":\"u\"}",
+ "{\"stringId\":1,\"contentType\":null,\"url\":\"u\"}",
+ "{\"stringId\":1,\"contentType\":5,\"url\":\"u\"}",
+ "{\"stringId\":1,\"contentType\":true,\"url\":\"u\"}",
+ };
+ for (String json : rejected) {
+ Exception e = assertThrows(Exception.class, () -> parse(json), json);
+ assertTrue(rootMessage(e).startsWith("Wrong type: "), json + " -> " + rootMessage(e));
+ }
+ for (String json : new String[]{"{\"stringId\":1,\"url\":\"u\"}", "{\"stringId\":1,\"contentType\":{\"x\":1},\"url\":\"u\"}",
+ "{\"stringId\":1,\"contentType\":[\"image/png\"],\"url\":\"u\"}"}) {
+ Exception e = assertThrows(Exception.class, () -> parse(json), json);
+ assertEquals("Couldn't get class", rootMessage(e), json);
+ }
+ }
+
+ @Test
+ void crowdinTextTypesAreNeverAssetsTest() {
+ for (String type : new String[]{"application/vnd.crowdin.text+new", "application/vnd.crowdin.text+markdown",
+ "application/vnd.crowdin.anything"}) {
+ String json = "{\"stringId\":1,\"contentType\":\"" + type + "\",\"url\":\"u\"}";
+ Exception e = assertThrows(Exception.class, () -> parse(json), json);
+ assertTrue(rootMessage(e).startsWith("Wrong type: "), json + " -> " + rootMessage(e));
+ }
+ }
+
+ @Test
+ void knownTypesAreUnchangedTest() {
+ assertInstanceOf(PlainLanguageTranslations.class, parse("{\"stringId\":1,\"contentType\":\"text/plain\",\"url\":\"u\"}"));
+ assertInstanceOf(PluralLanguageTranslations.class, parse("{\"stringId\":1,\"contentType\":\"application/vnd.crowdin.text+plural\",\"url\":\"u\"}"));
+ assertInstanceOf(ICULanguageTranslations.class, parse("{\"stringId\":1,\"contentType\":\"application/vnd.crowdin.text+icu\",\"url\":\"u\"}"));
+ }
+
+ @Test
+ void unknownTypesWithTextOrPluralsAreRejectedTest() {
+ String[] rejected = {
+ "{\"stringId\":1,\"contentType\":\"text/html\",\"text\":\"x\",\"url\":\"u\"}",
+ "{\"stringId\":1,\"contentType\":\"text/markdown\",\"text\":null,\"url\":\"u\"}",
+ "{\"stringId\":1,\"contentType\":\"image/png\",\"plurals\":[],\"url\":\"u\"}",
+ "{\"stringId\":1,\"contentType\":\"application/x-new\",\"plurals\":[]}",
+ };
+ for (String json : rejected) {
+ Exception e = assertThrows(Exception.class, () -> parse(json), json);
+ assertTrue(rootMessage(e).startsWith("Wrong type: "), json + " -> " + rootMessage(e));
+ }
+ }
+
+ /**
+ * Remaining ambiguity: the documented asset shape cannot distinguish an unknown {@code text/*} type that has a
+ * {@code url} but no {@code text}. {@code text/*} is not excluded because the API does not state that assets can
+ * never use a {@code text/*} MIME type.
+ */
+ @Test
+ void textMarkdownWithUrlAndNoTextIsReadAsAssetTest() {
+ AssetLanguageTranslations asset = assertInstanceOf(AssetLanguageTranslations.class,
+ parse("{\"stringId\":1,\"contentType\":\"text/markdown\",\"url\":\"u\"}"));
+ assertEquals("text/markdown", asset.getContentType());
+ }
+
private LanguageTranslations parse(String translation) {
return jsonTransformer.parse("{\"data\":" + translation + "}", LanguageTranslationsResponseObject.class).getData();
}
diff --git a/src/test/resources/api/advisors/advisorCheck.json b/src/test/resources/api/advisors/advisorCheck.json
new file mode 100644
index 00000000..06cfd5a6
--- /dev/null
+++ b/src/test/resources/api/advisors/advisorCheck.json
@@ -0,0 +1,19 @@
+{
+ "data": {
+ "identifier": "550e8400-e29b-41d4-a716-446655440000",
+ "status": "created",
+ "progress": 0,
+ "attributes": {
+ "category": null,
+ "inspectors": [
+ {
+ "key": "string_context_relevance"
+ }
+ ]
+ },
+ "createdAt": "2026-04-16T12:00:00+00:00",
+ "updatedAt": "2026-04-16T12:00:00+00:00",
+ "startedAt": null,
+ "finishedAt": null
+ }
+}
diff --git a/src/test/resources/api/advisors/advisorInsight.json b/src/test/resources/api/advisors/advisorInsight.json
new file mode 100644
index 00000000..f0eba344
--- /dev/null
+++ b/src/test/resources/api/advisors/advisorInsight.json
@@ -0,0 +1,41 @@
+{
+ "data": {
+ "id": 10,
+ "inspectorKey": "string_context_relevance",
+ "category": "context",
+ "isDismissed": false,
+ "status": "done",
+ "outcome": "not_applicable",
+ "severity": "high",
+ "refreshPolicy": "hourly",
+ "metrics": [
+ {
+ "key": "quality_score",
+ "value": 12.5,
+ "unit": "percent",
+ "threshold": 50,
+ "tone": "danger",
+ "source": "deterministic",
+ "checkedAt": "2026-04-06T10:30:00+00:00"
+ }
+ ],
+ "recommendations": [
+ {
+ "id": "run_ai_context_validation",
+ "primary": true,
+ "params": {
+ "projectId": 8
+ }
+ }
+ ],
+ "checkedAt": "2026-04-06T10:30:00+00:00",
+ "lastAiRun": {
+ "mode": "auto",
+ "promptId": 42,
+ "at": "2026-04-06T10:30:00+00:00"
+ },
+ "payload": {
+ "stringIds": [1, 2]
+ }
+ }
+}
diff --git a/src/test/resources/api/advisors/editAdvisorInsightRequest.json b/src/test/resources/api/advisors/editAdvisorInsightRequest.json
new file mode 100644
index 00000000..4c155b73
--- /dev/null
+++ b/src/test/resources/api/advisors/editAdvisorInsightRequest.json
@@ -0,0 +1,7 @@
+[
+ {
+ "op": "replace",
+ "path": "/isDismissed",
+ "value": true
+ }
+]
diff --git a/src/test/resources/api/advisors/listAdvisorInsights.json b/src/test/resources/api/advisors/listAdvisorInsights.json
new file mode 100644
index 00000000..b0b42afd
--- /dev/null
+++ b/src/test/resources/api/advisors/listAdvisorInsights.json
@@ -0,0 +1,73 @@
+{
+ "data": [
+ {
+ "data": {
+ "id": 10,
+ "inspectorKey": "string_context_relevance",
+ "category": "context",
+ "isDismissed": false,
+ "status": "done",
+ "outcome": "not_applicable",
+ "severity": "high",
+ "refreshPolicy": "hourly",
+ "metrics": [
+ {
+ "key": "quality_score",
+ "value": 12.5,
+ "unit": "percent",
+ "threshold": 50,
+ "tone": "danger",
+ "source": "deterministic",
+ "checkedAt": "2026-04-06T10:30:00+00:00"
+ }
+ ],
+ "recommendations": [
+ {
+ "id": "run_ai_context_validation",
+ "primary": true,
+ "params": {
+ "projectId": 8
+ }
+ }
+ ],
+ "checkedAt": "2026-04-06T10:30:00+00:00",
+ "lastAiRun": {
+ "mode": "auto",
+ "promptId": 42,
+ "at": "2026-04-06T10:30:00+00:00"
+ },
+ "payload": {
+ "stringIds": [1, 2]
+ }
+ }
+ },
+ {
+ "data": {
+ "id": 11,
+ "inspectorKey": "removed_inspector",
+ "category": null,
+ "isDismissed": true,
+ "status": "some_future_status",
+ "outcome": null,
+ "severity": null,
+ "refreshPolicy": null,
+ "metrics": null,
+ "recommendations": [
+ {
+ "id": "no_params",
+ "primary": false,
+ "params": []
+ }
+ ],
+ "checkedAt": null,
+ "lastAiRun": null,
+ "payload": []
+ }
+ }
+ ],
+ "pagination": {
+ "offset": 0,
+ "limit": 25,
+ "total": 2
+ }
+}
diff --git a/src/test/resources/api/advisors/runAdvisorCheckRequest.json b/src/test/resources/api/advisors/runAdvisorCheckRequest.json
new file mode 100644
index 00000000..537fc523
--- /dev/null
+++ b/src/test/resources/api/advisors/runAdvisorCheckRequest.json
@@ -0,0 +1,11 @@
+{
+ "inspectors": [
+ {
+ "key": "string_context_relevance",
+ "options": {
+ "mode": "auto",
+ "promptId": 42
+ }
+ }
+ ]
+}
diff --git a/src/test/resources/api/advisors/upsertAdvisorInsightRequest.json b/src/test/resources/api/advisors/upsertAdvisorInsightRequest.json
new file mode 100644
index 00000000..61954387
--- /dev/null
+++ b/src/test/resources/api/advisors/upsertAdvisorInsightRequest.json
@@ -0,0 +1,26 @@
+{
+ "outcome": "flagged",
+ "checkedAt": "2026-05-21T14:30:00.000+00:00",
+ "metrics": [
+ {
+ "key": "quality_score",
+ "value": 12.0,
+ "unit": "count",
+ "threshold": 50.0,
+ "tone": "default",
+ "source": "app"
+ }
+ ],
+ "recommendations": [
+ {
+ "id": "fix_strings",
+ "primary": true,
+ "params": {
+ "projectId": 8
+ }
+ }
+ ],
+ "payload": {
+ "foo": "bar"
+ }
+}
diff --git a/src/test/resources/api/organization/authSettings.json b/src/test/resources/api/organization/authSettings.json
new file mode 100644
index 00000000..ee75ee08
--- /dev/null
+++ b/src/test/resources/api/organization/authSettings.json
@@ -0,0 +1,13 @@
+{
+ "data": {
+ "allowSignUp": true,
+ "twoFactorAuthentication": false,
+ "authMethods": [
+ {
+ "name": "Password",
+ "isEnabled": true,
+ "isDefault": true
+ }
+ ]
+ }
+}
diff --git a/src/test/resources/api/organization/customSpellchecker.json b/src/test/resources/api/organization/customSpellchecker.json
new file mode 100644
index 00000000..824c34b4
--- /dev/null
+++ b/src/test/resources/api/organization/customSpellchecker.json
@@ -0,0 +1,17 @@
+{
+ "data": {
+ "id": 2,
+ "name": "Custom Spellchecker",
+ "config": {
+ "identifier": "app-identifier",
+ "key": "app-module-key",
+ "realTimeCheckEnabled": true,
+ "enabledLanguageIds": [
+ "en",
+ "es"
+ ]
+ },
+ "createdAt": "2019-09-23T07:19:47+00:00",
+ "updatedAt": null
+ }
+}
diff --git a/src/test/resources/api/organization/listCustomSpellcheckers.json b/src/test/resources/api/organization/listCustomSpellcheckers.json
new file mode 100644
index 00000000..06be6822
--- /dev/null
+++ b/src/test/resources/api/organization/listCustomSpellcheckers.json
@@ -0,0 +1,25 @@
+{
+ "data": [
+ {
+ "data": {
+ "id": 2,
+ "name": "Custom Spellchecker",
+ "config": {
+ "identifier": "app-identifier",
+ "key": "app-module-key",
+ "realTimeCheckEnabled": true,
+ "enabledLanguageIds": [
+ "en",
+ "es"
+ ]
+ },
+ "createdAt": "2019-09-23T07:19:47+00:00",
+ "updatedAt": null
+ }
+ }
+ ],
+ "pagination": {
+ "offset": 0,
+ "limit": 25
+ }
+}
diff --git a/src/test/resources/api/organization/listExternalQaChecks.json b/src/test/resources/api/organization/listExternalQaChecks.json
new file mode 100644
index 00000000..0feafc99
--- /dev/null
+++ b/src/test/resources/api/organization/listExternalQaChecks.json
@@ -0,0 +1,21 @@
+{
+ "data": [
+ {
+ "data": {
+ "id": 1,
+ "name": "Test QA check",
+ "description": "Do some QA checks",
+ "config": {
+ "identifier": "app-identifier",
+ "enabled": true
+ },
+ "createdAt": "2024-09-19T14:14:00+00:00",
+ "updatedAt": "2024-09-19T14:14:00+00:00"
+ }
+ }
+ ],
+ "pagination": {
+ "offset": 0,
+ "limit": 25
+ }
+}
diff --git a/src/test/resources/api/organization/organization.json b/src/test/resources/api/organization/organization.json
new file mode 100644
index 00000000..3e430437
--- /dev/null
+++ b/src/test/resources/api/organization/organization.json
@@ -0,0 +1,26 @@
+{
+ "data": {
+ "id": 1,
+ "domain": "crowdin",
+ "name": "Crowdin",
+ "logo": null,
+ "defaultLogo": "",
+ "description": null,
+ "internalDescription": "Internal",
+ "cname": null,
+ "isVendor": false,
+ "defaultPublicProjectsView": "grid",
+ "plan": {
+ "name": "Team+",
+ "wordsLimit": 7500000,
+ "managersLimit": 25
+ },
+ "defaults": [
+ {
+ "name": "editorView",
+ "isLocked": true,
+ "defaultValue": "side-by-side"
+ }
+ ]
+ }
+}
diff --git a/src/test/resources/api/organization/organizationStringIsVendor.json b/src/test/resources/api/organization/organizationStringIsVendor.json
new file mode 100644
index 00000000..f75648de
--- /dev/null
+++ b/src/test/resources/api/organization/organizationStringIsVendor.json
@@ -0,0 +1,6 @@
+{
+ "data": {
+ "id": 1,
+ "isVendor": "true"
+ }
+}
diff --git a/src/test/resources/api/placeholders/addCustomPlaceholderRequest.json b/src/test/resources/api/placeholders/addCustomPlaceholderRequest.json
new file mode 100644
index 00000000..b603ed9c
--- /dev/null
+++ b/src/test/resources/api/placeholders/addCustomPlaceholderRequest.json
@@ -0,0 +1,5 @@
+{
+ "definition": "start, then \"http\", maybe \"s\", then \"://\", end",
+ "description": "URL validation",
+ "argumentDelimiter": "\""
+}
diff --git a/src/test/resources/api/placeholders/addProjectPlaceholderRequest.json b/src/test/resources/api/placeholders/addProjectPlaceholderRequest.json
new file mode 100644
index 00000000..955337bc
--- /dev/null
+++ b/src/test/resources/api/placeholders/addProjectPlaceholderRequest.json
@@ -0,0 +1,10 @@
+{
+ "customPlaceholderId": 1,
+ "type": "high",
+ "index": 1,
+ "isBlocking": false,
+ "formats": [
+ "docbook",
+ "adoc"
+ ]
+}
diff --git a/src/test/resources/api/placeholders/customPlaceholder.json b/src/test/resources/api/placeholders/customPlaceholder.json
new file mode 100644
index 00000000..83f376c0
--- /dev/null
+++ b/src/test/resources/api/placeholders/customPlaceholder.json
@@ -0,0 +1,8 @@
+{
+ "data": {
+ "id": 1,
+ "definition": "start, then \"http\", maybe \"s\", then \"://\", end",
+ "description": "URL validation",
+ "argumentDelimiter": "\""
+ }
+}
diff --git a/src/test/resources/api/placeholders/editCustomPlaceholderRequest.json b/src/test/resources/api/placeholders/editCustomPlaceholderRequest.json
new file mode 100644
index 00000000..083f6e7d
--- /dev/null
+++ b/src/test/resources/api/placeholders/editCustomPlaceholderRequest.json
@@ -0,0 +1,7 @@
+[
+ {
+ "op": "replace",
+ "path": "/description",
+ "value": "URL validation"
+ }
+]
diff --git a/src/test/resources/api/placeholders/editProjectPlaceholderRequest.json b/src/test/resources/api/placeholders/editProjectPlaceholderRequest.json
new file mode 100644
index 00000000..0e9ae76c
--- /dev/null
+++ b/src/test/resources/api/placeholders/editProjectPlaceholderRequest.json
@@ -0,0 +1,7 @@
+[
+ {
+ "op": "replace",
+ "path": "/isBlocking",
+ "value": true
+ }
+]
diff --git a/src/test/resources/api/placeholders/editSystemPlaceholders.json b/src/test/resources/api/placeholders/editSystemPlaceholders.json
new file mode 100644
index 00000000..39b89668
--- /dev/null
+++ b/src/test/resources/api/placeholders/editSystemPlaceholders.json
@@ -0,0 +1,15 @@
+{
+ "data": [
+ {
+ "data": {
+ "id": "printfSpecifier",
+ "isEnabled": false,
+ "label": "C-style format specifiers",
+ "examples": [
+ "%s"
+ ],
+ "description": "C, PHP, Java, Objective-C, Android, and Qt"
+ }
+ }
+ ]
+}
diff --git a/src/test/resources/api/placeholders/editSystemPlaceholdersRequest.json b/src/test/resources/api/placeholders/editSystemPlaceholdersRequest.json
new file mode 100644
index 00000000..726fb08d
--- /dev/null
+++ b/src/test/resources/api/placeholders/editSystemPlaceholdersRequest.json
@@ -0,0 +1,7 @@
+[
+ {
+ "op": "replace",
+ "path": "/printfSpecifier/isEnabled",
+ "value": false
+ }
+]
diff --git a/src/test/resources/api/placeholders/listCustomPlaceholders.json b/src/test/resources/api/placeholders/listCustomPlaceholders.json
new file mode 100644
index 00000000..93c34603
--- /dev/null
+++ b/src/test/resources/api/placeholders/listCustomPlaceholders.json
@@ -0,0 +1,16 @@
+{
+ "data": [
+ {
+ "data": {
+ "id": 1,
+ "definition": "start, then \"http\", maybe \"s\", then \"://\", end",
+ "description": "URL validation",
+ "argumentDelimiter": "\""
+ }
+ }
+ ],
+ "pagination": {
+ "offset": 0,
+ "limit": 25
+ }
+}
diff --git a/src/test/resources/api/placeholders/listProjectPlaceholders.json b/src/test/resources/api/placeholders/listProjectPlaceholders.json
new file mode 100644
index 00000000..23776f44
--- /dev/null
+++ b/src/test/resources/api/placeholders/listProjectPlaceholders.json
@@ -0,0 +1,21 @@
+{
+ "data": [
+ {
+ "data": {
+ "id": 2,
+ "customPlaceholderId": 1,
+ "type": "high",
+ "index": 1,
+ "isBlocking": false,
+ "formats": [
+ "docbook",
+ "adoc"
+ ]
+ }
+ }
+ ],
+ "pagination": {
+ "offset": 0,
+ "limit": 25
+ }
+}
diff --git a/src/test/resources/api/placeholders/listSystemPlaceholders.json b/src/test/resources/api/placeholders/listSystemPlaceholders.json
new file mode 100644
index 00000000..107865ab
--- /dev/null
+++ b/src/test/resources/api/placeholders/listSystemPlaceholders.json
@@ -0,0 +1,19 @@
+{
+ "data": [
+ {
+ "data": {
+ "id": "printfSpecifier",
+ "isEnabled": true,
+ "label": "C-style format specifiers",
+ "examples": [
+ "%s"
+ ],
+ "description": "C, PHP, Java, Objective-C, Android, and Qt"
+ }
+ }
+ ],
+ "pagination": {
+ "offset": 0,
+ "limit": 25
+ }
+}
diff --git a/src/test/resources/api/placeholders/projectPlaceholder.json b/src/test/resources/api/placeholders/projectPlaceholder.json
new file mode 100644
index 00000000..29c5a3d1
--- /dev/null
+++ b/src/test/resources/api/placeholders/projectPlaceholder.json
@@ -0,0 +1,13 @@
+{
+ "data": {
+ "id": 2,
+ "customPlaceholderId": 1,
+ "type": "high",
+ "index": 1,
+ "isBlocking": false,
+ "formats": [
+ "docbook",
+ "adoc"
+ ]
+ }
+}