Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add YAML module #528

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ deno.d.ts
node_modules
package.json
package-lock.json
yarn.lock
.vscode
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Here are the dedicated documentations of modules:
- [strings](strings/README.md)
- [testing](testing/README.md)
- [ws](ws/README.md)
- [yaml](yaml/README.md)
lsagetlethias marked this conversation as resolved.
Show resolved Hide resolved

## Contributing

Expand Down
18 changes: 18 additions & 0 deletions encoding/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,3 +217,21 @@ const obj = {
};
const tomlString = stringify(obj);
```

## YAML

YAML parser / dumper for Deno

Heavily inspired from [js-yaml]

### Example
See [`./yaml/example`](./yaml/example) folder and [js-yaml] repository.

### :warning: Limitations
- `binary` type is currently not stable
- `function`, `regexp`, and `undefined` type are currently not supported

# Basic usage
TBD

[js-yaml]: https://github.com/nodeca/js-yaml
1 change: 1 addition & 0 deletions encoding/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
import "./hex_test.ts";
import "./toml_test.ts";
import "./csv_test.ts";
import "./yaml_test.ts";
7 changes: 7 additions & 0 deletions encoding/yaml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Ported from js-yaml:
// https://github.com/nodeca/js-yaml/
lsagetlethias marked this conversation as resolved.
Show resolved Hide resolved
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.

export * from "./yaml/parse.ts";
export * from "./yaml/stringify.ts";
export * from "./yaml/schema/mod.ts";
72 changes: 72 additions & 0 deletions encoding/yaml/Mark.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { repeat } from "./utils.ts";
lsagetlethias marked this conversation as resolved.
Show resolved Hide resolved

export class Mark {
constructor(
public name: string,
public buffer: string,
public position: number,
public line: number,
public column: number
) {}

public getSnippet(indent = 4, maxLength = 75) {
if (!this.buffer) return null;

let head = "";
let start = this.position;

while (
start > 0 &&
"\x00\r\n\x85\u2028\u2029".indexOf(this.buffer.charAt(start - 1)) === -1
) {
start -= 1;
if (this.position - start > maxLength / 2 - 1) {
head = " ... ";
start += 5;
break;
}
}

let tail = "";
let end = this.position;

while (
end < this.buffer.length &&
"\x00\r\n\x85\u2028\u2029".indexOf(this.buffer.charAt(end)) === -1
) {
end += 1;
if (end - this.position > maxLength / 2 - 1) {
tail = " ... ";
end -= 5;
break;
}
}

const snippet = this.buffer.slice(start, end);
return `${repeat(" ", indent)}${head}${snippet}${tail}\n${repeat(
" ",
indent + this.position - start + head.length
)}^`;
}

public toString(compact?: boolean) {
let snippet,
where = "";

if (this.name) {
where += `in "${this.name}" `;
}

where += `at line ${this.line + 1}, column ${this.column + 1}`;

if (!compact) {
snippet = this.getSnippet();

if (snippet) {
where += `:\n${snippet}`;
}
}

return where;
}
}
95 changes: 95 additions & 0 deletions encoding/yaml/Schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { YAMLError } from "./error/YAMLError.ts";
import { KindType, Type } from "./Type.ts";
import { ArrayObject } from "./utils.ts";

function compileList(
schema: Schema,
name: "implicit" | "explicit",
result: Type[]
): Type[] {
const exclude: number[] = [];

for (const includedSchema of schema.include) {
result = compileList(includedSchema, name, result);
}

for (const currentType of schema[name]) {
for (
let previousIndex = 0;
previousIndex < result.length;
previousIndex++
) {
const previousType = result[previousIndex];
if (
previousType.tag === currentType.tag &&
previousType.kind === currentType.kind
) {
exclude.push(previousIndex);
}
}

result.push(currentType);
}

return result.filter((type, index) => !exclude.includes(index));
}

export type TypeMap = { [k in KindType | "fallback"]: ArrayObject<Type> };
function compileMap(...typesList: Type[][]) {
const result: TypeMap = {
fallback: {},
mapping: {},
scalar: {},
sequence: {}
};

for (const types of typesList) {
for (const type of types) {
if (type.kind !== null) {
result[type.kind][type.tag] = result["fallback"][type.tag] = type;
}
}
}
return result;
}

export class Schema implements SchemaDefinition {
public static SCHEMA_DEFAULT?: Schema;

public implicit: Type[];
public explicit: Type[];
public include: Schema[];

public compiledImplicit: Type[];
public compiledExplicit: Type[];
public compiledTypeMap: TypeMap;

constructor(definition: SchemaDefinition) {
this.explicit = definition.explicit || [];
this.implicit = definition.implicit || [];
this.include = definition.include || [];

for (const type of this.implicit) {
if (type.loadKind && type.loadKind !== "scalar") {
throw new YAMLError(
"There is a non-scalar type in the implicit list of a schema. Implicit resolving of such types is not supported."
);
}
}

this.compiledImplicit = compileList(this, "implicit", []);
this.compiledExplicit = compileList(this, "explicit", []);
this.compiledTypeMap = compileMap(
this.compiledImplicit,
this.compiledExplicit
);
}

public static create() {}
}

export interface SchemaDefinition {
implicit?: any[];
explicit?: Type[];
include?: Schema[];
}
6 changes: 6 additions & 0 deletions encoding/yaml/State.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { SchemaDefinition } from "./Schema.ts";
import { DEFAULT_FULL_SCHEMA } from "./schema/mod.ts";

export abstract class State {
constructor(public schema: SchemaDefinition = DEFAULT_FULL_SCHEMA) {}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that these objects are so well organized but... splitting them into 4 files means there are 4 more deps to download. I'd consider putting them into single file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean all 4 kinds of schema to be grouped as one file ?

50 changes: 50 additions & 0 deletions encoding/yaml/Type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { ArrayObject } from "./utils.ts";

export type KindType = "sequence" | "scalar" | "mapping";
export type StyleVariant = "lowercase" | "uppercase" | "camelcase" | "decimal";
export type RepresentFn = (data: any, style?: StyleVariant) => any;

const DEFAULT_RESOLVE = () => true;
const DEFAULT_CONSTRUCT = (data: any) => data;

interface TypeOptions {
kind: KindType;
resolve?: (data: any) => boolean;
construct?: (data: string) => any;
instanceOf?: any;
predicate?: (data: object) => boolean;
represent?: RepresentFn | ArrayObject<RepresentFn>;
defaultStyle?: StyleVariant;
styleAliases?: { [x: string]: any };
}

function checkTagFormat(tag: string): string {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems superfluous

return tag;
}

export class Type {
public tag: string;
public kind: KindType | null = null;
public instanceOf: any;
public predicate?: (data: object) => boolean;
public represent?: RepresentFn | ArrayObject<RepresentFn>;
public defaultStyle?: StyleVariant;
public styleAliases?: { [x: string]: any };
public loadKind?: KindType;

constructor(tag: string, options?: TypeOptions) {
this.tag = checkTagFormat(tag);
if (options) {
this.kind = options.kind;
this.resolve = options.resolve || DEFAULT_RESOLVE;
this.construct = options.construct || DEFAULT_CONSTRUCT;
this.instanceOf = options.instanceOf;
this.predicate = options.predicate;
this.represent = options.represent;
this.defaultStyle = options.defaultStyle;
this.styleAliases = options.styleAliases;
}
}
public resolve: (data?: any) => boolean = () => true;
public construct: (data?: any) => any = data => data;
}
3 changes: 3 additions & 0 deletions encoding/yaml/devDeps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import * as asserts from "../testing/asserts.ts";
lsagetlethias marked this conversation as resolved.
Show resolved Hide resolved
import * as testing from "../testing/mod.ts";
export { testing, asserts };
111 changes: 111 additions & 0 deletions encoding/yaml/dumper/DumperState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { Schema, SchemaDefinition } from "../Schema.ts";
import { State } from "../State.ts";
import { StyleVariant, Type } from "../Type.ts";
import { ArrayObject } from "../utils.ts";

const _hasOwnProperty = Object.prototype.hasOwnProperty;

function compileStyleMap(
schema: Schema,
map?: ArrayObject<StyleVariant> | null
): ArrayObject<StyleVariant> {
if (typeof map === "undefined" || map === null) return {};

let type: Type;
const result: ArrayObject<StyleVariant> = {};
const keys = Object.keys(map);
let tag: string, style: StyleVariant;
for (let index = 0, length = keys.length; index < length; index += 1) {
tag = keys[index];
style = String(map[tag]) as StyleVariant;
if (tag.slice(0, 2) === "!!") {
tag = `tag:yaml.org,2002:${tag.slice(2)}`;
}
type = schema.compiledTypeMap.fallback[tag];

if (type && _hasOwnProperty.call(type.styleAliases, style)) {
style = type.styleAliases[style];
}

result[tag] = style;
}

return result;
}

export interface DumperStateOptions {
/** indentation width to use (in spaces). */
indent?: number;
/** when true, will not add an indentation level to array elements */
noArrayIndent?: boolean;
/** do not throw on invalid types (like function in the safe schema) and skip pairs and single values with such types. */
skipInvalid?: boolean;
/** specifies level of nesting, when to switch from block to flow style for collections. -1 means block style everwhere */
flowLevel?: number;
/** Each tag may have own set of styles. - "tag" => "style" map. */
styles?: ArrayObject<StyleVariant> | null;
/** specifies a schema to use. */
schema?: SchemaDefinition;
/** if true, sort keys when dumping YAML. If a function, use the function to sort the keys. (default: false) */
sortKeys?: boolean | ((a: any, b: any) => number);
/** set max line width. (default: 80) */
lineWidth?: number;
/** if true, don't convert duplicate objects into references (default: false) */
noRefs?: boolean;
/** if true don't try to be compatible with older yaml versions. Currently: don't quote "yes", "no" and so on, as required for YAML 1.1 (default: false) */
noCompatMode?: boolean;
/**
* if true flow sequences will be condensed, omitting the space between `key: value` or `a, b`. Eg. `'[a,b]'` or `{a:{b:c}}`.
* Can be useful when using yaml for pretty URL query params as spaces are %-encoded. (default: false).
*/
condenseFlow?: boolean;
}

export class DumperState extends State {
public indent: number;
public noArrayIndent: boolean;
public skipInvalid: boolean;
public flowLevel: number;
public sortKeys: boolean | ((a: any, b: any) => number);
public lineWidth: number;
public noRefs: boolean;
public noCompatMode: boolean;
public condenseFlow: boolean;
public implicitTypes: Type[];
public explicitTypes: Type[];
public tag: string | null = null;
public result: string = "";
public duplicates = [];
public usedDuplicates = null;
public styleMap: ArrayObject<StyleVariant>;
public dump: any;

constructor({
schema,
indent = 2,
noArrayIndent = false,
skipInvalid = false,
flowLevel = -1,
styles = null,
sortKeys = false,
lineWidth = 80,
noRefs = false,
noCompatMode = false,
condenseFlow = false
}: DumperStateOptions) {
super(schema);
this.indent = Math.max(1, indent);
this.noArrayIndent = noArrayIndent;
this.skipInvalid = skipInvalid;
this.flowLevel = flowLevel;
this.styleMap = compileStyleMap(this.schema as Schema, styles);
this.sortKeys = sortKeys;
this.lineWidth = lineWidth;
this.noRefs = noRefs;
this.noCompatMode = noCompatMode;
this.condenseFlow = condenseFlow;

this.implicitTypes = (this.schema as Schema).compiledImplicit;
this.explicitTypes = (this.schema as Schema).compiledExplicit;
}
}
Loading