Skip to content

Commit

Permalink
Fix issue where misconfigured directory could cause unhelpful error m…
Browse files Browse the repository at this point in the history
…essage (#4206)
  • Loading branch information
abarrell authored Aug 7, 2024
1 parent 24d2d00 commit 8029208
Show file tree
Hide file tree
Showing 14 changed files with 186 additions and 50 deletions.
2 changes: 2 additions & 0 deletions fern/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ navigation:
contents:
- page: What is an API Definition?
path: ./pages/api-definition/introduction/what-is-an-api-definition.mdx
- page: What is the Fern Folder?
path: ./pages/api-definition/introduction/what-is-the-fern-folder.mdx
- section: OpenAPI Specification
slug: openapi
contents:
Expand Down
98 changes: 98 additions & 0 deletions fern/pages/api-definition/introduction/what-is-the-fern-folder.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
---
title: The Fern Folder
description: Describes the Fern folder structure
---

Configuring fern starts with the `fern` folder. The fern folder contains your API definitions,
generators, and your CLI version.

## Directory Structure

When you run `fern init`, your Fern folder will be initialized with the following files:
```yaml
fern/
├─ fern.config.json
├─ generators.yml
└─ definition/
├─ api.yml
└─ imdb.yml
```

If you want to intialize Fern with an OpenAPI spec, run `fern init --openapi path/to/openapi` instead.
```yaml
fern/
├─ fern.config.json
├─ generators.yml
└─ openapi/
├─ openapi.yml
```

### `fern.config.json`

Every fern folder has a single `fern.config.json` file. This file stores the organization and
the version of the Fern CLI that you are using.

```json
{
"organization": "imdb",
"version": "0.31.2"
}
```

Every time you run a fern CLI command, the CLI downloads itself at the correct version to ensure
determinism.

<Note>To upgrade the CLI, run `fern upgrade`. This will update the version field in `fern.config.json` </Note>

### `generators.yml`

The `generators.yml` file includes information about which generators you are using, where each package gets published,
as well as configuration specific to each generator.

Every `generators.yml` file contains groups of generators. In the example below, we have a group called `public` which
generates Python + TypeScript SDKs.
```yaml
groups:
public:
generators:
- name: fernapi/fern-python-sdk
version: 3.0.0
output:
location: pypi
package-name: imdb
token: ${PYPI_TOKEN}
github:
repository: imdb/imdb-python
config:
client_class_name: imdb
- name: fernapi/fern-typescript-node-sdk
version: 0.31.0
output:
location: npm
package-name: imdb
token: ${NPM_TOKEN}
github:
repository: imdb/imdb-node
config:
namespaceExport: imdb
```
## Multiple APIs
The Fern folder is capable of housing multiple API definitions. Instead of placing your API definition
at the top-level, you can nest them within an `apis` folder.

```yaml
fern/
├─ fern.config.json
├─ generators.yml
└─ apis/
└─ imdb/
├─ generators.yml
└─ openapi/
├─ openapi.yml
└─ disney/
├─ generators.yml
└─ openapi/
├─ openapi.yml
```
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`validate docs 1`] = `
"[docs]:Found 3 errors and 0 warnings. Run fern check --warnings to print out the warnings.
[docs]:docs.yml -> favicon
Path favicon.ico does not exist
[docs]:docs.yml -> logo -> dark
Path logo.png does not exist
[docs]:docs.yml -> navigation -> 0 -> layout -> 0 -> section -> contents -> 0 -> page
Path ./dummy-page.mdx does not exist"
"Misconfigured fern directory: please see the docs at https://buildwithfern.com/learn/api-definition/introduction/what-is-the-fern-folder
[docs]: ✓ All checks passed"
`;

exports[`validate no-api 1`] = `
"[docs]:✓ All checks passed
[api]: Missing file: api.yml"
`;

exports[`validate no-generator 1`] = `"Misconfigured fern directory: please see the docs at https://buildwithfern.com/learn/api-definition/introduction/what-is-the-fern-folder"`;

exports[`validate simple 1`] = `
"[api]: Found 2 errors and 0 warnings. Run fern check --warnings to print out the warnings.
[api]: api.yml -> error-discrimination
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,2 @@
instances:
- url: dummy.docs.buildwithfern.com

tabs:
help:
display-name: Help Center
icon: "fa-solid fa-cube"
navigation:
- tab: help
layout:
- section: Dummy Section
contents:
- page: Dummy Page
path: ./dummy-page.mdx
title: Dummy | Documentation
colors:
accentPrimary: "#a3d3ff"
logo:
dark: logo.png
href: https://www.dummy.com/
favicon: favicon.ico
- url: ferndevtest.docs.dev.buildwithfern.com
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
types:
MyType: MissingType
errors:
MyError:
type: string
status-code: 400
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
instances:
- url: ferndevtest.docs.dev.buildwithfern.com
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"version": "*",
"organization": "fern"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
name: simple-api
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"version": "*",
"organization": "fern"
}
2 changes: 2 additions & 0 deletions packages/cli/ete-tests/src/tests/validate/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ const FIXTURES_DIR = path.join(__dirname, "fixtures");
describe("validate", () => {
itFixture("simple");
itFixture("docs");
itFixture("no-api");
itFixture("no-generator");
});

function itFixture(fixtureName: string) {
Expand Down
32 changes: 23 additions & 9 deletions packages/cli/project-loader/src/loadProject.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import {
APIS_DIRECTORY,
ASYNCAPI_DIRECTORY,
DEFINITION_DIRECTORY,
fernConfigJson,
FERN_DIRECTORY,
generatorsYml,
getFernDirectory
GENERATORS_CONFIGURATION_FILENAME,
getFernDirectory,
OPENAPI_DIRECTORY
} from "@fern-api/configuration";
import { AbsoluteFilePath, doesPathExist, join, RelativeFilePath } from "@fern-api/fs-utils";
import { TaskContext } from "@fern-api/task-context";
Expand Down Expand Up @@ -46,14 +50,24 @@ export async function loadProject({
return context.failAndThrow(`Directory "${nameOverride ?? FERN_DIRECTORY}" not found.`);
}

const apiWorkspaces = await loadApis({
cliName,
fernDirectory,
cliVersion,
context,
commandLineApiWorkspace,
defaultToAllApiWorkspaces
});
let apiWorkspaces: APIWorkspace[] = [];

if (
(await doesPathExist(join(fernDirectory, RelativeFilePath.of(APIS_DIRECTORY)))) ||
doesPathExist(join(fernDirectory, RelativeFilePath.of(DEFINITION_DIRECTORY))) ||
doesPathExist(join(fernDirectory, RelativeFilePath.of(GENERATORS_CONFIGURATION_FILENAME))) ||
doesPathExist(join(fernDirectory, RelativeFilePath.of(OPENAPI_DIRECTORY))) ||
doesPathExist(join(fernDirectory, RelativeFilePath.of(ASYNCAPI_DIRECTORY)))
) {
apiWorkspaces = await loadApis({
cliName,
fernDirectory,
cliVersion,
context,
commandLineApiWorkspace,
defaultToAllApiWorkspaces
});
}

return {
config: await fernConfigJson.loadProjectConfig({ directory: fernDirectory, context }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ function handleWorkspaceParserFailureForFile({
logger: Logger;
}): void {
switch (failure.type) {
case WorkspaceLoaderFailureType.MISCONFIGURED_DIRECTORY:
logger.error(
"Misconfigured fern directory: please see the docs at https://buildwithfern.com/learn/api-definition/introduction/what-is-the-fern-folder"
);
break;
case WorkspaceLoaderFailureType.FILE_READ:
logger.error("Failed to open file: " + relativeFilepath);
break;
Expand Down
33 changes: 21 additions & 12 deletions packages/cli/workspace-loader/src/loadAPIWorkspace.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ASYNCAPI_DIRECTORY, generatorsYml, OPENAPI_DIRECTORY } from "@fern-api/configuration";
import { ASYNCAPI_DIRECTORY, DEFINITION_DIRECTORY, generatorsYml, OPENAPI_DIRECTORY } from "@fern-api/configuration";
import { AbsoluteFilePath, doesPathExist, join, RelativeFilePath } from "@fern-api/fs-utils";
import { TaskContext } from "@fern-api/task-context";
import { loadAPIChangelog } from "./loadAPIChangelog";
Expand Down Expand Up @@ -38,7 +38,6 @@ export async function loadAPIWorkspace({

if (generatorsConfiguration?.api != null && generatorsConfiguration.api.definitions.length > 0) {
const specs: Spec[] = [];

for (const definition of generatorsConfiguration.api.definitions) {
const absoluteFilepath = join(absolutePathToWorkspace, RelativeFilePath.of(definition.path));
const absoluteFilepathToOverrides =
Expand Down Expand Up @@ -134,18 +133,28 @@ export async function loadAPIWorkspace({
})
};
}
if (await doesPathExist(join(absolutePathToWorkspace, RelativeFilePath.of(DEFINITION_DIRECTORY)))) {
const fernWorkspace = new LazyFernWorkspace({
absoluteFilepath: absolutePathToWorkspace,
generatorsConfiguration,
workspaceName,
changelog,
context,
cliVersion
});

const fernWorkspace = new LazyFernWorkspace({
absoluteFilepath: absolutePathToWorkspace,
generatorsConfiguration,
workspaceName,
changelog,
context,
cliVersion
});
return {
didSucceed: true,
workspace: fernWorkspace
};
}

return {
didSucceed: true,
workspace: fernWorkspace
didSucceed: false,
failures: {
[RelativeFilePath.of(OPENAPI_DIRECTORY)]: {
type: WorkspaceLoaderFailureType.MISCONFIGURED_DIRECTORY
}
}
};
}
10 changes: 8 additions & 2 deletions packages/cli/workspace-loader/src/types/Result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export declare namespace WorkspaceLoader {
type: WorkspaceLoaderFailureType.FILE_MISSING;
}

export interface MisconfiguredDirectoryFailure {
type: WorkspaceLoaderFailureType.MISCONFIGURED_DIRECTORY;
}

export interface StructureValidationFailure {
type: WorkspaceLoaderFailureType.STRUCTURE_VALIDATION;
error: ZodError;
Expand All @@ -46,7 +50,8 @@ export declare namespace WorkspaceLoader {
| DependencyNotListedFailure
| FailedToLoadDependencyFailure
| ExportPackageHasDefinitionsFailure
| ExportingPackageMarkerHasOtherKeysFailure;
| ExportingPackageMarkerHasOtherKeysFailure
| MisconfiguredDirectoryFailure;

export interface DependencyNotListedFailure {
type: WorkspaceLoaderFailureType.DEPENDENCY_NOT_LISTED;
Expand Down Expand Up @@ -77,5 +82,6 @@ export enum WorkspaceLoaderFailureType {
DEPENDENCY_NOT_LISTED = "DEPENDENCY_NOT_LISTED",
FAILED_TO_LOAD_DEPENDENCY = "FAILED_TO_LOAD_DEPENDENCY",
EXPORTING_PACKAGE_MARKER_OTHER_KEYS = "EXPORTING_PACKAGE_MARKER_OTHER_KEYS",
EXPORT_PACKAGE_HAS_DEFINITIONS = "EXPORT_PACKAGE_HAS_DEFINITIONS"
EXPORT_PACKAGE_HAS_DEFINITIONS = "EXPORT_PACKAGE_HAS_DEFINITIONS",
MISCONFIGURED_DIRECTORY = "MISCONFIGURED_DIRECTORY"
}

0 comments on commit 8029208

Please sign in to comment.