diff --git a/src/main/java/com/crowdin/cli/commands/Actions.java b/src/main/java/com/crowdin/cli/commands/Actions.java index db446dfa9..09640cbd4 100644 --- a/src/main/java/com/crowdin/cli/commands/Actions.java +++ b/src/main/java/com/crowdin/cli/commands/Actions.java @@ -28,7 +28,8 @@ NewAction download( boolean ignoreMatch, boolean isVerbose, boolean plainView, boolean userServerSources, boolean keepArchive ); - NewAction generate(FilesInterface files, Path destinationPath, boolean skipGenerateDescription); + NewAction generate(FilesInterface files, String token, String baseUrl, String basePath, + String projectId, String source, String translation, Boolean preserveHierarchy, Path destinationPath, boolean skipGenerateDescription); NewAction listBranches(boolean noProgress, boolean plainView); diff --git a/src/main/java/com/crowdin/cli/commands/actions/CliActions.java b/src/main/java/com/crowdin/cli/commands/actions/CliActions.java index eef1b3a87..20f1ee80b 100644 --- a/src/main/java/com/crowdin/cli/commands/actions/CliActions.java +++ b/src/main/java/com/crowdin/cli/commands/actions/CliActions.java @@ -34,8 +34,10 @@ public NewAction download( } @Override - public NewAction generate(FilesInterface files, Path destinationPath, boolean skipGenerateDescription) { - return new GenerateAction(files, destinationPath, skipGenerateDescription); + public NewAction generate(FilesInterface files, String token, String baseUrl, String basePath, + String projectId, String source, String translation, Boolean preserveHierarchy, Path destinationPath, boolean skipGenerateDescription + ) { + return new GenerateAction(files, token, baseUrl, basePath, projectId, source, translation, preserveHierarchy, destinationPath, skipGenerateDescription); } @Override diff --git a/src/main/java/com/crowdin/cli/commands/actions/GenerateAction.java b/src/main/java/com/crowdin/cli/commands/actions/GenerateAction.java index a8479e6c7..aa0d5ee3e 100644 --- a/src/main/java/com/crowdin/cli/commands/actions/GenerateAction.java +++ b/src/main/java/com/crowdin/cli/commands/actions/GenerateAction.java @@ -12,6 +12,7 @@ import com.crowdin.cli.utils.console.ConsoleSpinner; import com.crowdin.cli.utils.console.ExecutionStatus; import com.crowdin.cli.utils.http.OAuthUtil; +import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import java.io.ByteArrayInputStream; @@ -19,21 +20,19 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Scanner; +import java.util.*; import static com.crowdin.cli.BaseCli.OAUTH_CLIENT_ID; import static com.crowdin.cli.BaseCli.RESOURCE_BUNDLE; -import static com.crowdin.cli.properties.PropertiesBuilder.API_TOKEN; -import static com.crowdin.cli.properties.PropertiesBuilder.BASE_PATH; -import static com.crowdin.cli.properties.PropertiesBuilder.BASE_URL; -import static com.crowdin.cli.properties.PropertiesBuilder.PROJECT_ID; +import static com.crowdin.cli.properties.PropertiesBuilder.*; import static com.crowdin.cli.utils.console.ExecutionStatus.ERROR; import static com.crowdin.cli.utils.console.ExecutionStatus.OK; import static com.crowdin.cli.utils.console.ExecutionStatus.WARNING; +import static java.lang.Boolean.TRUE; +import static java.util.Objects.isNull; +import static java.util.Objects.nonNull; +@RequiredArgsConstructor class GenerateAction implements NewAction { public static final String BASE_PATH_DEFAULT = "."; @@ -46,15 +45,16 @@ class GenerateAction implements NewAction { public static final String LINK = "https://support.crowdin.com/configuration-file/"; public static final String ENTERPRISE_LINK = "https://support.crowdin.com/enterprise/configuration-file/"; - private FilesInterface files; - private Path destinationPath; - private boolean skipGenerateDescription; - - public GenerateAction(FilesInterface files, Path destinationPath, boolean skipGenerateDescription) { - this.files = files; - this.destinationPath = destinationPath; - this.skipGenerateDescription = skipGenerateDescription; - } + private final FilesInterface files; + private final String token; + private final String baseUrl; + private final String basePath; + private final String projectId; + private final String source; + private final String translation; + private final Boolean preserveHierarchy; + private final Path destinationPath; + private final boolean skipGenerateDescription; @Override public void act(Outputter out, NoProperties noProperties, NoClient noClient) { @@ -87,8 +87,9 @@ public void act(Outputter out, NoProperties noProperties, NoClient noClient) { private void updateWithUserInputs(Outputter out, Asking asking, List fileLines) { Map values = new HashMap<>(); + setGivenParams(values); - withBrowser = !StringUtils.startsWithAny(asking.ask( + withBrowser = isNull(token) && !StringUtils.startsWithAny(asking.ask( RESOURCE_BUNDLE.getString("message.ask_auth_via_browser") + ": (Y/n) "), "n", "N", "-"); if (withBrowser) { String token; @@ -108,38 +109,45 @@ private void updateWithUserInputs(Outputter out, Asking asking, List fil values.put(BASE_URL, BASE_URL_DEFAULT); } } else { - this.isEnterprise = StringUtils.startsWithAny(asking.ask( + if (isNull(baseUrl)) { + this.isEnterprise = StringUtils.startsWithAny(asking.ask( RESOURCE_BUNDLE.getString("message.ask_is_enterprise") + ": (N/y) "), "y", "Y", "+"); - if (this.isEnterprise) { - String organizationName = asking.ask(RESOURCE_BUNDLE.getString("message.ask_organization_name") + ": "); - if (StringUtils.isNotEmpty(organizationName)) { - if (PropertiesBeanUtils.isUrlValid(organizationName)) { - String realOrganizationName = PropertiesBeanUtils.getOrganization(organizationName); - System.out.println(String.format(RESOURCE_BUNDLE.getString("message.extracted_organization_name"), realOrganizationName)); - values.put(BASE_URL, String.format(BASE_ENTERPRISE_URL_DEFAULT, realOrganizationName)); + if (this.isEnterprise) { + String organizationName = asking.ask(RESOURCE_BUNDLE.getString("message.ask_organization_name") + ": "); + if (StringUtils.isNotEmpty(organizationName)) { + if (PropertiesBeanUtils.isUrlValid(organizationName)) { + String realOrganizationName = PropertiesBeanUtils.getOrganization(organizationName); + System.out.println(String.format(RESOURCE_BUNDLE.getString("message.extracted_organization_name"), realOrganizationName)); + values.put(BASE_URL, String.format(BASE_ENTERPRISE_URL_DEFAULT, realOrganizationName)); + } else { + values.put(BASE_URL, String.format(BASE_ENTERPRISE_URL_DEFAULT, PropertiesBeanUtils.getOrganization(organizationName))); + } } else { - values.put(BASE_URL, String.format(BASE_ENTERPRISE_URL_DEFAULT, PropertiesBeanUtils.getOrganization(organizationName))); + this.isEnterprise = false; + values.put(BASE_URL, BASE_URL_DEFAULT); } } else { - this.isEnterprise = false; values.put(BASE_URL, BASE_URL_DEFAULT); } - } else { - values.put(BASE_URL, BASE_URL_DEFAULT); } - String apiToken = asking.askParam(API_TOKEN); - if (!apiToken.isEmpty()) { - values.put(API_TOKEN, apiToken); + if (isNull(token)) { + String apiToken = asking.askParam(API_TOKEN); + if (!apiToken.isEmpty()) { + values.put(API_TOKEN, apiToken); + } } } + boolean projectIdSpecified = nonNull(projectId); while (true) { - String projectId = asking.askParam(PROJECT_ID); - if (projectId.isEmpty()) { + String projectIdToSet = projectIdSpecified ? projectId : asking.askParam(PROJECT_ID); + if (projectIdToSet.isEmpty()) { break; - } else if (StringUtils.isNumeric(projectId)) { - values.put(PROJECT_ID, projectId); + } else if (StringUtils.isNumeric(projectIdToSet)) { + values.put(PROJECT_ID, projectIdToSet); break; } else { + projectIdSpecified = false; + values.remove(PROJECT_ID); System.out.println(String.format(RESOURCE_BUNDLE.getString("error.init.project_id_is_not_number"), projectId)); } } @@ -148,23 +156,37 @@ private void updateWithUserInputs(Outputter out, Asking asking, List fil } else { System.out.println(WARNING.withIcon(RESOURCE_BUNDLE.getString("error.init.skip_project_validation"))); } - String basePath = asking.askWithDefault(RESOURCE_BUNDLE.getString("message.ask_project_directory"), BASE_PATH_DEFAULT); - java.io.File basePathFile = Paths.get(basePath).normalize().toAbsolutePath().toFile(); + String basePathToSet = nonNull(basePath) ? basePath : asking.askWithDefault(RESOURCE_BUNDLE.getString("message.ask_project_directory"), BASE_PATH_DEFAULT); + java.io.File basePathFile = Paths.get(basePathToSet).normalize().toAbsolutePath().toFile(); if (!basePathFile.exists()) { System.out.println(WARNING.withIcon(String.format(RESOURCE_BUNDLE.getString("error.init.path_not_exist"), basePathFile))); } - values.put(BASE_PATH, basePath); + values.put(BASE_PATH, basePathToSet); for (Map.Entry entry : values.entrySet()) { for (int i = 0; i < fileLines.size(); i++) { - if (fileLines.get(i).contains(entry.getKey())) { - fileLines.set(i, fileLines.get(i).replaceFirst(": \"*\"", String.format(": \"%s\"", Utils.regexPath(entry.getValue())))); + String keyToSearch = String.format("\"%s\"", entry.getKey()); + if (fileLines.get(i).contains(keyToSearch)) { + String updatedLine = PRESERVE_HIERARCHY.equals(entry.getKey()) ? + fileLines.get(i).replace(String.valueOf(TRUE), entry.getValue()) + : fileLines.get(i).replaceFirst(": \"*\"", String.format(": \"%s\"", Utils.regexPath(entry.getValue()))); + fileLines.set(i, updatedLine); break; } } } } + private void setGivenParams(Map values) { + Optional.ofNullable(token).ifPresent(v -> values.put(API_TOKEN, token)); + Optional.ofNullable(baseUrl).ifPresent(v -> values.put(BASE_URL, baseUrl)); + Optional.ofNullable(basePath).ifPresent(v -> values.put(BASE_PATH, basePath)); + Optional.ofNullable(projectId).ifPresent(v -> values.put(PROJECT_ID, projectId)); + Optional.ofNullable(source).ifPresent(v -> values.put(SOURCE, source)); + Optional.ofNullable(translation).ifPresent(v -> values.put(TRANSLATION, translation)); + Optional.ofNullable(preserveHierarchy).ifPresent(v -> values.put(PRESERVE_HIERARCHY, String.valueOf(preserveHierarchy))); + } + public static class Asking { private Outputter out; diff --git a/src/main/java/com/crowdin/cli/commands/picocli/GenerateSubcommand.java b/src/main/java/com/crowdin/cli/commands/picocli/GenerateSubcommand.java index 81286889e..24653b9af 100644 --- a/src/main/java/com/crowdin/cli/commands/picocli/GenerateSubcommand.java +++ b/src/main/java/com/crowdin/cli/commands/picocli/GenerateSubcommand.java @@ -14,17 +14,40 @@ @CommandLine.Command( name = CommandNames.GENERATE, - aliases = CommandNames.ALIAS_GENERATE) + aliases = CommandNames.ALIAS_GENERATE, + sortOptions = false +) public class GenerateSubcommand extends GenericActCommand { - @CommandLine.Option(names = {"-d", "--destination"}, paramLabel = "...", defaultValue = "crowdin.yml") + @CommandLine.Option(names = {"-d", "--destination"}, paramLabel = "...", descriptionKey = "crowdin.generate.destination", defaultValue = "crowdin.yml", order = -2) private Path destinationPath; + @CommandLine.Option(names = {"-T", "--token"}, paramLabel = "...", descriptionKey = "params.token", order = -2) + private String token; + + @CommandLine.Option(names = {"-i", "--project-id"}, paramLabel = "...", descriptionKey = "params.project-id", order = -2) + private String projectId; + + @CommandLine.Option(names = {"--base-path"}, paramLabel = "...", descriptionKey = "params.base-path", order = -2) + private String basePath; + + @CommandLine.Option(names = {"--base-url"}, paramLabel = "...", descriptionKey = "params.base-url", order = -2) + private String baseUrl; + + @CommandLine.Option(names = {"-s", "--source"}, paramLabel = "...", descriptionKey = "params.source", order = -2) + private String source; + + @CommandLine.Option(names = {"-t", "--translation"}, paramLabel = "...", descriptionKey = "params.translation", order = -2) + private String translation; + + @CommandLine.Option(names = {"--preserve-hierarchy"}, negatable = true, paramLabel = "...", descriptionKey = "params.preserve-hierarchy", order = -2) + private Boolean preserveHierarchy; + @CommandLine.Option(names = "--skip-generate-description", hidden = true) private boolean skipGenerateDescription; protected NewAction getAction(Actions actions) { - return actions.generate(new FsFiles(), destinationPath, skipGenerateDescription); + return actions.generate(new FsFiles(), token, baseUrl, basePath, projectId, source, translation, preserveHierarchy, destinationPath, skipGenerateDescription); } protected NoProperties getProperties(PropertiesBuilders propertiesBuilders, Outputter out) { diff --git a/src/test/java/com/crowdin/cli/commands/actions/CliActionsTest.java b/src/test/java/com/crowdin/cli/commands/actions/CliActionsTest.java index bdf61204f..f513e67ee 100644 --- a/src/test/java/com/crowdin/cli/commands/actions/CliActionsTest.java +++ b/src/test/java/com/crowdin/cli/commands/actions/CliActionsTest.java @@ -19,7 +19,7 @@ public void testDownload() { @Test public void testGenerate() { - assertNotNull(actions.generate(new FsFiles(), null, false)); + assertNotNull(actions.generate(new FsFiles(), null, null, null, null, null, null, null, null, false)); } @Test diff --git a/src/test/java/com/crowdin/cli/commands/actions/GenerateActionTest.java b/src/test/java/com/crowdin/cli/commands/actions/GenerateActionTest.java index 271e1011a..b129ea2fa 100644 --- a/src/test/java/com/crowdin/cli/commands/actions/GenerateActionTest.java +++ b/src/test/java/com/crowdin/cli/commands/actions/GenerateActionTest.java @@ -10,20 +10,19 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; +import java.io.*; import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.*; public class GenerateActionTest { @@ -47,13 +46,48 @@ public void simpleTest() throws IOException { InputStream responsesIS = setResponses(false, false, "apiToken", "42", "."); System.setIn(responsesIS); - action = new GenerateAction(files, Paths.get(project.getBasePath() + "/crowdin.yml"), false); + action = new GenerateAction(files, null, null, null, null, null, null, null, Paths.get(project.getBasePath() + "/crowdin.yml"), false); action.act(Outputter.getDefault(), new NoProperties(), mock(NoClient.class)); verify(files).writeToFile(anyString(), any()); verifyNoMoreInteractions(files); } + @Test + public void userInputTest() throws IOException { + FilesInterface files = mock(FilesInterface.class); + + action = new GenerateAction(files, "token", "", ".", "42", "file.json", "translation.json", true, Paths.get(project.getBasePath() + "/crowdin.yml"), false); + action.act(Outputter.getDefault(), new NoProperties(), mock(NoClient.class)); + + verify(files).writeToFile(anyString(), any()); + verifyNoMoreInteractions(files); + } + + @Test + public void userInputAllTest() throws IOException { + FilesInterface files = mock(FilesInterface.class); + + action = new GenerateAction(files, "token", "https://api.crowdin.com", ".", "42", "file.json", "translation.json", true, Paths.get(project.getBasePath() + "/crowdin.yml"), false); + action.act(Outputter.getDefault(), new NoProperties(), mock(NoClient.class)); + + ArgumentCaptor contentCaptor = ArgumentCaptor.forClass(InputStream.class); + verify(files).writeToFile(anyString(), contentCaptor.capture()); + verifyNoMoreInteractions(files); + + List actualLines = new BufferedReader(new InputStreamReader(contentCaptor.getValue(), UTF_8)) + .lines() + .collect(Collectors.toList()); + + assertTrue(actualLines.contains("\"project_id\": \"42\"")); + assertTrue(actualLines.contains("\"api_token\": \"token\"")); + assertTrue(actualLines.contains("\"base_path\": \".\"")); + assertTrue(actualLines.contains("\"base_url\": \"https://api.crowdin.com\"")); + assertTrue(actualLines.contains("\"preserve_hierarchy\": true")); + assertTrue(actualLines.contains(" \"source\": \"file.json\",")); + assertTrue(actualLines.contains(" \"translation\": \"translation.json\",")); + } + @Test public void writeToFileThrowsTest() throws IOException { FilesInterface files = mock(FilesInterface.class); @@ -61,7 +95,7 @@ public void writeToFileThrowsTest() throws IOException { InputStream responsesIS = setResponses(false, false, "apiToken", "42", "."); System.setIn(responsesIS); - action = new GenerateAction(files, Paths.get(project.getBasePath() + "/crowdin.yml"), false); + action = new GenerateAction(files, null, null, null, null, null, null, null, Paths.get(project.getBasePath() + "/crowdin.yml"), false); assertThrows(RuntimeException.class, () -> action.act(Outputter.getDefault(), new NoProperties(), mock(NoClient.class))); verify(files).writeToFile(anyString(), any()); @@ -75,7 +109,7 @@ public void enterprisetest() throws IOException { InputStream responsesIS = setResponses(false, true, "undefined", "apiToken", "42", "."); System.setIn(responsesIS); - action = new GenerateAction(files, Paths.get(project.getBasePath() + "/crowdin.yml"), false); + action = new GenerateAction(files, null, null, null, null, null, null, null, Paths.get(project.getBasePath() + "/crowdin.yml"), false); assertThrows(RuntimeException.class, () -> action.act(Outputter.getDefault(), new NoProperties(), mock(NoClient.class))); verify(files).writeToFile(anyString(), any()); @@ -89,7 +123,7 @@ public void enterpriseUrlTest() throws IOException { InputStream responsesIS = setResponses(false, true, "https://undefined.crowdin.com", "apiToken", "42", "."); System.setIn(responsesIS); - action = new GenerateAction(files, Paths.get(project.getBasePath() + "/crowdin.yml"), false); + action = new GenerateAction(files, null, null, null, null, null, null, null, Paths.get(project.getBasePath() + "/crowdin.yml"), false); assertThrows(RuntimeException.class, () -> action.act(Outputter.getDefault(), new NoProperties(), mock(NoClient.class))); verify(files).writeToFile(anyString(), any()); @@ -103,7 +137,7 @@ public void enterpriseNoNametest() throws IOException { InputStream responsesIS = setResponses(false, true, "", "apiToken", "42", "."); System.setIn(responsesIS); - action = new GenerateAction(files, Paths.get(project.getBasePath() + "/crowdin.yml"), false); + action = new GenerateAction(files, null, null, null, null, null, null, null, Paths.get(project.getBasePath() + "/crowdin.yml"), false); assertThrows(RuntimeException.class, () -> action.act(Outputter.getDefault(), new NoProperties(), mock(NoClient.class))); verify(files).writeToFile(anyString(), any()); @@ -118,7 +152,7 @@ public void fileExists() throws IOException { InputStream responsesIS = setResponses(false, true, "https://undefined.crowdin.com", "apiToken", "42", "."); System.setIn(responsesIS); - action = new GenerateAction(files, Paths.get(project.getBasePath() + "/crowdin.yml"), false); + action = new GenerateAction(files, null, null, null, null, null, null, null, Paths.get(project.getBasePath() + "/crowdin.yml"), false); action.act(Outputter.getDefault(), new NoProperties(), mock(NoClient.class)); verifyNoMoreInteractions(files); @@ -137,6 +171,6 @@ private static InputStream setResponses( + apiToken + "\n" + projectId + "\n" + basePath + "\n"; - return new ByteArrayInputStream(responsesString.getBytes(StandardCharsets.UTF_8)); + return new ByteArrayInputStream(responsesString.getBytes(UTF_8)); } } diff --git a/src/test/java/com/crowdin/cli/commands/picocli/GenerateSubcommandTest.java b/src/test/java/com/crowdin/cli/commands/picocli/GenerateSubcommandTest.java index b3557eb41..93a60a168 100644 --- a/src/test/java/com/crowdin/cli/commands/picocli/GenerateSubcommandTest.java +++ b/src/test/java/com/crowdin/cli/commands/picocli/GenerateSubcommandTest.java @@ -12,7 +12,7 @@ public class GenerateSubcommandTest extends PicocliTestUtils { public void testGenerate() { this.execute(CommandNames.GENERATE); verify(actionsMock) - .generate(any(), any(), anyBoolean()); + .generate(any(), any(), any(), any(), any(), any(), any(), any(), any(), anyBoolean()); this.check(false); } } diff --git a/src/test/java/com/crowdin/cli/commands/picocli/PicocliTestUtils.java b/src/test/java/com/crowdin/cli/commands/picocli/PicocliTestUtils.java index ff6d60b38..7ff5b45b2 100644 --- a/src/test/java/com/crowdin/cli/commands/picocli/PicocliTestUtils.java +++ b/src/test/java/com/crowdin/cli/commands/picocli/PicocliTestUtils.java @@ -60,7 +60,7 @@ void mockActions() { when(actionsMock.download(any(), anyBoolean(), any(), any(), anyBoolean(), any(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean())) .thenReturn(actionMock); - when(actionsMock.generate(any(), any(), anyBoolean())) + when(actionsMock.generate(any(), any(), any(), any(), any(), any(), any(), any(), any(), anyBoolean())) .thenReturn(actionMock); when(actionsMock.listBranches(anyBoolean(), anyBoolean())) .thenReturn(actionMock); diff --git a/website/mantemplates/crowdin-generate.adoc b/website/mantemplates/crowdin-generate.adoc index b83bdbebb..4ce9b0d26 100644 --- a/website/mantemplates/crowdin-generate.adoc +++ b/website/mantemplates/crowdin-generate.adoc @@ -15,6 +15,23 @@ include::{includedir}/{command}.adoc[tag=picocli-generated-man-section-options] include::{includedir}/{command}.adoc[tag=picocli-generated-man-section-footer] +=== Examples + +Instead of interactive mode, you can also pass the parameters directly to the command: + +---- +crowdin init \ + --base-path "." \ + --base-url "https://api.crowdin.com" \ + -i "1" \ + -T "personal-access-token" \ + -s "/locales/**/*" \ + -t "/%two_letters_code%/%original_file_name%" \ + --preserve-hierarchy +---- + +As a result, the configuration file will be filled with the passed parameters. + === Notes *Warning*: The browser authorization token you receive has an expiration period of 30 days. This means that after 30 days, the token will expire and you need to generate a new token to continue using CLI.