From 79ade7afa988e11ddfc2bcd9d3d533a798888a47 Mon Sep 17 00:00:00 2001 From: Kateryna Oblakevych Date: Tue, 3 Nov 2026 20:00:42 -0800 Subject: [PATCH 1/3] fix(translations): require a non-empty string content type for asset translations Unknown language translation types are read as assets only when contentType is a non-empty string outside Crowdin's application/vnd.crowdin.* text types and the item has a url without text or plurals. Other unknown shapes stay rejected. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../LanguageTranslationsDeserializer.java | 18 ++++- ...nguageTranslationsDeserializationTest.java | 72 +++++++++++++++++++ 2 files changed, 87 insertions(+), 3 deletions(-) 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/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(); } From 56cf5aad6e1f8652e4b7ab92800cff15a83dc4c1 Mon Sep 17 00:00:00 2001 From: Kateryna Oblakevych Date: Tue, 3 Nov 2026 20:00:42 -0800 Subject: [PATCH 2/3] feat: add Advisors, Placeholders and Organization APIs - Advisors: run and get advisor checks, list and edit insights, upsert an application module insight - Placeholders: system placeholders (list, batch edit), organization custom placeholders and project placeholders (list, add, get, edit, delete) - Organization: get organization and auth settings, custom spellcheckers, external QA checks list - Organization.isVendor accepts a boolean or a string and keeps the raw value - GET /external-qa-checks/{id} is not added: the spec documents a collection response for a single resource Co-Authored-By: Claude Opus 5.5 (1M context) --- src/main/java/com/crowdin/client/Client.java | 12 + .../crowdin/client/advisors/AdvisorsApi.java | 124 ++++++++++ .../client/advisors/model/AdvisorAiMode.java | 21 ++ .../client/advisors/model/AdvisorAiRun.java | 13 ++ .../client/advisors/model/AdvisorCheck.java | 21 ++ .../model/AdvisorCheckAttributes.java | 12 + .../advisors/model/AdvisorCheckInspector.java | 9 + .../model/AdvisorCheckResponseObject.java | 9 + .../client/advisors/model/AdvisorInsight.java | 28 +++ .../advisors/model/AdvisorInsightMetric.java | 17 ++ .../advisors/model/AdvisorInsightOutcome.java | 21 ++ .../model/AdvisorInsightPagination.java | 18 ++ .../model/AdvisorInsightRecommendation.java | 16 ++ .../model/AdvisorInsightResponseList.java | 25 ++ .../model/AdvisorInsightResponseObject.java | 9 + .../model/AdvisorInsightSeverity.java | 21 ++ .../advisors/model/AdvisorInsightStatus.java | 21 ++ .../model/AdvisorInspectorOptions.java | 10 + .../advisors/model/AdvisorMetricSource.java | 21 ++ .../advisors/model/AdvisorMetricTone.java | 21 ++ .../advisors/model/AdvisorMetricUnit.java | 21 ++ .../advisors/model/AdvisorRefreshPolicy.java | 21 ++ .../model/ListAdvisorInsightsParams.java | 16 ++ .../model/RunAdvisorCheckInspector.java | 10 + .../model/RunAdvisorCheckRequest.java | 15 ++ .../model/UpsertAdvisorInsightRequest.java | 17 ++ .../client/organization/OrganizationApi.java | 102 +++++++++ .../client/organization/model/AuthMethod.java | 11 + .../model/CustomSpellchecker.java | 15 ++ .../model/CustomSpellcheckerConfig.java | 14 ++ .../model/CustomSpellcheckerResponseList.java | 26 +++ .../CustomSpellcheckerResponseObject.java | 9 + .../organization/model/ExternalQaCheck.java | 20 ++ .../model/ExternalQaCheckResponseList.java | 26 +++ .../model/ExternalQaCheckResponseObject.java | 9 + .../organization/model/Organization.java | 65 ++++++ .../model/OrganizationAuthSettings.java | 13 ++ ...rganizationAuthSettingsResponseObject.java | 9 + .../model/OrganizationDefault.java | 11 + .../organization/model/OrganizationPlan.java | 11 + .../model/OrganizationResponseObject.java | 9 + .../model/PublicProjectsView.java | 21 ++ .../client/placeholders/PlaceholdersApi.java | 203 ++++++++++++++++ .../AddOrganizationPlaceholderRequest.java | 11 + .../model/AddProjectPlaceholderRequest.java | 15 ++ .../model/OrganizationPlaceholder.java | 16 ++ .../OrganizationPlaceholderResponseList.java | 26 +++ ...OrganizationPlaceholderResponseObject.java | 9 + .../model/ProjectPlaceholder.java | 19 ++ .../model/ProjectPlaceholderResponseList.java | 26 +++ .../ProjectPlaceholderResponseObject.java | 9 + .../model/ProjectPlaceholderType.java | 21 ++ .../placeholders/model/SystemPlaceholder.java | 21 ++ .../model/SystemPlaceholderResponseList.java | 26 +++ .../SystemPlaceholderResponseObject.java | 9 + .../client/advisors/AdvisorsApiTest.java | 216 ++++++++++++++++++ .../organization/OrganizationApiTest.java | 149 ++++++++++++ .../OrganizationIsVendorTest.java | 64 ++++++ .../placeholders/PlaceholdersApiTest.java | 185 +++++++++++++++ .../resources/api/advisors/advisorCheck.json | 19 ++ .../api/advisors/advisorInsight.json | 41 ++++ .../advisors/editAdvisorInsightRequest.json | 7 + .../api/advisors/listAdvisorInsights.json | 73 ++++++ .../api/advisors/runAdvisorCheckRequest.json | 11 + .../advisors/upsertAdvisorInsightRequest.json | 26 +++ .../api/organization/authSettings.json | 13 ++ .../api/organization/customSpellchecker.json | 17 ++ .../organization/listCustomSpellcheckers.json | 25 ++ .../organization/listExternalQaChecks.json | 21 ++ .../api/organization/organization.json | 26 +++ .../organizationStringIsVendor.json | 6 + .../addCustomPlaceholderRequest.json | 5 + .../addProjectPlaceholderRequest.json | 10 + .../api/placeholders/customPlaceholder.json | 8 + .../editCustomPlaceholderRequest.json | 7 + .../editProjectPlaceholderRequest.json | 7 + .../placeholders/editSystemPlaceholders.json | 15 ++ .../editSystemPlaceholdersRequest.json | 7 + .../placeholders/listCustomPlaceholders.json | 16 ++ .../placeholders/listProjectPlaceholders.json | 21 ++ .../placeholders/listSystemPlaceholders.json | 19 ++ .../api/placeholders/projectPlaceholder.json | 13 ++ 82 files changed, 2358 insertions(+) create mode 100644 src/main/java/com/crowdin/client/advisors/AdvisorsApi.java create mode 100644 src/main/java/com/crowdin/client/advisors/model/AdvisorAiMode.java create mode 100644 src/main/java/com/crowdin/client/advisors/model/AdvisorAiRun.java create mode 100644 src/main/java/com/crowdin/client/advisors/model/AdvisorCheck.java create mode 100644 src/main/java/com/crowdin/client/advisors/model/AdvisorCheckAttributes.java create mode 100644 src/main/java/com/crowdin/client/advisors/model/AdvisorCheckInspector.java create mode 100644 src/main/java/com/crowdin/client/advisors/model/AdvisorCheckResponseObject.java create mode 100644 src/main/java/com/crowdin/client/advisors/model/AdvisorInsight.java create mode 100644 src/main/java/com/crowdin/client/advisors/model/AdvisorInsightMetric.java create mode 100644 src/main/java/com/crowdin/client/advisors/model/AdvisorInsightOutcome.java create mode 100644 src/main/java/com/crowdin/client/advisors/model/AdvisorInsightPagination.java create mode 100644 src/main/java/com/crowdin/client/advisors/model/AdvisorInsightRecommendation.java create mode 100644 src/main/java/com/crowdin/client/advisors/model/AdvisorInsightResponseList.java create mode 100644 src/main/java/com/crowdin/client/advisors/model/AdvisorInsightResponseObject.java create mode 100644 src/main/java/com/crowdin/client/advisors/model/AdvisorInsightSeverity.java create mode 100644 src/main/java/com/crowdin/client/advisors/model/AdvisorInsightStatus.java create mode 100644 src/main/java/com/crowdin/client/advisors/model/AdvisorInspectorOptions.java create mode 100644 src/main/java/com/crowdin/client/advisors/model/AdvisorMetricSource.java create mode 100644 src/main/java/com/crowdin/client/advisors/model/AdvisorMetricTone.java create mode 100644 src/main/java/com/crowdin/client/advisors/model/AdvisorMetricUnit.java create mode 100644 src/main/java/com/crowdin/client/advisors/model/AdvisorRefreshPolicy.java create mode 100644 src/main/java/com/crowdin/client/advisors/model/ListAdvisorInsightsParams.java create mode 100644 src/main/java/com/crowdin/client/advisors/model/RunAdvisorCheckInspector.java create mode 100644 src/main/java/com/crowdin/client/advisors/model/RunAdvisorCheckRequest.java create mode 100644 src/main/java/com/crowdin/client/advisors/model/UpsertAdvisorInsightRequest.java create mode 100644 src/main/java/com/crowdin/client/organization/OrganizationApi.java create mode 100644 src/main/java/com/crowdin/client/organization/model/AuthMethod.java create mode 100644 src/main/java/com/crowdin/client/organization/model/CustomSpellchecker.java create mode 100644 src/main/java/com/crowdin/client/organization/model/CustomSpellcheckerConfig.java create mode 100644 src/main/java/com/crowdin/client/organization/model/CustomSpellcheckerResponseList.java create mode 100644 src/main/java/com/crowdin/client/organization/model/CustomSpellcheckerResponseObject.java create mode 100644 src/main/java/com/crowdin/client/organization/model/ExternalQaCheck.java create mode 100644 src/main/java/com/crowdin/client/organization/model/ExternalQaCheckResponseList.java create mode 100644 src/main/java/com/crowdin/client/organization/model/ExternalQaCheckResponseObject.java create mode 100644 src/main/java/com/crowdin/client/organization/model/Organization.java create mode 100644 src/main/java/com/crowdin/client/organization/model/OrganizationAuthSettings.java create mode 100644 src/main/java/com/crowdin/client/organization/model/OrganizationAuthSettingsResponseObject.java create mode 100644 src/main/java/com/crowdin/client/organization/model/OrganizationDefault.java create mode 100644 src/main/java/com/crowdin/client/organization/model/OrganizationPlan.java create mode 100644 src/main/java/com/crowdin/client/organization/model/OrganizationResponseObject.java create mode 100644 src/main/java/com/crowdin/client/organization/model/PublicProjectsView.java create mode 100644 src/main/java/com/crowdin/client/placeholders/PlaceholdersApi.java create mode 100644 src/main/java/com/crowdin/client/placeholders/model/AddOrganizationPlaceholderRequest.java create mode 100644 src/main/java/com/crowdin/client/placeholders/model/AddProjectPlaceholderRequest.java create mode 100644 src/main/java/com/crowdin/client/placeholders/model/OrganizationPlaceholder.java create mode 100644 src/main/java/com/crowdin/client/placeholders/model/OrganizationPlaceholderResponseList.java create mode 100644 src/main/java/com/crowdin/client/placeholders/model/OrganizationPlaceholderResponseObject.java create mode 100644 src/main/java/com/crowdin/client/placeholders/model/ProjectPlaceholder.java create mode 100644 src/main/java/com/crowdin/client/placeholders/model/ProjectPlaceholderResponseList.java create mode 100644 src/main/java/com/crowdin/client/placeholders/model/ProjectPlaceholderResponseObject.java create mode 100644 src/main/java/com/crowdin/client/placeholders/model/ProjectPlaceholderType.java create mode 100644 src/main/java/com/crowdin/client/placeholders/model/SystemPlaceholder.java create mode 100644 src/main/java/com/crowdin/client/placeholders/model/SystemPlaceholderResponseList.java create mode 100644 src/main/java/com/crowdin/client/placeholders/model/SystemPlaceholderResponseObject.java create mode 100644 src/test/java/com/crowdin/client/advisors/AdvisorsApiTest.java create mode 100644 src/test/java/com/crowdin/client/organization/OrganizationApiTest.java create mode 100644 src/test/java/com/crowdin/client/organization/OrganizationIsVendorTest.java create mode 100644 src/test/java/com/crowdin/client/placeholders/PlaceholdersApiTest.java create mode 100644 src/test/resources/api/advisors/advisorCheck.json create mode 100644 src/test/resources/api/advisors/advisorInsight.json create mode 100644 src/test/resources/api/advisors/editAdvisorInsightRequest.json create mode 100644 src/test/resources/api/advisors/listAdvisorInsights.json create mode 100644 src/test/resources/api/advisors/runAdvisorCheckRequest.json create mode 100644 src/test/resources/api/advisors/upsertAdvisorInsightRequest.json create mode 100644 src/test/resources/api/organization/authSettings.json create mode 100644 src/test/resources/api/organization/customSpellchecker.json create mode 100644 src/test/resources/api/organization/listCustomSpellcheckers.json create mode 100644 src/test/resources/api/organization/listExternalQaChecks.json create mode 100644 src/test/resources/api/organization/organization.json create mode 100644 src/test/resources/api/organization/organizationStringIsVendor.json create mode 100644 src/test/resources/api/placeholders/addCustomPlaceholderRequest.json create mode 100644 src/test/resources/api/placeholders/addProjectPlaceholderRequest.json create mode 100644 src/test/resources/api/placeholders/customPlaceholder.json create mode 100644 src/test/resources/api/placeholders/editCustomPlaceholderRequest.json create mode 100644 src/test/resources/api/placeholders/editProjectPlaceholderRequest.json create mode 100644 src/test/resources/api/placeholders/editSystemPlaceholders.json create mode 100644 src/test/resources/api/placeholders/editSystemPlaceholdersRequest.json create mode 100644 src/test/resources/api/placeholders/listCustomPlaceholders.json create mode 100644 src/test/resources/api/placeholders/listProjectPlaceholders.json create mode 100644 src/test/resources/api/placeholders/listSystemPlaceholders.json create mode 100644 src/test/resources/api/placeholders/projectPlaceholder.json 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/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..1c692e3f --- /dev/null +++ b/src/main/java/com/crowdin/client/organization/model/Organization.java @@ -0,0 +1,65 @@ +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; + + /** + * 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.isVendor = null; + this.isVendorRaw = null; + return; + } + this.isVendorRaw = value.isTextual() ? value.textValue() : value.toString(); + if (value.isBoolean()) { + this.isVendor = value.booleanValue(); + } else if ("true".equalsIgnoreCase(this.isVendorRaw) || "false".equalsIgnoreCase(this.isVendorRaw)) { + this.isVendor = Boolean.valueOf(this.isVendorRaw); + } else { + this.isVendor = 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..5a24fb67 --- /dev/null +++ b/src/test/java/com/crowdin/client/organization/OrganizationIsVendorTest.java @@ -0,0 +1,64 @@ +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)); + } + + 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/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" + ] + } +} From 802c9ddf9dfa0c1075a7d3dd13ef0cc5ce20efe5 Mon Sep 17 00:00:00 2001 From: Kateryna Oblakevych Date: Tue, 3 Nov 2026 20:22:28 -0800 Subject: [PATCH 3/3] fix(organization): keep isVendor and isVendorRaw in sync in setters setIsVendor also sets the raw value ("true" / "false"), setIsVendorRaw keeps the raw string and recomputes the boolean with the same rules as the JSON reader, and null clears both fields. Signatures and JSON output are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../organization/model/Organization.java | 44 +++++++--- .../OrganizationIsVendorTest.java | 87 +++++++++++++++++++ 2 files changed, 121 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/crowdin/client/organization/model/Organization.java b/src/main/java/com/crowdin/client/organization/model/Organization.java index 1c692e3f..67d61ea5 100644 --- a/src/main/java/com/crowdin/client/organization/model/Organization.java +++ b/src/main/java/com/crowdin/client/organization/model/Organization.java @@ -35,24 +35,48 @@ public class Organization { 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.isVendor = null; - this.isVendorRaw = null; - return; - } - this.isVendorRaw = value.isTextual() ? value.textValue() : value.toString(); - if (value.isBoolean()) { - this.isVendor = value.booleanValue(); - } else if ("true".equalsIgnoreCase(this.isVendorRaw) || "false".equalsIgnoreCase(this.isVendorRaw)) { - this.isVendor = Boolean.valueOf(this.isVendorRaw); + this.setIsVendorRaw(null); + } else if (value.isBoolean()) { + this.setIsVendor(value.booleanValue()); } else { - this.isVendor = null; + 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; } /** diff --git a/src/test/java/com/crowdin/client/organization/OrganizationIsVendorTest.java b/src/test/java/com/crowdin/client/organization/OrganizationIsVendorTest.java index 5a24fb67..2bc9fd9c 100644 --- a/src/test/java/com/crowdin/client/organization/OrganizationIsVendorTest.java +++ b/src/test/java/com/crowdin/client/organization/OrganizationIsVendorTest.java @@ -55,6 +55,93 @@ void serializationTest() { 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);