Skip to content

Commit

Permalink
Introduce README.md as default documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
azinneera committed Oct 19, 2024
1 parent 79b1a87 commit 81270be
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ public class PackageManifest {
private final String visibility;
private boolean template;
private final String icon;
private final String readme;
private final Map<String, Modules> modulesMap;

// Other entries hold other key/value pairs available in the Ballerina.toml file.
// These keys are not part of the Ballerina package specification.
Expand Down Expand Up @@ -82,6 +84,8 @@ private PackageManifest(PackageDescriptor packageDesc,
this.visibility = "";
this.icon = "";
this.tools = Collections.emptyList();
this.readme = "";
this.modulesMap = Map.of();
}

private PackageManifest(PackageDescriptor packageDesc,
Expand Down Expand Up @@ -109,6 +113,8 @@ private PackageManifest(PackageDescriptor packageDesc,
this.visibility = "";
this.icon = "";
this.tools = Collections.unmodifiableList(tools);
this.readme = "";
this.modulesMap = Map.of();
}

private PackageManifest(PackageDescriptor packageDesc,
Expand Down Expand Up @@ -146,6 +152,8 @@ private PackageManifest(PackageDescriptor packageDesc,
this.template = template;
this.icon = icon;
this.tools = Collections.emptyList();
this.readme = "";
this.modulesMap = Map.of();
}

private PackageManifest(PackageDescriptor packageDesc,
Expand All @@ -165,7 +173,9 @@ private PackageManifest(PackageDescriptor packageDesc,
String visibility,
boolean template,
String icon,
List<Tool> tools) {
List<Tool> tools,
String readme,
Map<String, Modules> modulesMap) {
this.packageDesc = packageDesc;
this.compilerPluginDesc = compilerPluginDesc;
this.balToolDesc = balToolDesc;
Expand All @@ -184,6 +194,8 @@ private PackageManifest(PackageDescriptor packageDesc,
this.template = template;
this.icon = icon;
this.tools = tools;
this.readme = readme;
this.modulesMap = modulesMap;
}
public static PackageManifest from(PackageDescriptor packageDesc) {
return new PackageManifest(packageDesc, null, null, Collections.emptyMap(), Collections.emptyList(),
Expand Down Expand Up @@ -217,10 +229,12 @@ public static PackageManifest from(PackageDescriptor packageDesc,
String visibility,
boolean template,
String icon,
List<Tool> tools) {
List<Tool> tools,
String readme,
Map<String, Modules> modulesMap) {
return new PackageManifest(packageDesc, compilerPluginDesc, balToolDesc, platforms, dependencies, otherEntries,
diagnostics, license, authors, keywords, export, include, repository, ballerinaVersion, visibility,
template, icon, tools);
template, icon, tools, readme, modulesMap);
}

public static PackageManifest from(PackageDescriptor packageDesc,
Expand Down Expand Up @@ -331,6 +345,14 @@ public boolean template() {
return template;
}

public String readme() {
return readme;
}

public Map<String, Modules> modules() {
return modulesMap;
}

/**
* Represents the platform section in Ballerina.toml file.
*
Expand Down Expand Up @@ -539,6 +561,9 @@ public record Field(String value, TomlNodeLocation location) {
}
}

public record Modules(String name, boolean export, Optional<String> description, Optional<String> readme) {
}

private List<String> getExport(PackageDescriptor packageDesc, List<String> export) {
if (export == null || export.isEmpty()) {
return Collections.singletonList(packageDesc.name().value());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import io.ballerina.projects.internal.model.BalToolDescriptor;
import io.ballerina.projects.internal.model.CompilerPluginDescriptor;
import io.ballerina.projects.util.FileUtils;
import io.ballerina.projects.util.ProjectConstants;
import io.ballerina.projects.util.ProjectUtils;
import io.ballerina.toml.api.Toml;
import io.ballerina.toml.semantic.TomlType;
Expand All @@ -51,6 +52,7 @@
import io.ballerina.tools.diagnostics.Diagnostic;
import io.ballerina.tools.diagnostics.DiagnosticInfo;
import io.ballerina.tools.diagnostics.DiagnosticSeverity;
import org.apache.commons.io.FilenameUtils;
import org.ballerinalang.compiler.CompilerOptionName;

import java.io.IOException;
Expand All @@ -62,9 +64,11 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import static io.ballerina.projects.internal.ManifestUtils.ToolNodeValueType;
import static io.ballerina.projects.internal.ManifestUtils.convertDiagnosticToString;
Expand Down Expand Up @@ -122,6 +126,9 @@ public class ManifestBuilder {
private static final String OPTIONS = "options";
private static final String TOOL = "tool";

private static final String DESCRIPTION = "description";
private static final String README = "readme";

private ManifestBuilder(TomlDocument ballerinaToml,
TomlDocument compilerPluginToml,
TomlDocument balToolToml,
Expand Down Expand Up @@ -196,6 +203,8 @@ private PackageManifest parseAsPackageManifest() {
String visibility = "";
boolean template = false;
String icon = "";
String readme = null;
Map<String, PackageManifest.Modules> moduleEntries = new HashMap<>();

if (!tomlAstNode.entries().isEmpty()) {
TopLevelNode topLevelPkgNode = tomlAstNode.entries().get(PACKAGE);
Expand All @@ -210,10 +219,14 @@ private PackageManifest parseAsPackageManifest() {
ballerinaVersion = getStringValueFromTomlTableNode(pkgNode, DISTRIBUTION, "");
visibility = getStringValueFromTomlTableNode(pkgNode, VISIBILITY, "");
template = getBooleanFromTemplateNode(pkgNode, TEMPLATE);
icon = getStringValueFromTomlTableNode(pkgNode, ICON, "");

icon = getStringValueFromTomlTableNode(pkgNode, ICON, "");
// we ignore file types except png here, since file type error will be shown
validateIconPathForPng(icon, pkgNode);

String customReadmeVal = getStringValueFromTomlTableNode(pkgNode, README, null);
readme = validateAndGetReadmePath(pkgNode, customReadmeVal);
moduleEntries = getModuleEntries(pkgNode, customReadmeVal);
}
}

Expand Down Expand Up @@ -252,7 +265,130 @@ private PackageManifest parseAsPackageManifest() {
}
return PackageManifest.from(packageDescriptor, pluginDescriptor, balToolDescriptor, platforms,
localRepoDependencies, otherEntries, diagnostics(), license, authors, keywords, exported, includes,
repository, ballerinaVersion, visibility, template, icon, tools);
repository, ballerinaVersion, visibility, template, icon, tools, readme, moduleEntries);
}

private Map<String, PackageManifest.Modules> getModuleEntries(
TomlTableNode pkgNode, String customReadmeVal) {

Map<String, PackageManifest.Modules> moduleMap = new HashMap<>();
Path modulesRoot = this.projectPath.resolve(ProjectConstants.MODULES_ROOT);
if (!Files.exists(modulesRoot)) {
return moduleMap;
}
List<Path> moduleDirs;
try (Stream<Path> stream = Files.walk(modulesRoot, 1)) { // depth of 1 ensures we only look at direct children
moduleDirs = stream
.filter(Files::isDirectory)
.filter(path -> !path.equals(modulesRoot))
.toList();
} catch (IOException e) {
throw new ProjectException("Failed to read the module README:", e);
}

if (customReadmeVal == null) {
if (Files.exists(this.projectPath.resolve(ProjectConstants.PACKAGE_MD_FILE_NAME))) {
// old structure. Module READMEs are captured by <module-root>/Module.md file.
for (Path moduleDir : moduleDirs) {
Path modReadmePath = moduleDir.resolve(ProjectConstants.MODULE_MD_FILE_NAME);
String modReadme = null;
if (Files.exists(modReadmePath)) {
modReadme = modReadmePath.toString();
}
PackageManifest.Modules module = new PackageManifest.Modules(
moduleDir.getFileName().toString(), false,
Optional.empty(), Optional.ofNullable(modReadme));
moduleMap.put(moduleDir.getFileName().toString(), module);
}
return moduleMap;
}
}

// new structure
TopLevelNode dependencyEntries = pkgNode.entries().get("modules");
if (dependencyEntries == null || dependencyEntries.kind() == TomlType.NONE) {
for (Path moduleDir : moduleDirs) {
Path modReadmePath = moduleDir.resolve(ProjectConstants.README_MD_FILE_NAME);
String modReadme = null;
if (Files.exists(modReadmePath)) {
modReadme = modReadmePath.toString();
}
PackageManifest.Modules module = new PackageManifest.Modules(
moduleDir.getFileName().toString(), false,
Optional.empty(), Optional.ofNullable(modReadme));
moduleMap.put(moduleDir.getFileName().toString(), module);
}
return moduleMap;
}
if (dependencyEntries.kind() == TomlType.TABLE_ARRAY) {
TomlTableArrayNode dependencyTableArray = (TomlTableArrayNode) dependencyEntries;
for (TomlTableNode modulesNode : dependencyTableArray.children()) {
String moduleName = getStringValueFromTomlTableNode(modulesNode, NAME, null);
if (moduleName == null) {
continue;
}
if (moduleName.equals(getStringValueFromTomlTableNode(pkgNode, NAME))) {
// TODO: report diagnostic - default module not allowed
continue;
}
if (Files.notExists(this.projectPath.resolve(ProjectConstants.MODULES_ROOT).resolve(moduleName))) {
// TODO: report diagnostic - invalid module provided. module not found
continue;
}
boolean export = Boolean.TRUE.equals(getBooleanValueFromTomlTableNode(modulesNode, EXPORT));
String description = getStringValueFromTomlTableNode(modulesNode, DESCRIPTION, null);
String modReadme = getStringValueFromTomlTableNode(modulesNode, README, null);
if (modReadme == null) {
Path defaultReadme = modulesRoot.resolve(moduleName).resolve(ProjectConstants.README_MD_FILE_NAME);
if (Files.exists(defaultReadme)) {
modReadme = defaultReadme.toString();
}
} else {
if (!Paths.get(modReadme).isAbsolute()) {
modReadme = this.projectPath.resolve(modReadme).toString();
}
}
PackageManifest.Modules module = new PackageManifest.Modules(moduleName, export,
Optional.ofNullable(description), Optional.ofNullable(modReadme));
moduleMap.put(moduleName, module);
}
}
return moduleMap;
}

private String validateAndGetReadmePath(TomlTableNode pkgNode, String readme) {
Path readmeMdPath;
if (readme == null) {
readmeMdPath = this.projectPath.resolve(ProjectConstants.PACKAGE_MD_FILE_NAME);
if (Files.exists(readmeMdPath)) {
// TODO: can we report the diagnostic?
return readmeMdPath.toString();
} else {
readmeMdPath = this.projectPath.resolve(ProjectConstants.README_MD_FILE_NAME);
if (Files.exists(readmeMdPath)) {
return readmeMdPath.toString();
} else {
return null;
}
}
}

readmeMdPath = Paths.get(readme);
if (!readmeMdPath.isAbsolute()) {
readmeMdPath = this.projectPath.resolve(readmeMdPath);
}
if (Files.notExists(readmeMdPath)) {
reportDiagnostic(pkgNode.entries().get(README),
"could not locate the readme file '" + readmeMdPath + "'",
ProjectDiagnosticErrorCode.INVALID_PATH, DiagnosticSeverity.ERROR);
}

if (!FilenameUtils.getExtension(readme).equals(ProjectConstants.README_EXTENSION)) {
reportDiagnostic(pkgNode.entries().get(README),
"invalid 'readme' under [package]: 'readme' can only have '.md' files",
ProjectDiagnosticErrorCode.INVALID_FILE_FORMAT, DiagnosticSeverity.ERROR);
}
return readme;
}

private List<PackageManifest.Tool> getTools() {
Expand Down Expand Up @@ -438,7 +574,7 @@ private void validateIconPathForPng(String icon, TomlTableNode pkgNode) {
if (!FileUtils.isValidPng(iconPath)) {
reportDiagnostic(pkgNode.entries().get("icon"),
"invalid 'icon' under [package]: 'icon' can only have 'png' images",
ProjectDiagnosticErrorCode.INVALID_ICON, DiagnosticSeverity.ERROR);
ProjectDiagnosticErrorCode.INVALID_FILE_FORMAT, DiagnosticSeverity.ERROR);
}
} catch (IOException e) {
// should not reach to this line
Expand Down Expand Up @@ -566,7 +702,7 @@ private PackageManifest.Platform getDependencyPlatform(TopLevelNode dependencyNo
String artifactId = getStringValueFromPlatformEntry(platformEntryTable, ARTIFACT_ID);
String version = getStringValueFromPlatformEntry(platformEntryTable, VERSION);
String scope = getStringValueFromPlatformEntry(platformEntryTable, SCOPE);
Boolean graalvmCompatibility = getBooleanValueFromPlatformEntry(platformEntryTable,
Boolean graalvmCompatibility = getBooleanValueFromTomlTableNode(platformEntryTable,
GRAALVM_COMPATIBLE);
if (PlatformLibraryScope.PROVIDED.getStringValue().equals(scope)
&& !providedPlatformDependencyIsValid(artifactId, groupId, version)) {
Expand Down Expand Up @@ -844,7 +980,7 @@ private String getStringValueFromPlatformEntry(TomlTableNode pkgNode, String key
return getStringFromTomlTableNode(topLevelNode);
}

private Boolean getBooleanValueFromPlatformEntry(TomlTableNode pkgNode, String key) {
private Boolean getBooleanValueFromTomlTableNode(TomlTableNode pkgNode, String key) {
TopLevelNode topLevelNode = pkgNode.entries().get(key);
if (topLevelNode == null || topLevelNode.kind() == TomlType.NONE) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public enum ProjectDiagnosticErrorCode implements DiagnosticCode {
// Error codes used in the ManifestBuilder
MISSING_PKG_INFO_IN_BALLERINA_TOML("BCE5001", "missing.package.info"),
INVALID_PATH("BCE5002", "error.invalid.path"),
INVALID_ICON("BCE5003", "error.invalid.icon"),
INVALID_FILE_FORMAT("BCE5003", "error.invalid.file.format"),
INVALID_PROVIDED_DEPENDENCY("BCE5004", "invalid.provided.dependency"),
INVALID_PROVIDED_SCOPE_IN_BUILD("BCE5005", "invalid.provided.scope"),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,19 @@
* under the License.
*/
package io.ballerina.projects.util;

import java.nio.file.Path;

/**
* Defines constants related to the project directory.
*
* @since 2.0.0
*/
public final class ProjectConstants {

public static final String README_MD_FILE_NAME = "README.md";
public static final String README_EXTENSION = "md";

private ProjectConstants() {}

public static final String BLANG_SOURCE_EXT = ".bal";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,40 @@
},
"template": {
"type": "boolean"
},
"readme": {
"type": "string",
"pattern": ".*.md$",
"message": {
"pattern": "invalid 'readme' under [package]: 'readme' can only have a '.md' file"
}
},
"modules": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"name": {
"type": "string"
},
"readme": {
"type": "string",
"pattern": ".*.md$",
"message": {
"pattern": "invalid 'readme' under [package.modules]: 'readme' can only have a '.md' file"
}
},
"export": {
"type": "boolean"
}
}, "required": [
"name"
],
"message": {
"required": "'${property}' under [[dependency]] is missing"
}
}
}
}
},
Expand Down

0 comments on commit 81270be

Please sign in to comment.