From 4eb2b195a6d5074906df9ef0b5d8c1e39f127b78 Mon Sep 17 00:00:00 2001 From: Guilherme Bernal Date: Fri, 16 Oct 2020 21:27:26 +0000 Subject: [PATCH 01/10] feat: add eslint do typescript-generator and playground --- playground/.eslintrc.json | 296 ++++++++++++ playground/package.json | 7 + playground/src/components/header/index.tsx | 7 +- playground/src/components/loading/index.tsx | 24 +- .../requestCard/annotations/index.tsx | 10 +- .../requestCard/bottom/bottom.test.tsx | 21 +- .../components/requestCard/bottom/index.tsx | 14 +- .../components/requestCard/content/index.tsx | 34 +- .../components/requestCard/errors/index.tsx | 2 +- .../components/requestCard/header/index.tsx | 15 +- .../src/components/requestCard/index.tsx | 8 +- .../src/components/requestCard/tabs/index.tsx | 2 +- .../src/components/searchInput/index.tsx | 2 +- .../searchInput/searchInput.test.tsx | 1 + playground/src/components/section/index.tsx | 4 +- playground/src/configuration/index.ts | 3 +- playground/src/configuration/polyfills.ts | 3 +- playground/src/configuration/serviceWorker.ts | 11 +- playground/src/containers/app/index.tsx | 4 +- .../src/containers/errorBoundary/index.tsx | 10 +- .../src/containers/mainWrapper/index.tsx | 3 +- playground/src/containers/routes/index.tsx | 6 +- playground/src/helpers/arrayHelpers/index.ts | 3 +- .../src/helpers/componentSwitch/index.tsx | 7 +- .../localStorage/bookmarkedEndpoints.ts | 30 +- playground/src/helpers/requestModel/index.ts | 39 +- playground/src/pages/home/index.tsx | 27 +- playground/src/pages/notfound/index.tsx | 2 +- playground/src/resources/types/ast.d.ts | 8 +- playground/src/resources/types/modules.d.ts | 3 +- playground/src/stores/config.ts | 37 +- playground/src/stores/index.ts | 11 +- playground/src/stores/requests.ts | 109 +++-- playground/tsconfig.json | 2 +- typescript-generator/.eslintrc.json | 423 ++++++++++++++++++ typescript-generator/package.json | 7 + typescript-generator/spec/helpers.spec.ts | 17 + typescript-generator/src/browser-client.ts | 6 +- typescript-generator/src/helpers.ts | 122 ++--- typescript-generator/src/interfaces.ts | 5 +- typescript-generator/src/node-client.ts | 6 +- typescript-generator/src/node-server.ts | 8 +- 42 files changed, 1097 insertions(+), 262 deletions(-) create mode 100644 playground/.eslintrc.json create mode 100644 typescript-generator/.eslintrc.json diff --git a/playground/.eslintrc.json b/playground/.eslintrc.json new file mode 100644 index 000000000..8be6ae7a0 --- /dev/null +++ b/playground/.eslintrc.json @@ -0,0 +1,296 @@ +{ + "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"], + "env": { + "node": true, + "es2017": true, + "jest": true + }, + "globals": { + "BigInt": true + }, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2019, + "project": "tsconfig.json", + "sourceType": "module" + }, + "plugins": ["@typescript-eslint/eslint-plugin", "prettier"], + "rules": { + "prettier/prettier": "error", + "spaced-comment": ["error", "always"], + "@typescript-eslint/array-type": [ + "error", + { + "default": "array-simple" + } + ], + "@typescript-eslint/brace-style": "error", + "@typescript-eslint/consistent-type-definitions": ["error", "interface"], + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/func-call-spacing": ["error", "never"], + "@typescript-eslint/interface-name-prefix": "off", + "@typescript-eslint/member-delimiter-style": "off", + "@typescript-eslint/no-empty-interface": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unnecessary-qualifier": "error", + "@typescript-eslint/no-unnecessary-type-arguments": "error", + "@typescript-eslint/no-unused-expressions": "error", + "@typescript-eslint/no-use-before-define": "error", + "@typescript-eslint/no-useless-constructor": "error", + "@typescript-eslint/no-var-requires": "off", + "@typescript-eslint/prefer-for-of": "error", + "@typescript-eslint/promise-function-async": "error", + "@typescript-eslint/require-array-sort-compare": "error", + "@typescript-eslint/restrict-plus-operands": "error", + "@typescript-eslint/semi": [ + "error", + "always", + { + "omitLastInOneLineBlock": false + } + ], + "array-callback-return": "error", + "block-scoped-var": "error", + "consistent-return": "error", + "curly": "error", + "default-case": "error", + "default-param-last": "error", + "dot-notation": "error", + "eqeqeq": ["error", "always"], + "global-require": "error", + "guard-for-in": "error", + "no-alert": "error", + "no-buffer-constructor": "error", + "no-caller": "error", + "no-div-regex": "error", + "no-else-return": "error", + "no-empty-function": [ + "error", + { + "allow": ["constructors"] + } + ], + "no-eq-null": "error", + "no-eval": "error", + "no-extend-native": "error", + "no-extra-bind": "error", + "no-floating-decimal": "error", + "no-implicit-coercion": "error", + "no-implied-eval": "error", + "no-import-assign": "error", + "no-invalid-this": "error", + "no-iterator": "error", + "no-label-var": "error", + "no-labels": "error", + "no-lone-blocks": "error", + "no-loop-func": "error", + "no-multi-spaces": "error", + "no-multi-str": "error", + "no-new-func": "error", + "no-new-wrappers": "error", + "no-new": "error", + "no-octal-escape": "error", + "no-path-concat": "error", + "no-process-exit": "error", + "no-proto": "error", + "no-prototype-builtins": "off", + "no-restricted-modules": ["error", "moment"], + "no-return-assign": "error", + "no-return-await": "error", + "no-script-url": "error", + "no-self-compare": "error", + "no-sequences": "error", + "no-shadow": "error", + "no-sync": [ + "error", + { + "allowAtRootLevel": true + } + ], + "no-template-curly-in-string": "error", + "no-throw-literal": "error", + "no-undef-init": "error", + "no-unmodified-loop-condition": "error", + "no-useless-call": "error", + "no-useless-concat": "error", + "no-void": "error", + "prefer-named-capture-group": "warn", + "prefer-regex-literals": "error", + "radix": "error", + "require-unicode-regexp": "error", + "strict": "error", + "wrap-iife": ["error", "any"], + "yoda": "error", + "array-bracket-spacing": ["error", "never"], + "array-element-newline": ["error", "consistent"], + "block-spacing": "error", + "camelcase": "warn", + "capitalized-comments": ["error", "always"], + "comma-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "comma-style": ["error", "last"], + "computed-property-spacing": ["error", "never"], + "eol-last": ["error", "always"], + "func-name-matching": ["error", "always"], + "func-names": ["error", "as-needed"], + "func-style": ["error", "declaration"], + "function-call-argument-newline": ["error", "consistent"], + "jsx-quotes": ["error", "prefer-double"], + "key-spacing": [ + "error", + { + "beforeColon": false + } + ], + "keyword-spacing": [ + "error", + { + "before": true, + "after": true + } + ], + "linebreak-style": ["error", "unix"], + "lines-between-class-members": ["error", "always"], + "max-depth": ["error", 5], + "max-nested-callbacks": ["error", 10], + "max-params": ["error", 10], + "max-statements-per-line": [ + "error", + { + "max": 2 + } + ], + "multiline-comment-style": ["error", "starred-block"], + "new-parens": "error", + "newline-per-chained-call": [ + "error", + { + "ignoreChainWithDepth": 3 + } + ], + "no-array-constructor": "error", + "no-lonely-if": "error", + "no-multi-assign": "error", + "no-multiple-empty-lines": "error", + "no-negated-condition": "error", + "no-new-object": "error", + "no-tabs": "error", + "no-trailing-spaces": "error", + "no-unneeded-ternary": "error", + "no-whitespace-before-property": "error", + "object-curly-spacing": ["error", "always"], + "one-var": ["error", "never"], + "operator-assignment": ["error", "always"], + "padded-blocks": [ + "error", + { + "classes": "never" + } + ], + "padding-line-between-statements": [ + "error", + { + "blankLine": "always", + "prev": ["const", "let"], + "next": "*" + }, + { + "blankLine": "any", + "prev": ["const", "let"], + "next": ["const", "let", "var"] + }, + { + "blankLine": "always", + "prev": "directive", + "next": "*" + }, + { + "blankLine": "any", + "prev": "directive", + "next": "directive" + }, + { + "blankLine": "always", + "prev": "break", + "next": "*" + }, + { + "blankLine": "always", + "prev": "block-like", + "next": "*" + } + ], + "prefer-object-spread": "error", + "quote-props": ["error", "as-needed"], + "semi-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "semi-style": ["error", "last"], + "sort-keys": "error", + "space-before-blocks": "error", + "space-in-parens": ["error", "never"], + "space-infix-ops": [ + "error", + { + "int32Hint": false + } + ], + "space-unary-ops": "error", + "switch-colon-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "template-tag-spacing": ["error", "never"], + "unicode-bom": ["error", "never"], + "arrow-parens": ["error", "as-needed"], + "arrow-spacing": [ + "error", + { + "before": true, + "after": true + } + ], + "generator-star-spacing": [ + "error", + { + "before": true, + "after": false + } + ], + "no-confusing-arrow": "error", + "no-duplicate-imports": "error", + "no-useless-computed-key": "error", + "no-useless-rename": "error", + "no-var": "error", + "object-shorthand": ["error", "properties"], + "prefer-arrow-callback": "error", + "prefer-const": "error", + "prefer-destructuring": "error", + "prefer-numeric-literals": "error", + "prefer-rest-params": "error", + "prefer-spread": "error", + "prefer-template": "error", + "rest-spread-spacing": ["error", "never"], + "sort-imports": [ + "error", + { + "ignoreCase": true, + "ignoreDeclarationSort": true + } + ], + "template-curly-spacing": ["error", "never"], + "yield-star-spacing": ["error", "before"] + } +} diff --git a/playground/package.json b/playground/package.json index 6a0a531c1..3a6acdc53 100644 --- a/playground/package.json +++ b/playground/package.json @@ -15,6 +15,8 @@ "check": "tsc -p tsconfig.json --noEmit", "prepush": "npm run check", "lint": "tslint --config tslint.json --project tsconfig.json", + "eslint:fix": "eslint --fix '**/*.{ts,tsx}'", + "eslint:check": "eslint '**/*.{ts,tsx}'", "prettier:fix": "prettier --write '**/*.{ts,tsx,js,jsx,json,scss}'", "prettier:check": "prettier --check '**/*.{t,j}s'" }, @@ -50,6 +52,11 @@ "@types/react-router-dom": "^5.1.6", "@types/react-test-renderer": "^16.9.2", "@types/uuid": "^8.3.0", + "@typescript-eslint/eslint-plugin": "^4.4.1", + "@typescript-eslint/parser": "^4.4.1", + "eslint": "^7.11.0", + "eslint-config-prettier": "^6.10.0", + "eslint-plugin-prettier": "^3.1.2", "autoprefixer": "^10.0.1", "cache-loader": "^4.1.0", "classnames": "^2.2.6", diff --git a/playground/src/components/header/index.tsx b/playground/src/components/header/index.tsx index a0890b897..e258d8800 100644 --- a/playground/src/components/header/index.tsx +++ b/playground/src/components/header/index.tsx @@ -15,11 +15,10 @@ interface LinkInfo { } const links: LinkInfo[] = [ - { to: "/playground", label: "Endpoints", icon: faClone }, - { to: "/playground/configuration", label: "Configuration", icon: faCog }, + { icon: faClone, label: "Endpoints", to: "/playground" }, + { icon: faCog, label: "Configuration", to: "/playground/configuration" }, ]; -export const MainHeader = observer(Header); function Header() { const { routerStore } = React.useContext(RootStore); @@ -49,3 +48,5 @@ function Header() { ); } + +export const MainHeader = observer(Header); diff --git a/playground/src/components/loading/index.tsx b/playground/src/components/loading/index.tsx index cb3802fe9..3a8457a86 100644 --- a/playground/src/components/loading/index.tsx +++ b/playground/src/components/loading/index.tsx @@ -2,14 +2,18 @@ import * as React from "react"; const s = require("./loading.scss"); -export const Loading = () => ( -
-
-
-); +export function Loading(): JSX.Element { + return ( +
+
+
+ ); +} -export const PageLoading = () => ( -
-
-
-); +export function PageLoading(): JSX.Element { + return ( +
+
+
+ ); +} diff --git a/playground/src/components/requestCard/annotations/index.tsx b/playground/src/components/requestCard/annotations/index.tsx index 90a04852b..0b290994d 100644 --- a/playground/src/components/requestCard/annotations/index.tsx +++ b/playground/src/components/requestCard/annotations/index.tsx @@ -5,7 +5,7 @@ interface Props { annotations: ModelAnotations; } -export function Annotations(props: Props) { +export function Annotations(props: Props): JSX.Element { const { args, func } = props.annotations; const isEmpty = func.length === 0 && Object.keys(args).length === 0; @@ -20,12 +20,12 @@ export function Annotations(props: Props) { )); const funcSection = - funcAnnotations.length !== 0 ? ( + funcAnnotations.length === 0 ? null : (
Function
{funcAnnotations}
- ) : null; + ); const argsAnnotations = Object.keys(args).reduce((acc, argName) => { const annotations = args[argName]; @@ -47,12 +47,12 @@ export function Annotations(props: Props) { }, []); const argsSection = - argsAnnotations.length !== 0 ? ( + argsAnnotations.length === 0 ? null : (
Arguments
{argsAnnotations}
- ) : null; + ); return isEmpty ? ( emptyMessage diff --git a/playground/src/components/requestCard/bottom/bottom.test.tsx b/playground/src/components/requestCard/bottom/bottom.test.tsx index 834c73934..c5a896fc1 100644 --- a/playground/src/components/requestCard/bottom/bottom.test.tsx +++ b/playground/src/components/requestCard/bottom/bottom.test.tsx @@ -4,23 +4,26 @@ import * as React from "react"; import Bottom from "."; describe("", () => { - const testForStatus = (status: RequestStatus, text: string, icon: string, colorClass: string) => { + function testForStatus(status: RequestStatus, text: string, icon: string, colorClass: string) { it(`verify label, icon, classes and onClick Callback for ${status}`, () => { const onClickCallback = jest.fn(); const wrapper = mount(); const domNode = wrapper.getDOMNode(); + wrapper.simulate("click"); - // split classnames - // "bottom orange" => ["bottom" , "orange"] + /* + * Split classnames + * "bottom orange" => ["bottom" , "orange"] + */ const classes = domNode.className.split(" "); - expect(onClickCallback).toHaveBeenCalled(); // onCLick callback - expect(domNode.children[0].textContent).toBe(text); // label content - expect(domNode.children[1].getAttribute("data-icon")).toBe(icon); // svg icon - expect(classes[0]).toBe("bottom"); // default class - expect(classes[1]).toBe(colorClass); // color class + expect(onClickCallback).toHaveBeenCalled(); // OnCLick callback + expect(domNode.children[0].textContent).toBe(text); // Label content + expect(domNode.children[1].getAttribute("data-icon")).toBe(icon); // Svg icon + expect(classes[0]).toBe("bottom"); // Default class + expect(classes[1]).toBe(colorClass); // Color class }); - }; + } testForStatus("notFetched", "Make Request", "play", "blue"); testForStatus("fetching", "Fetching", "pause", "orange"); diff --git a/playground/src/components/requestCard/bottom/index.tsx b/playground/src/components/requestCard/bottom/index.tsx index 1debe9785..f78c8a26c 100644 --- a/playground/src/components/requestCard/bottom/index.tsx +++ b/playground/src/components/requestCard/bottom/index.tsx @@ -10,23 +10,23 @@ interface BottomProps { onClick: (status: RequestStatus) => void; status: RequestStatus; } -export default function Bottom(props: BottomProps) { +export default function Bottom(props: BottomProps): JSX.Element { const icons: Record = { - notFetched: faPlay, - fetching: faPause, error: faRedo, + fetching: faPause, + notFetched: faPlay, sucess: faRedo, }; const labels: Record = { - notFetched: "Make Request", - fetching: "Fetching", error: "Error, Retry?", + fetching: "Fetching", + notFetched: "Make Request", sucess: "Success, Retry?", }; const colors: Record = { - notFetched: s.blue, - fetching: s.orange, error: s.red, + fetching: s.orange, + notFetched: s.blue, sucess: s.green, }; const selectedIcon = icons[props.status]; diff --git a/playground/src/components/requestCard/content/index.tsx b/playground/src/components/requestCard/content/index.tsx index bc1c22b04..3f338526b 100644 --- a/playground/src/components/requestCard/content/index.tsx +++ b/playground/src/components/requestCard/content/index.tsx @@ -16,12 +16,15 @@ interface ContentProps { model: requestModel; } -export default observer(Content); - function Content(props: ContentProps) { const { activeTab, args, setJsonArgs, model } = props; - const Content = componentSwitch(activeTab, { + const content = componentSwitch(activeTab, { + annotations: ( +
+ +
+ ), arguments: ( { @@ -44,14 +47,12 @@ function Content(props: ContentProps) { }} /> ), - response: ( -
- -
- ), - annotations: ( + default: (
- +

+ Did you know this is a work in progress? +

+

... well, now you know.

), error: ( @@ -59,15 +60,14 @@ function Content(props: ContentProps) {
), - default: ( + response: (
-

- Did you know this is a work in progress? -

-

... well, now you know.

+
), }); - return
{Content}
; + return
{content}
; } + +export default observer(Content); diff --git a/playground/src/components/requestCard/errors/index.tsx b/playground/src/components/requestCard/errors/index.tsx index 9a4e08c1d..3d5ddb75f 100644 --- a/playground/src/components/requestCard/errors/index.tsx +++ b/playground/src/components/requestCard/errors/index.tsx @@ -5,6 +5,6 @@ interface Props { error: string | undefined; } -export function Errors(props: Props) { +export function Errors(props: Props): JSX.Element { return props.error ?
{props.error}
:
There are no errors
; } diff --git a/playground/src/components/requestCard/header/index.tsx b/playground/src/components/requestCard/header/index.tsx index 1522081a7..b550699db 100644 --- a/playground/src/components/requestCard/header/index.tsx +++ b/playground/src/components/requestCard/header/index.tsx @@ -11,24 +11,24 @@ interface HeaderProps { model: requestModel; closeCard?: () => void; } -export default observer(Header); + function Header(props: HeaderProps) { const { open, closeCard, model } = props; const colors: Record = { - notFetched: s.gray, - fetching: s.orange, error: s.red, + fetching: s.orange, + notFetched: s.gray, sucess: s.green, }; const accentColorClass = colors[model.status]; - const onClickBookmark = (event: React.MouseEvent) => { + function onClickBookmark(event: React.MouseEvent) { event.stopPropagation(); model.toogleBookmark(); - }; + } - if (!open) + if (!open) { return ( <>
@@ -48,6 +48,7 @@ function Header(props: HeaderProps) {
); + } return (
@@ -69,3 +70,5 @@ function Header(props: HeaderProps) {
); } + +export default observer(Header); diff --git a/playground/src/components/requestCard/index.tsx b/playground/src/components/requestCard/index.tsx index db5803738..53e22be0b 100644 --- a/playground/src/components/requestCard/index.tsx +++ b/playground/src/components/requestCard/index.tsx @@ -11,19 +11,19 @@ interface CardProps { model: requestModel; } -export const RequestCard = observer(Card); function Card(props: CardProps) { const [open, setOpen] = React.useState(false); const [activeTab, setActiveTab] = React.useState("arguments"); const [jsonArgs, setJsonArgs] = React.useState(props.model.args); const { args, status } = props.model; - if (!open) + if (!open) { return (
setOpen(true)}>
); + } return (
@@ -32,7 +32,7 @@ function Card(props: CardProps) { { + onClick={() => { props.model.reset(); props.model.call(jsonArgs, newStatus => (newStatus === "sucess" ? setActiveTab("response") : setActiveTab("error"))); }} @@ -40,3 +40,5 @@ function Card(props: CardProps) {
); } + +export const RequestCard = observer(Card); diff --git a/playground/src/components/requestCard/tabs/index.tsx b/playground/src/components/requestCard/tabs/index.tsx index 2a9ec56d3..fd2380f6c 100644 --- a/playground/src/components/requestCard/tabs/index.tsx +++ b/playground/src/components/requestCard/tabs/index.tsx @@ -12,7 +12,7 @@ interface TabInfo { label: string; key: TabKeys; } -export default function Tabs(props: TabsProps) { +export default function Tabs(props: TabsProps): JSX.Element { const tabs: TabInfo[] = [ { key: "arguments", label: "Arguments" }, { key: "response", label: "Response" }, diff --git a/playground/src/components/searchInput/index.tsx b/playground/src/components/searchInput/index.tsx index be88aa00f..3901a031e 100644 --- a/playground/src/components/searchInput/index.tsx +++ b/playground/src/components/searchInput/index.tsx @@ -7,7 +7,7 @@ interface Props { onChange: (value: string) => void; } -export function SearchInput(props: Props) { +export function SearchInput(props: Props): JSX.Element { return (
props.onChange(ev.target.value)} /> diff --git a/playground/src/components/searchInput/searchInput.test.tsx b/playground/src/components/searchInput/searchInput.test.tsx index 722566716..e0dcf9d6a 100644 --- a/playground/src/components/searchInput/searchInput.test.tsx +++ b/playground/src/components/searchInput/searchInput.test.tsx @@ -8,6 +8,7 @@ describe("", () => { const onChangeCallback = jest.fn(); const wrapper = mount(); const input = wrapper.find("input"); + input.simulate("change", { target: { value: mockValue } }); expect(onChangeCallback).toHaveBeenCalled(); expect(onChangeCallback).toHaveBeenCalledWith(mockValue); diff --git a/playground/src/components/section/index.tsx b/playground/src/components/section/index.tsx index 99a9dee37..faa32c36f 100644 --- a/playground/src/components/section/index.tsx +++ b/playground/src/components/section/index.tsx @@ -1,5 +1,5 @@ -import * as React from "react"; import classnames from "classnames"; +import * as React from "react"; import s from "./section.scss"; interface TitleProps { @@ -7,7 +7,7 @@ interface TitleProps { featured?: boolean; } -export function Section(props: React.PropsWithChildren) { +export function Section(props: React.PropsWithChildren): JSX.Element { const { title, featured, children } = props; return ( diff --git a/playground/src/configuration/index.ts b/playground/src/configuration/index.ts index facc3b029..8fb296337 100644 --- a/playground/src/configuration/index.ts +++ b/playground/src/configuration/index.ts @@ -8,10 +8,11 @@ export interface ConfigEnvs { isProductionBuild: boolean; } -export function setupConfiguration() { +export function setupConfiguration(): void { const configEnvs: ConfigEnvs = { isProductionBuild, }; + setupPolyfills(); setupServiceWorker(configEnvs); } diff --git a/playground/src/configuration/polyfills.ts b/playground/src/configuration/polyfills.ts index 4acd04804..0437de010 100644 --- a/playground/src/configuration/polyfills.ts +++ b/playground/src/configuration/polyfills.ts @@ -1,3 +1,4 @@ -export function setupPolyfills() { +export function setupPolyfills(): void { + // eslint-disable-next-line global-require window.Buffer = require("buffer"); } diff --git a/playground/src/configuration/serviceWorker.ts b/playground/src/configuration/serviceWorker.ts index 8cfdba08c..190322bd6 100644 --- a/playground/src/configuration/serviceWorker.ts +++ b/playground/src/configuration/serviceWorker.ts @@ -1,12 +1,17 @@ +/* eslint-disable camelcase */ import { ConfigEnvs } from "."; -export function setupServiceWorker(configEnvs: ConfigEnvs) { - if (!configEnvs.isProductionBuild) return; //should not use service worker on development mode +export function setupServiceWorker(configEnvs: ConfigEnvs): void { + if (!configEnvs.isProductionBuild) { + return; + } // Should not use service worker on development mode + // eslint-disable-next-line global-require const runtime = require("offline-plugin/runtime"); + runtime.install({ onUpdateReady: () => runtime.applyUpdate(), - // tslint:disable-next-line: deprecation + // Tslint:disable-next-line: deprecation onUpdated: () => window.location.reload(true), }); diff --git a/playground/src/containers/app/index.tsx b/playground/src/containers/app/index.tsx index 0c1aa3f84..72c10f88b 100644 --- a/playground/src/containers/app/index.tsx +++ b/playground/src/containers/app/index.tsx @@ -1,13 +1,15 @@ import * as React from "react"; import ErrorBoundary from "../errorBoundary"; const s = require("./app.scss"); + interface Props {} -function App(props: React.PropsWithChildren) { +function App(props: React.PropsWithChildren): JSX.Element { return (
{props.children}
); } + export { App }; diff --git a/playground/src/containers/errorBoundary/index.tsx b/playground/src/containers/errorBoundary/index.tsx index d5ac08943..d5281441f 100644 --- a/playground/src/containers/errorBoundary/index.tsx +++ b/playground/src/containers/errorBoundary/index.tsx @@ -15,8 +15,8 @@ export default class ErrorBoundary extends React.Component { showError: false, }; - public componentDidCatch(error: Error, _errorInfo: React.ErrorInfo) { - this.setState({ haveError: true, error }); + public componentDidCatch(error: Error): void { + this.setState({ error, haveError: true }); } private reloadPage = () => { @@ -28,11 +28,11 @@ export default class ErrorBoundary extends React.Component { window.location.reload(true); }; - private toogleErrorDisplay = () => { + private toogleErrorDisplay() { this.setState({ ...this.state, showError: !this.state.showError }); - }; + } - public render() { + public render(): React.ReactNode { const { haveError, error, showError } = this.state; if (haveError) { diff --git a/playground/src/containers/mainWrapper/index.tsx b/playground/src/containers/mainWrapper/index.tsx index 502285099..edc5dd7b4 100644 --- a/playground/src/containers/mainWrapper/index.tsx +++ b/playground/src/containers/mainWrapper/index.tsx @@ -3,7 +3,8 @@ import * as React from "react"; import s from "./mainWrapper.scss"; interface Props {} -function MainWrapper(props: React.PropsWithChildren) { + +function MainWrapper(props: React.PropsWithChildren): JSX.Element { return (
diff --git a/playground/src/containers/routes/index.tsx b/playground/src/containers/routes/index.tsx index 0b85413d5..1b34eae40 100644 --- a/playground/src/containers/routes/index.tsx +++ b/playground/src/containers/routes/index.tsx @@ -14,9 +14,9 @@ const asyncOptions: Options = { fallback: , }; -const NotFound = loadable(() => import("pages/notfound"), asyncOptions); -const Home = loadable(() => import("pages/home"), asyncOptions); -const Configuration = loadable(() => import("pages/condiguration"), asyncOptions); +const NotFound = loadable(async () => import("pages/notfound"), asyncOptions); +const Home = loadable(async () => import("pages/home"), asyncOptions); +const Configuration = loadable(async () => import("pages/condiguration"), asyncOptions); export const Routes = observer(() => { return ( diff --git a/playground/src/helpers/arrayHelpers/index.ts b/playground/src/helpers/arrayHelpers/index.ts index 3ab6842db..d97db5823 100644 --- a/playground/src/helpers/arrayHelpers/index.ts +++ b/playground/src/helpers/arrayHelpers/index.ts @@ -1,4 +1,5 @@ -export function sample(array: T[] | null) { +export function sample(array: T[] | null): T | undefined { const length = array === null ? 0 : array.length; + return length && array ? array[Math.floor(Math.random() * length)] : undefined; } diff --git a/playground/src/helpers/componentSwitch/index.tsx b/playground/src/helpers/componentSwitch/index.tsx index b59175b98..96877f20b 100644 --- a/playground/src/helpers/componentSwitch/index.tsx +++ b/playground/src/helpers/componentSwitch/index.tsx @@ -1,8 +1,7 @@ import * as React from "react"; -export function componentSwitch( +export function componentSwitch( choice: Options, - //@ts-ignore components: Record, ): React.ReactNode { const hasDefault = components.default !== undefined; @@ -12,7 +11,7 @@ export function componentSwitch( return components[choice]; } else if (hasDefault) { return components.default; - } else { - return null; } + + return null; } diff --git a/playground/src/helpers/localStorage/bookmarkedEndpoints.ts b/playground/src/helpers/localStorage/bookmarkedEndpoints.ts index f3ffc5938..3ed7c29a3 100644 --- a/playground/src/helpers/localStorage/bookmarkedEndpoints.ts +++ b/playground/src/helpers/localStorage/bookmarkedEndpoints.ts @@ -2,26 +2,28 @@ import { safeLocalStorage } from "./safeLocalStorage"; const BOOKMARK_LOCALSTORAGE_KEY = "bookmarked_endpoints"; -export function persistEndpointBookmarkStatus(name: string, status: boolean) { - const currentBookmaked = getLocalStorageBookmarks(); - if (status) { - // bookmark - setLocalStorageBookmarks([...currentBookmaked, name]); - } else { - // unbookmark - setLocalStorageBookmarks(currentBookmaked.filter(n => n !== name)); - } -} - export function getLocalStorageBookmarks(): string[] { const content = safeLocalStorage.getItem(BOOKMARK_LOCALSTORAGE_KEY); + if (content) { return JSON.parse(content); - } else { - return []; } + + return []; } -export function setLocalStorageBookmarks(names: string[]) { +export function setLocalStorageBookmarks(names: string[]): void { safeLocalStorage.setItem(BOOKMARK_LOCALSTORAGE_KEY, JSON.stringify(names)); } + +export function persistEndpointBookmarkStatus(name: string, status: boolean): void { + const currentBookmaked = getLocalStorageBookmarks(); + + if (status) { + // Bookmark + setLocalStorageBookmarks([...currentBookmaked, name]); + } else { + // Unbookmark + setLocalStorageBookmarks(currentBookmaked.filter(n => n !== name)); + } +} diff --git a/playground/src/helpers/requestModel/index.ts b/playground/src/helpers/requestModel/index.ts index ea8d98538..4a93594b8 100644 --- a/playground/src/helpers/requestModel/index.ts +++ b/playground/src/helpers/requestModel/index.ts @@ -1,13 +1,13 @@ +import { persistEndpointBookmarkStatus } from "helpers/localStorage/bookmarkedEndpoints"; import { observable } from "mobx"; import { AnnotationJson } from "resources/types/ast"; -import { persistEndpointBookmarkStatus } from "helpers/localStorage/bookmarkedEndpoints"; export type RequestStatus = "notFetched" | "sucess" | "fetching" | "error"; export interface ModelAnotations { func: AnnotationJson[]; args: Record< - string, // argumentName + string, // ArgumentName AnnotationJson[] >; } @@ -23,9 +23,13 @@ interface ConstructorArgument { export class requestModel { public args: any; + public baseUrl: string; + public deviceId: string; + public name: string; + public annotations: ModelAnotations; @observable @@ -43,12 +47,12 @@ export class requestModel { @observable public bookmarked: boolean; - public async toogleBookmark() { + public toogleBookmark(): void { this.bookmarked = !this.bookmarked; persistEndpointBookmarkStatus(this.name, this.bookmarked); } - public async call(args: any, callBack?: (status: RequestStatus) => void) { + public async call(args: unknown, callBack?: (status: RequestStatus) => void): Promise { this.args = args; this.loading = true; this.status = "fetching"; @@ -56,44 +60,51 @@ export class requestModel { const url = `${this.baseUrl}/${this.name}`; const requestBody = { - id: "sdkgen-playground", + args, device: { - type: "web", id: this.deviceId, + type: "web", }, + id: "sdkgen-playground", name: this.name, - args, }; try { const r = await fetch(url, { - method: "POST", + body: JSON.stringify(requestBody), cache: "no-cache", headers: { "Content-Type": "application/json", }, - body: JSON.stringify(requestBody), + method: "POST", }); const res = await r.json(); + if (res.ok) { this.response = res.result; this.status = "sucess"; - if (callBack) callBack("sucess"); + if (callBack) { + callBack("sucess"); + } } else { this.status = "error"; this.error = `${res.error.type}: ${res.error.message}`; - if (callBack) callBack("error"); + if (callBack) { + callBack("error"); + } } } catch (err) { this.status = "error"; this.error = err.message; - if (callBack) callBack("error"); + if (callBack) { + callBack("error"); + } } finally { this.loading = false; } } - public reset() { + public reset(): void { this.loading = false; this.response = undefined; this.error = undefined; @@ -101,7 +112,7 @@ export class requestModel { } constructor(config: ConstructorArgument) { - //MOCK + // MOCK this.args = config.defaultArgsMock; this.name = config.name; this.deviceId = config.deviceId; diff --git a/playground/src/pages/home/index.tsx b/playground/src/pages/home/index.tsx index ba45a479b..b8e4d8ffc 100644 --- a/playground/src/pages/home/index.tsx +++ b/playground/src/pages/home/index.tsx @@ -1,12 +1,12 @@ -import * as React from "react"; -import { Section } from "components/section"; import { RequestCard } from "components/requestCard"; import { SearchInput } from "components/searchInput"; +import { Section } from "components/section"; +import { requestModel } from "helpers/requestModel"; import { observer } from "mobx-react-lite"; +import * as React from "react"; import RootStore from "stores"; import { useDebounce } from "use-debounce"; import s from "./home.scss"; -import { requestModel } from "helpers/requestModel"; function Home() { const { requestsStore } = React.useContext(RootStore); @@ -15,24 +15,29 @@ function Home() { const { api } = requestsStore; - const filterBySearch = (value: [string, requestModel], _index: number, _array: [string, requestModel][]): boolean => { + function filterBySearch(value: [string, requestModel]): boolean { const [fnName] = value; + return fnName.toLocaleLowerCase().includes(searchStringDebounced.toLocaleLowerCase()); - }; + } - const filterBookmarked = (value: [string, requestModel], _index: number, _array: [string, requestModel][]): boolean => { + function filterBookmarked(value: [string, requestModel]): boolean { const [, model] = value; + return model.bookmarked; - }; + } - const renderCard = (value: [string, requestModel], _index: number, _array: [string, requestModel][]): JSX.Element => { + function renderCard(value: [string, requestModel]): JSX.Element { const [fnName, model] = value; + return ; - }; + } + + const list = Object.entries(api).filter(filterBySearch); - const BookmarkedCards = Object.entries(api).filter(filterBySearch).filter(filterBookmarked).map(renderCard); + const BookmarkedCards = list.filter(filterBookmarked).map(renderCard); - const AllCards = Object.entries(api).filter(filterBySearch).map(renderCard); + const AllCards = list.map(renderCard); const hasBookmarks = BookmarkedCards.length > 0; diff --git a/playground/src/pages/notfound/index.tsx b/playground/src/pages/notfound/index.tsx index bae4e4c7f..cf762c429 100644 --- a/playground/src/pages/notfound/index.tsx +++ b/playground/src/pages/notfound/index.tsx @@ -1,6 +1,6 @@ import * as React from "react"; -function NotFound() { +function NotFound(): JSX.Element { return

Essa página não existe

; } diff --git a/playground/src/resources/types/ast.d.ts b/playground/src/resources/types/ast.d.ts index f6d6d633a..9bc4f8a93 100644 --- a/playground/src/resources/types/ast.d.ts +++ b/playground/src/resources/types/ast.d.ts @@ -1,10 +1,12 @@ +export type TypeDescription = string | string[] | { [name: string]: TypeDescription }; + export interface TypeTable { [name: string]: TypeDescription; } -export type ArgsType = { +export interface ArgsType { [arg: string]: TypeDescription; -}; +} export interface FunctionTable { [name: string]: { @@ -13,8 +15,6 @@ export interface FunctionTable { }; } -export type TypeDescription = string | string[] | { [name: string]: TypeDescription }; - interface AnnotationJson { type: string; value: any; diff --git a/playground/src/resources/types/modules.d.ts b/playground/src/resources/types/modules.d.ts index 24c649038..c5ccd7c79 100644 --- a/playground/src/resources/types/modules.d.ts +++ b/playground/src/resources/types/modules.d.ts @@ -1,4 +1,5 @@ -// sass imports +/* eslint-disable camelcase */ +// Sass imports declare module "*.scss" { const styles: { [key: string]: string }; export default styles; diff --git a/playground/src/stores/config.ts b/playground/src/stores/config.ts index 504c9183d..0a6d3e38b 100644 --- a/playground/src/stores/config.ts +++ b/playground/src/stores/config.ts @@ -1,12 +1,16 @@ +import { safeLocalStorage } from "helpers/localStorage/safeLocalStorage"; import { observable } from "mobx"; import { RootStore } from "."; -import { safeLocalStorage } from "helpers/localStorage/safeLocalStorage"; const endpointUrlFallback = process.env.NODE_ENV === "development" ? `http://localhost:${process.env.SERVER_PORT}` : location.origin; function randomBytesHex(len: number) { let hex = ""; - for (let i = 0; i < 2 * len; ++i) hex += "0123456789abcdef"[Math.floor(Math.random() * 16)]; + + for (let i = 0; i < 2 * len; ++i) { + hex += "0123456789abcdef"[Math.floor(Math.random() * 16)]; + } + return hex; } @@ -14,9 +18,11 @@ export class ConfigStore { public rootStore: RootStore; @observable - public deviceId: null | string = null; + public deviceId!: string; + @observable public endpointUrl!: string; + @observable public canChangeEndpoint = false; @@ -25,24 +31,29 @@ export class ConfigStore { this.syncWithLocalStorage(false); } - public setNewEndpoint = (newEndpoint: string) => { + public setNewEndpoint(newEndpoint: string): void { this.endpointUrl = newEndpoint; this.syncWithLocalStorage(true); this.rootStore.requestsStore.fetchAST(); - }; + } - public setNewDeviceId = (newDeviceId: string) => { + public setNewDeviceId(newDeviceId: string): void { this.deviceId = newDeviceId; this.syncWithLocalStorage(true); - }; + } + + private syncWithLocalStorage(override: boolean) { + if (override) { + if (this.deviceId) { + safeLocalStorage.setItem("deviceId", this.deviceId); + } - private syncWithLocalStorage = (override: boolean) => { - if (!override) { + if (this.endpointUrl) { + safeLocalStorage.setItem("endpointUrl", this.endpointUrl); + } + } else { this.deviceId = safeLocalStorage.getItem("deviceId") || randomBytesHex(16); this.endpointUrl = (this.canChangeEndpoint && safeLocalStorage.getItem("endpointUrl")) || endpointUrlFallback; - } else { - if (this.deviceId) safeLocalStorage.setItem("deviceId", this.deviceId); - if (this.endpointUrl) safeLocalStorage.setItem("endpointUrl", this.endpointUrl); } - }; + } } diff --git a/playground/src/stores/index.ts b/playground/src/stores/index.ts index e9f0e317a..9fce8a2bb 100644 --- a/playground/src/stores/index.ts +++ b/playground/src/stores/index.ts @@ -5,8 +5,15 @@ import { RequestsStore } from "./requests"; export class RootStore { public routerStore = new RouterStore(); - public configStore = new ConfigStore(this); - public requestsStore = new RequestsStore(this); + + public configStore: ConfigStore; + + public requestsStore: RequestsStore; + + constructor() { + this.configStore = new ConfigStore(this); + this.requestsStore = new RequestsStore(this); + } } export const rootStore = new RootStore(); diff --git a/playground/src/stores/requests.ts b/playground/src/stores/requests.ts index 145e4b3e2..dec6f6b3a 100644 --- a/playground/src/stores/requests.ts +++ b/playground/src/stores/requests.ts @@ -21,70 +21,77 @@ export class RequestsStore { this.fetchAST(); } - public fetchAST = async () => { + public async fetchAST(): Promise { try { const response = await fetch(`${this.rootStore.configStore.endpointUrl}/ast.json`); const ast = await response.json(); + this.AST = ast; - if (ast) this.createModels(ast); + if (ast) { + this.createModels(ast); + } } catch (err) { console.log(err); } - }; + } - private createMockBasedOnTypes = (args: ArgsType, typeTable: TypeTable) => { + private createMockBasedOnTypes(args: ArgsType, typeTable: TypeTable) { return Object.keys(args).reduce((acc, curKey) => ({ ...acc, [curKey]: this.encodeTransform(typeTable, args[curKey]) }), {}); - }; + } - private simpleTypeMock = (type: string) => { + private simpleTypeMock(type: string) { const types: Record = { - json: { anything: [1, 2, 3] }, - bool: true, - hex: "deadbeef", - uuid: uuidV4(), base64: "c2RrZ2Vu", - int: 123, - uint: 123, - float: 12.3, - money: 123, - void: undefined, - latlng: undefined, - string: "string", + bool: true, cep: undefined, cnpj: undefined, cpf: undefined, email: "hello@example.com", + float: 12.3, + hex: "deadbeef", + int: 123, + json: { anything: [1, 2, 3] }, + latlng: undefined, + money: 123, phone: undefined, safehtml: "Hello", + string: "string", + uint: 123, url: location.origin, + uuid: uuidV4(), + void: undefined, xml: "", }; + if (types[type] === undefined) { console.log(`Unknown simple type '${type}'`); return null; } + return types[type]; - }; + } - private encodeTransform = (typeTable: TypeTable, type: TypeDescription): any => { + private encodeTransform(typeTable: TypeTable, type: TypeDescription): any { if (Array.isArray(type)) { - // things like "car" | "motorcycle" + // Things like "car" | "motorcycle" return type[0]; } else if (typeof type === "object") { - // resolution of complex type + // Resolution of complex type const obj: any = {}; - for (const key in type) { - obj[key] = this.encodeTransform(typeTable, type[key]); + + for (const [key, value] of Object.entries(type)) { + obj[key] = this.encodeTransform(typeTable, value); } + return obj; } else if (type.endsWith("?")) { - // nullish + // Nullish return this.encodeTransform(typeTable, type.replace("?", "")); } else if (type.endsWith("[]")) { - // arrayOf + // ArrayOf return [1, 2, 3].map(() => this.encodeTransform(typeTable, type.replace("[]", ""))); } else if (simpleTypes.includes(type)) { - // simple types + // Simple types return this.simpleTypeMock(type); } else if (type === "bytes") { return "deadbeef"; @@ -92,38 +99,42 @@ export class RequestsStore { return new Date().toISOString().split("T")[0]; } else if (type === "datetime") { return new Date().toISOString().replace("Z", ""); - } else { - // complex type - const resolved = typeTable[type]; - if (resolved) { - return this.encodeTransform(typeTable, resolved); - } else { - throw new Error(`Unknown type '${type}'`); - } - return "complex type"; } - }; + + // Complex type + const resolved = typeTable[type]; + + if (resolved) { + return this.encodeTransform(typeTable, resolved); + } + + throw new Error(`Unknown type '${type}'`); + } public getAnotations = (AST: AstJson, functionName: string): ModelAnotations => { const functionAnnotations = AST.annotations[`fn.${functionName}`] || []; - const regex = RegExp(`fn.${functionName}\\.[^\.]*`); + const regex = RegExp(`fn.${functionName}\\.[^.]*`, "u"); const argsKeys = Object.keys(AST.annotations).filter(target => regex.test(target)); const argsAnnotations = argsKeys.reduce((acc, argKey) => { - // breaks 'fn.getBalance.bankCode' into ["fn", "getBalance", "bankCode"] - // and gets the last part, that is the arguemnt name - const argName = argKey.split(".")[2]; + /* + * Breaks 'fn.getBalance.bankCode' into ["fn", "getBalance", "bankCode"] + * and gets the last part, that is the argument name + */ + const pieces = argKey.split("."); + return { ...acc, - [argName]: AST.annotations[argKey], + [pieces[2]]: AST.annotations[argKey], }; }, {}); const annotations: ModelAnotations = { - func: functionAnnotations, args: argsAnnotations, + func: functionAnnotations, }; + return annotations; }; @@ -131,26 +142,28 @@ export class RequestsStore { return getLocalStorageBookmarks().reduce((acc, name) => ({ ...acc, [name]: true }), {}); }; - public createModels = (AST: AstJson) => { + public createModels(AST: AstJson): void { console.log("createModels"); const { endpointUrl, deviceId } = this.rootStore.configStore; const FNs = Object.entries(AST.functionTable); const bookmarkedEndpointsIndex = this.createBookmarkedEndpointIndex(); + this.api = FNs.reduce((acc, [fName, fStruct]) => { const argsMock = this.createMockBasedOnTypes(fStruct.args, AST.typeTable); const annotations = this.getAnotations(AST, fName); + return { ...acc, [fName]: new requestModel({ - name: fName, - defaultArgsMock: argsMock, - baseUrl: endpointUrl, - deviceId: deviceId!, annotations, + baseUrl: endpointUrl, bookmarked: Boolean(bookmarkedEndpointsIndex[fName]), + defaultArgsMock: argsMock, + deviceId, + name: fName, }), }; }, {}); - }; + } } diff --git a/playground/tsconfig.json b/playground/tsconfig.json index 5c0659a1b..496726478 100644 --- a/playground/tsconfig.json +++ b/playground/tsconfig.json @@ -38,6 +38,6 @@ "types/*": ["types/*"] } }, - "include": ["src/", "_tests_"], + "include": ["src/", "_tests_", "index.d.ts"], "exclude": ["node_modules", "!node_modules/@types", "**/__tests__/*", "dist"] } diff --git a/typescript-generator/.eslintrc.json b/typescript-generator/.eslintrc.json new file mode 100644 index 000000000..9c2b6a1a8 --- /dev/null +++ b/typescript-generator/.eslintrc.json @@ -0,0 +1,423 @@ +{ + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended" + ], + "env": { + "node": true, + "es2017": true, + "jest": true + }, + "globals": { + "BigInt": true + }, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2019, + "project": "tsconfig.json", + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint/eslint-plugin", + "prettier" + ], + "rules": { + "prettier/prettier": "error", + "spaced-comment": [ + "error", + "always" + ], + "@typescript-eslint/array-type": [ + "error", + { + "default": "array-simple" + } + ], + "@typescript-eslint/brace-style": "error", + "@typescript-eslint/consistent-type-definitions": [ + "error", + "interface" + ], + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-var-requires": "off", + "@typescript-eslint/func-call-spacing": [ + "error", + "never" + ], + "@typescript-eslint/interface-name-prefix": "off", + "@typescript-eslint/member-delimiter-style": "off", + "@typescript-eslint/no-empty-interface": "off", + "@typescript-eslint/no-unnecessary-qualifier": "error", + "@typescript-eslint/no-unnecessary-type-arguments": "error", + "@typescript-eslint/no-unused-expressions": "error", + "@typescript-eslint/no-use-before-define": "error", + "@typescript-eslint/no-useless-constructor": "error", + "@typescript-eslint/prefer-for-of": "error", + "@typescript-eslint/promise-function-async": "error", + "@typescript-eslint/require-array-sort-compare": "error", + "@typescript-eslint/restrict-plus-operands": "error", + "@typescript-eslint/semi": [ + "error", + "always", + { + "omitLastInOneLineBlock": false + } + ], + "array-callback-return": "error", + "block-scoped-var": "error", + "consistent-return": "error", + "curly": "error", + "default-case": "error", + "default-param-last": "error", + "dot-notation": "error", + "eqeqeq": [ + "error", + "always" + ], + "global-require": "error", + "guard-for-in": "error", + "no-alert": "error", + "no-buffer-constructor": "error", + "no-caller": "error", + "no-div-regex": "error", + "no-else-return": "error", + "no-empty-function": [ + "error", + { + "allow": [ + "constructors" + ] + } + ], + "no-eq-null": "error", + "no-eval": "error", + "no-extend-native": "error", + "no-extra-bind": "error", + "no-floating-decimal": "error", + "no-implicit-coercion": "error", + "no-implied-eval": "error", + "no-import-assign": "error", + "no-invalid-this": "error", + "no-iterator": "error", + "no-label-var": "error", + "no-labels": "error", + "no-lone-blocks": "error", + "no-loop-func": "error", + "no-multi-spaces": "error", + "no-multi-str": "error", + "no-new-func": "error", + "no-new-wrappers": "error", + "no-new": "error", + "no-octal-escape": "error", + "no-path-concat": "error", + "no-process-exit": "error", + "no-proto": "error", + "no-prototype-builtins": "off", + "no-restricted-modules": [ + "error", + "moment" + ], + "no-return-assign": "error", + "no-return-await": "error", + "no-script-url": "error", + "no-self-compare": "error", + "no-sequences": "error", + "no-shadow": "error", + "no-sync": [ + "error", + { + "allowAtRootLevel": true + } + ], + "no-template-curly-in-string": "error", + "no-throw-literal": "error", + "no-undef-init": "error", + "no-unmodified-loop-condition": "error", + "no-useless-call": "error", + "no-useless-concat": "error", + "no-void": "error", + "prefer-named-capture-group": "warn", + "prefer-regex-literals": "error", + "radix": "error", + "require-unicode-regexp": "error", + "strict": "error", + "wrap-iife": [ + "error", + "any" + ], + "yoda": "error", + "array-bracket-spacing": [ + "error", + "never" + ], + "array-element-newline": [ + "error", + "consistent" + ], + "block-spacing": "error", + "camelcase": "warn", + "capitalized-comments": [ + "error", + "always" + ], + "comma-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "comma-style": [ + "error", + "last" + ], + "computed-property-spacing": [ + "error", + "never" + ], + "eol-last": [ + "error", + "always" + ], + "func-name-matching": [ + "error", + "always" + ], + "func-names": [ + "error", + "as-needed" + ], + "func-style": [ + "error", + "declaration" + ], + "function-call-argument-newline": [ + "error", + "consistent" + ], + "jsx-quotes": [ + "error", + "prefer-double" + ], + "key-spacing": [ + "error", + { + "beforeColon": false + } + ], + "keyword-spacing": [ + "error", + { + "before": true, + "after": true + } + ], + "linebreak-style": [ + "error", + "unix" + ], + "lines-between-class-members": [ + "error", + "always" + ], + "max-depth": [ + "error", + 5 + ], + "max-nested-callbacks": [ + "error", + 10 + ], + "max-params": [ + "error", + 10 + ], + "max-statements-per-line": [ + "error", + { + "max": 2 + } + ], + "multiline-comment-style": [ + "error", + "starred-block" + ], + "new-parens": "error", + "newline-per-chained-call": [ + "error", + { + "ignoreChainWithDepth": 3 + } + ], + "no-array-constructor": "error", + "no-lonely-if": "error", + "no-multi-assign": "error", + "no-multiple-empty-lines": "error", + "no-negated-condition": "error", + "no-new-object": "error", + "no-tabs": "error", + "no-trailing-spaces": "error", + "no-unneeded-ternary": "error", + "no-whitespace-before-property": "error", + "object-curly-spacing": [ + "error", + "always" + ], + "one-var": [ + "error", + "never" + ], + "operator-assignment": [ + "error", + "always" + ], + "padded-blocks": [ + "error", + { + "classes": "never" + } + ], + "padding-line-between-statements": [ + "error", + { + "blankLine": "always", + "prev": [ + "const", + "let" + ], + "next": "*" + }, + { + "blankLine": "any", + "prev": [ + "const", + "let" + ], + "next": [ + "const", + "let", + "var" + ] + }, + { + "blankLine": "always", + "prev": "directive", + "next": "*" + }, + { + "blankLine": "any", + "prev": "directive", + "next": "directive" + }, + { + "blankLine": "always", + "prev": "break", + "next": "*" + }, + { + "blankLine": "always", + "prev": "block-like", + "next": "*" + } + ], + "prefer-object-spread": "error", + "quote-props": [ + "error", + "as-needed" + ], + "semi-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "semi-style": [ + "error", + "last" + ], + "sort-keys": "error", + "space-before-blocks": "error", + "space-in-parens": [ + "error", + "never" + ], + "space-infix-ops": [ + "error", + { + "int32Hint": false + } + ], + "space-unary-ops": "error", + "switch-colon-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "template-tag-spacing": [ + "error", + "never" + ], + "unicode-bom": [ + "error", + "never" + ], + "arrow-parens": [ + "error", + "as-needed" + ], + "arrow-spacing": [ + "error", + { + "before": true, + "after": true + } + ], + "generator-star-spacing": [ + "error", + { + "before": true, + "after": false + } + ], + "no-confusing-arrow": "error", + "no-duplicate-imports": "error", + "no-useless-computed-key": "error", + "no-useless-rename": "error", + "no-var": "error", + "object-shorthand": [ + "error", + "properties" + ], + "prefer-arrow-callback": "error", + "prefer-const": "error", + "prefer-destructuring": "error", + "prefer-numeric-literals": "error", + "prefer-rest-params": "error", + "prefer-spread": "error", + "prefer-template": "error", + "rest-spread-spacing": [ + "error", + "never" + ], + "sort-imports": [ + "error", + { + "ignoreCase": true, + "ignoreDeclarationSort": true + } + ], + "template-curly-spacing": [ + "error", + "never" + ], + "yield-star-spacing": [ + "error", + "before" + ] + } +} diff --git a/typescript-generator/package.json b/typescript-generator/package.json index 30a260908..ce406b58b 100644 --- a/typescript-generator/package.json +++ b/typescript-generator/package.json @@ -5,6 +5,8 @@ "main": "src/index.ts", "scripts": { "test": "jest --passWithNoTests", + "eslint:fix": "eslint --fix '**/*.ts'", + "eslint:check": "eslint '**/*.ts'", "prettier:fix": "prettier --write '**/*.{t,j}s'", "prettier:check": "prettier --check '**/*.{t,j}s'", "build": "tsc" @@ -25,6 +27,11 @@ "devDependencies": { "@types/jest": "^26.0.14", "@types/node": "^14.11.9", + "@typescript-eslint/eslint-plugin": "^4.4.1", + "@typescript-eslint/parser": "^4.4.1", + "eslint": "^7.11.0", + "eslint-config-prettier": "^6.10.0", + "eslint-plugin-prettier": "^3.1.2", "jest": "^26.5.3", "prettier": "^2.1.2", "ts-jest": "^26.4.1", diff --git a/typescript-generator/spec/helpers.spec.ts b/typescript-generator/spec/helpers.spec.ts index bd9c22d9b..ab70772cc 100644 --- a/typescript-generator/spec/helpers.spec.ts +++ b/typescript-generator/spec/helpers.spec.ts @@ -17,6 +17,7 @@ describe("helpers.ts", () => { ], [], ); + structType.name = "awesomeInterface"; expect(generateTypescriptInterface(structType)).toBe(`export interface awesomeInterface { @@ -35,71 +36,85 @@ describe("helpers.ts", () => { test("generateTypescriptTypeName: IntPrimitiveType", () => { const type = new parser.IntPrimitiveType(); + expect(generateTypescriptTypeName(type)).toBe("number"); }); test("generateTypescriptTypeName: UIntPrimitiveType", () => { const type = new parser.UIntPrimitiveType(); + expect(generateTypescriptTypeName(type)).toBe("number"); }); test("generateTypescriptTypeName: MoneyPrimitiveType", () => { const type = new parser.MoneyPrimitiveType(); + expect(generateTypescriptTypeName(type)).toBe("number"); }); test("generateTypescriptTypeName: FloatPrimitiveType", () => { const type = new parser.FloatPrimitiveType(); + expect(generateTypescriptTypeName(type)).toBe("number"); }); test("generateTypescriptTypeName: DateTimePrimitiveType", () => { const type = new parser.DateTimePrimitiveType(); + expect(generateTypescriptTypeName(type)).toBe("Date"); }); test("generateTypescriptTypeName: StringPrimitiveType", () => { const type = new parser.StringPrimitiveType(); + expect(generateTypescriptTypeName(type)).toBe("string"); }); test("generateTypescriptTypeName: CpfPrimitiveType", () => { const type = new parser.CpfPrimitiveType(); + expect(generateTypescriptTypeName(type)).toBe("string"); }); test("generateTypescriptTypeName: CnpjPrimitiveType", () => { const type = new parser.CnpjPrimitiveType(); + expect(generateTypescriptTypeName(type)).toBe("string"); }); test("generateTypescriptTypeName: EmailPrimitiveType", () => { const type = new parser.EmailPrimitiveType(); + expect(generateTypescriptTypeName(type)).toBe("string"); }); test("generateTypescriptTypeName: HtmlPrimitiveType", () => { const type = new parser.HtmlPrimitiveType(); + expect(generateTypescriptTypeName(type)).toBe("string"); }); test("generateTypescriptTypeName: UrlPrimitiveType", () => { const type = new parser.UrlPrimitiveType(); + expect(generateTypescriptTypeName(type)).toBe("string"); }); test("generateTypescriptTypeName: HexPrimitiveType", () => { const type = new parser.HexPrimitiveType(); + expect(generateTypescriptTypeName(type)).toBe("string"); }); test("generateTypescriptTypeName: Base64PrimitiveType", () => { const type = new parser.Base64PrimitiveType(); + expect(generateTypescriptTypeName(type)).toBe("string"); }); test("generateTypescriptTypeName: XmlPrimitiveType", () => { const type = new parser.XmlPrimitiveType(); + expect(generateTypescriptTypeName(type)).toBe("string"); }); @@ -108,6 +123,7 @@ describe("helpers.ts", () => { [new parser.Field("int", new parser.IntPrimitiveType()), new parser.Field("bigint", new parser.BigIntPrimitiveType())], [], ); + structType.name = "simpleInterface"; expect(generateTypescriptTypeName(structType)).toBe(structType.name); }); @@ -122,6 +138,7 @@ describe("helpers.ts", () => { test("generateTypescriptTypeName: TypeReference", () => { const enumType = new parser.TypeReference("typeRef"); + enumType.type = new parser.HexPrimitiveType(); expect(generateTypescriptTypeName(enumType)).toBe("string"); diff --git a/typescript-generator/src/browser-client.ts b/typescript-generator/src/browser-client.ts index 39810879e..df9cc25a9 100644 --- a/typescript-generator/src/browser-client.ts +++ b/typescript-generator/src/browser-client.ts @@ -1,9 +1,7 @@ import { AstRoot, astToJson } from "@sdkgen/parser"; import { generateTypescriptEnum, generateTypescriptErrorClass, generateTypescriptInterface, generateTypescriptTypeName } from "./helpers"; -interface Options {} - -export function generateBrowserClientSource(ast: AstRoot, options: Options) { +export function generateBrowserClientSource(ast: AstRoot): string { let code = ""; code += `import { SdkgenError, SdkgenHttpClient } from "@sdkgen/browser-runtime"; @@ -41,7 +39,7 @@ ${ast.operations code += `const errClasses = {\n${ast.errors.map(err => ` ${err}`).join(",\n")}\n};\n\n`; - code += `const astJson = ${JSON.stringify(astToJson(ast), null, 4).replace(/"(\w+)":/g, "$1:")};\n`; + code += `const astJson = ${JSON.stringify(astToJson(ast), null, 4).replace(/"(?\w+)":/gu, "$:")};\n`; return code; } diff --git a/typescript-generator/src/helpers.ts b/typescript-generator/src/helpers.ts index 76d701bec..09b0d87d2 100644 --- a/typescript-generator/src/helpers.ts +++ b/typescript-generator/src/helpers.ts @@ -28,54 +28,6 @@ import { XmlPrimitiveType, } from "@sdkgen/parser"; -export function generateTypescriptInterface(type: StructType) { - return `export interface ${type.name} { -${type.fields.map(field => ` ${field.name}: ${generateTypescriptTypeName(field.type)}`).join("\n")} -}\n`; -} - -export function generateTypescriptEnum(type: EnumType) { - return `export type ${type.name} = ${type.values.map(x => `"${x.value}"`).join(" | ")};\n`; -} - -export function generateTypescriptErrorClass(name: string) { - return `export class ${name} extends SdkgenError {}\n`; -} - -export function clearForLogging(path: string, type: Type): string { - switch (type.constructor) { - case TypeReference: - return clearForLogging(path, (type as TypeReference).type); - - case OptionalType: { - const code = clearForLogging(path, (type as OptionalType).base); - if (code) return `if (${path} !== null && ${path} !== undefined) { ${code} }`; - else return ""; - } - - case ArrayType: { - const code = clearForLogging("el", (type as ArrayType).base); - if (code) return `for (const el of ${path}) { ${code} }`; - else return ""; - } - - case StructType: - const codes: string[] = []; - for (const field of (type as StructType).fields) { - if (field.secret) { - codes.push(`${path}.${field.name} = "";`); - } else { - const code = clearForLogging(`${path}.${field.name}`, field.type); - if (code) codes.push(code); - } - } - return codes.join(" "); - - default: - return ""; - } -} - export function generateTypescriptTypeName(type: Type): string { switch (type.constructor) { case IntPrimitiveType: @@ -116,13 +68,17 @@ export function generateTypescriptTypeName(type: Type): string { return "any"; case OptionalType: - return generateTypescriptTypeName((type as OptionalType).base) + " | null"; + return `${generateTypescriptTypeName((type as OptionalType).base)} | null`; case ArrayType: { - const base = (type as ArrayType).base; + const { base } = type as ArrayType; const baseGen = generateTypescriptTypeName(base); - if (base instanceof OptionalType) return `(${baseGen})[]`; - else return `${baseGen}[]`; + + if (base instanceof OptionalType) { + return `(${baseGen})[]`; + } + + return `${baseGen}[]`; } case StructType: @@ -136,3 +92,65 @@ export function generateTypescriptTypeName(type: Type): string { throw new Error(`BUG: generateTypescriptTypeName with ${type.constructor.name}`); } } + +export function generateTypescriptInterface(type: StructType): string { + return `export interface ${type.name} { +${type.fields.map(field => ` ${field.name}: ${generateTypescriptTypeName(field.type)}`).join("\n")} +}\n`; +} + +export function generateTypescriptEnum(type: EnumType): string { + return `export type ${type.name} = ${type.values.map(x => `"${x.value}"`).join(" | ")};\n`; +} + +export function generateTypescriptErrorClass(name: string): string { + return `export class ${name} extends SdkgenError {}\n`; +} + +export function clearForLogging(path: string, type: Type): string { + switch (type.constructor) { + case TypeReference: + return clearForLogging(path, (type as TypeReference).type); + + case OptionalType: { + const code = clearForLogging(path, (type as OptionalType).base); + + if (code) { + return `if (${path} !== null && ${path} !== undefined) { ${code} }`; + } + + return ""; + } + + case ArrayType: { + const code = clearForLogging("el", (type as ArrayType).base); + + if (code) { + return `for (const el of ${path}) { ${code} }`; + } + + return ""; + } + + case StructType: { + const codes: string[] = []; + + for (const field of (type as StructType).fields) { + if (field.secret) { + codes.push(`${path}.${field.name} = "";`); + } else { + const code = clearForLogging(`${path}.${field.name}`, field.type); + + if (code) { + codes.push(code); + } + } + } + + return codes.join(" "); + } + + default: + return ""; + } +} diff --git a/typescript-generator/src/interfaces.ts b/typescript-generator/src/interfaces.ts index 9b8dcec58..f55015f10 100644 --- a/typescript-generator/src/interfaces.ts +++ b/typescript-generator/src/interfaces.ts @@ -1,14 +1,13 @@ import { AstRoot } from "@sdkgen/parser"; import { generateTypescriptEnum, generateTypescriptInterface } from "./helpers"; -interface Options {} - -export function generateTypescriptInterfaces(ast: AstRoot, options: Options) { +export function generateTypescriptInterfaces(ast: AstRoot): string { let code = ""; for (const type of ast.enumTypes) { code += generateTypescriptEnum(type); } + code += "\n"; for (const type of ast.structTypes) { diff --git a/typescript-generator/src/node-client.ts b/typescript-generator/src/node-client.ts index 3a8f7a0ac..4aefbf393 100644 --- a/typescript-generator/src/node-client.ts +++ b/typescript-generator/src/node-client.ts @@ -1,9 +1,7 @@ import { AstRoot, astToJson } from "@sdkgen/parser"; import { generateTypescriptEnum, generateTypescriptErrorClass, generateTypescriptInterface, generateTypescriptTypeName } from "./helpers"; -interface Options {} - -export function generateNodeClientSource(ast: AstRoot, options: Options) { +export function generateNodeClientSource(ast: AstRoot): string { let code = ""; code += `import { Context, SdkgenError, SdkgenHttpClient } from "@sdkgen/node-runtime"; @@ -41,7 +39,7 @@ ${ast.operations code += `const errClasses = {\n${ast.errors.map(err => ` ${err}`).join(",\n")}\n};\n\n`; - code += `const astJson = ${JSON.stringify(astToJson(ast), null, 4).replace(/"(\w+)":/g, "$1:")};\n`; + code += `const astJson = ${JSON.stringify(astToJson(ast), null, 4).replace(/"(?\w+)":/gu, "$:")};\n`; return code; } diff --git a/typescript-generator/src/node-server.ts b/typescript-generator/src/node-server.ts index 9bb58b45d..8d8bf901f 100644 --- a/typescript-generator/src/node-server.ts +++ b/typescript-generator/src/node-server.ts @@ -1,9 +1,7 @@ import { AstRoot, astToJson } from "@sdkgen/parser"; import { generateTypescriptEnum, generateTypescriptErrorClass, generateTypescriptInterface, generateTypescriptTypeName } from "./helpers"; -interface Options {} - -export function generateNodeServerSource(ast: AstRoot, options: Options) { +export function generateNodeServerSource(ast: AstRoot): string { let code = ""; code += `import { BaseApiConfig, Context, SdkgenError } from "@sdkgen/node-runtime"; @@ -45,8 +43,8 @@ export function generateNodeServerSource(ast: AstRoot, options: Options) { } astJson = ${JSON.stringify(astToJson(ast), null, 4) - .replace(/"(\w+)":/g, "$1:") - .replace(/\n/g, "\n ")} + .replace(/"(?\w+)":/gu, "$:") + .replace(/\n/gu, "\n ")} } export const api = new ApiConfig<{}>(); From e355916138c4fbbd81f53aeda79ffe591a8e1e68 Mon Sep 17 00:00:00 2001 From: Guilherme Bernal Date: Sat, 17 Oct 2020 12:27:19 +0000 Subject: [PATCH 02/10] feat: add eslint to parser --- .github/workflows/test.yml | 6 + cli/src/build.ts | 8 +- parser/.eslintrc.json | 423 ++++++++++++++++++ parser/package.json | 7 + parser/spec/lexer.spec.ts | 62 +-- parser/spec/parser.spec.ts | 273 +++++------ parser/src/ast.ts | 28 +- parser/src/compatibility/index.ts | 28 +- parser/src/json.ts | 85 ++-- parser/src/lexer.ts | 86 +++- parser/src/parser.ts | 128 ++++-- parser/src/restparser.ts | 84 ++-- .../semantic/01_check_multiple_declaration.ts | 4 +- .../src/semantic/02_match_type_definitions.ts | 4 +- .../semantic/03_check_no_recursive_types.ts | 4 +- .../semantic/04_check_dont_return_secret.ts | 6 +- ...check_naming_for_getters_returning_bool.ts | 5 +- .../semantic/06_give_struct_and_enum_names.ts | 7 +- .../semantic/07_check_empty_struct_or_enum.ts | 10 +- .../08_collect_struct_and_enum_types.ts | 2 +- .../src/semantic/09_apply_struct_spreads.ts | 16 +- .../src/semantic/10_validate_annotations.ts | 44 +- parser/src/semantic/analyser.ts | 2 +- parser/src/semantic/visitor.ts | 31 +- parser/src/token.ts | 25 +- 25 files changed, 999 insertions(+), 379 deletions(-) create mode 100644 parser/.eslintrc.json diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 93078e27e..a6acfec99 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -122,6 +122,8 @@ jobs: working-directory: ./parser - run: npm run prettier:check working-directory: ./parser + - run: npm run eslint:check + working-directory: ./parser - run: npm run build working-directory: ./parser - run: npm test @@ -141,6 +143,8 @@ jobs: working-directory: ./playground - run: npm run prettier:check working-directory: ./playground + - run: npm run eslint:check + working-directory: ./playground - run: npm run build working-directory: ./playground @@ -158,6 +162,8 @@ jobs: working-directory: ./typescript-generator - run: npm run prettier:check working-directory: ./typescript-generator + - run: npm run eslint:check + working-directory: ./typescript-generator - run: npm run build working-directory: ./typescript-generator - run: npm test diff --git a/cli/src/build.ts b/cli/src/build.ts index 61aeb645a..3e564b26f 100644 --- a/cli/src/build.ts +++ b/cli/src/build.ts @@ -70,19 +70,19 @@ export function buildCmd(argv: string[]) { switch (options.target) { case "typescript_nodeserver": { - writeFileSync(options.output, generateNodeServerSource(ast, {})); + writeFileSync(options.output, generateNodeServerSource(ast)); break; } case "typescript_nodeclient": { - writeFileSync(options.output, generateNodeClientSource(ast, {})); + writeFileSync(options.output, generateNodeClientSource(ast)); break; } case "typescript_web": { - writeFileSync(options.output, generateBrowserClientSource(ast, {})); + writeFileSync(options.output, generateBrowserClientSource(ast)); break; } case "typescript_interfaces": { - writeFileSync(options.output, generateTypescriptInterfaces(ast, {})); + writeFileSync(options.output, generateTypescriptInterfaces(ast)); break; } case "flutter": { diff --git a/parser/.eslintrc.json b/parser/.eslintrc.json new file mode 100644 index 000000000..9c2b6a1a8 --- /dev/null +++ b/parser/.eslintrc.json @@ -0,0 +1,423 @@ +{ + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended" + ], + "env": { + "node": true, + "es2017": true, + "jest": true + }, + "globals": { + "BigInt": true + }, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2019, + "project": "tsconfig.json", + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint/eslint-plugin", + "prettier" + ], + "rules": { + "prettier/prettier": "error", + "spaced-comment": [ + "error", + "always" + ], + "@typescript-eslint/array-type": [ + "error", + { + "default": "array-simple" + } + ], + "@typescript-eslint/brace-style": "error", + "@typescript-eslint/consistent-type-definitions": [ + "error", + "interface" + ], + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-var-requires": "off", + "@typescript-eslint/func-call-spacing": [ + "error", + "never" + ], + "@typescript-eslint/interface-name-prefix": "off", + "@typescript-eslint/member-delimiter-style": "off", + "@typescript-eslint/no-empty-interface": "off", + "@typescript-eslint/no-unnecessary-qualifier": "error", + "@typescript-eslint/no-unnecessary-type-arguments": "error", + "@typescript-eslint/no-unused-expressions": "error", + "@typescript-eslint/no-use-before-define": "error", + "@typescript-eslint/no-useless-constructor": "error", + "@typescript-eslint/prefer-for-of": "error", + "@typescript-eslint/promise-function-async": "error", + "@typescript-eslint/require-array-sort-compare": "error", + "@typescript-eslint/restrict-plus-operands": "error", + "@typescript-eslint/semi": [ + "error", + "always", + { + "omitLastInOneLineBlock": false + } + ], + "array-callback-return": "error", + "block-scoped-var": "error", + "consistent-return": "error", + "curly": "error", + "default-case": "error", + "default-param-last": "error", + "dot-notation": "error", + "eqeqeq": [ + "error", + "always" + ], + "global-require": "error", + "guard-for-in": "error", + "no-alert": "error", + "no-buffer-constructor": "error", + "no-caller": "error", + "no-div-regex": "error", + "no-else-return": "error", + "no-empty-function": [ + "error", + { + "allow": [ + "constructors" + ] + } + ], + "no-eq-null": "error", + "no-eval": "error", + "no-extend-native": "error", + "no-extra-bind": "error", + "no-floating-decimal": "error", + "no-implicit-coercion": "error", + "no-implied-eval": "error", + "no-import-assign": "error", + "no-invalid-this": "error", + "no-iterator": "error", + "no-label-var": "error", + "no-labels": "error", + "no-lone-blocks": "error", + "no-loop-func": "error", + "no-multi-spaces": "error", + "no-multi-str": "error", + "no-new-func": "error", + "no-new-wrappers": "error", + "no-new": "error", + "no-octal-escape": "error", + "no-path-concat": "error", + "no-process-exit": "error", + "no-proto": "error", + "no-prototype-builtins": "off", + "no-restricted-modules": [ + "error", + "moment" + ], + "no-return-assign": "error", + "no-return-await": "error", + "no-script-url": "error", + "no-self-compare": "error", + "no-sequences": "error", + "no-shadow": "error", + "no-sync": [ + "error", + { + "allowAtRootLevel": true + } + ], + "no-template-curly-in-string": "error", + "no-throw-literal": "error", + "no-undef-init": "error", + "no-unmodified-loop-condition": "error", + "no-useless-call": "error", + "no-useless-concat": "error", + "no-void": "error", + "prefer-named-capture-group": "warn", + "prefer-regex-literals": "error", + "radix": "error", + "require-unicode-regexp": "error", + "strict": "error", + "wrap-iife": [ + "error", + "any" + ], + "yoda": "error", + "array-bracket-spacing": [ + "error", + "never" + ], + "array-element-newline": [ + "error", + "consistent" + ], + "block-spacing": "error", + "camelcase": "warn", + "capitalized-comments": [ + "error", + "always" + ], + "comma-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "comma-style": [ + "error", + "last" + ], + "computed-property-spacing": [ + "error", + "never" + ], + "eol-last": [ + "error", + "always" + ], + "func-name-matching": [ + "error", + "always" + ], + "func-names": [ + "error", + "as-needed" + ], + "func-style": [ + "error", + "declaration" + ], + "function-call-argument-newline": [ + "error", + "consistent" + ], + "jsx-quotes": [ + "error", + "prefer-double" + ], + "key-spacing": [ + "error", + { + "beforeColon": false + } + ], + "keyword-spacing": [ + "error", + { + "before": true, + "after": true + } + ], + "linebreak-style": [ + "error", + "unix" + ], + "lines-between-class-members": [ + "error", + "always" + ], + "max-depth": [ + "error", + 5 + ], + "max-nested-callbacks": [ + "error", + 10 + ], + "max-params": [ + "error", + 10 + ], + "max-statements-per-line": [ + "error", + { + "max": 2 + } + ], + "multiline-comment-style": [ + "error", + "starred-block" + ], + "new-parens": "error", + "newline-per-chained-call": [ + "error", + { + "ignoreChainWithDepth": 3 + } + ], + "no-array-constructor": "error", + "no-lonely-if": "error", + "no-multi-assign": "error", + "no-multiple-empty-lines": "error", + "no-negated-condition": "error", + "no-new-object": "error", + "no-tabs": "error", + "no-trailing-spaces": "error", + "no-unneeded-ternary": "error", + "no-whitespace-before-property": "error", + "object-curly-spacing": [ + "error", + "always" + ], + "one-var": [ + "error", + "never" + ], + "operator-assignment": [ + "error", + "always" + ], + "padded-blocks": [ + "error", + { + "classes": "never" + } + ], + "padding-line-between-statements": [ + "error", + { + "blankLine": "always", + "prev": [ + "const", + "let" + ], + "next": "*" + }, + { + "blankLine": "any", + "prev": [ + "const", + "let" + ], + "next": [ + "const", + "let", + "var" + ] + }, + { + "blankLine": "always", + "prev": "directive", + "next": "*" + }, + { + "blankLine": "any", + "prev": "directive", + "next": "directive" + }, + { + "blankLine": "always", + "prev": "break", + "next": "*" + }, + { + "blankLine": "always", + "prev": "block-like", + "next": "*" + } + ], + "prefer-object-spread": "error", + "quote-props": [ + "error", + "as-needed" + ], + "semi-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "semi-style": [ + "error", + "last" + ], + "sort-keys": "error", + "space-before-blocks": "error", + "space-in-parens": [ + "error", + "never" + ], + "space-infix-ops": [ + "error", + { + "int32Hint": false + } + ], + "space-unary-ops": "error", + "switch-colon-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "template-tag-spacing": [ + "error", + "never" + ], + "unicode-bom": [ + "error", + "never" + ], + "arrow-parens": [ + "error", + "as-needed" + ], + "arrow-spacing": [ + "error", + { + "before": true, + "after": true + } + ], + "generator-star-spacing": [ + "error", + { + "before": true, + "after": false + } + ], + "no-confusing-arrow": "error", + "no-duplicate-imports": "error", + "no-useless-computed-key": "error", + "no-useless-rename": "error", + "no-var": "error", + "object-shorthand": [ + "error", + "properties" + ], + "prefer-arrow-callback": "error", + "prefer-const": "error", + "prefer-destructuring": "error", + "prefer-numeric-literals": "error", + "prefer-rest-params": "error", + "prefer-spread": "error", + "prefer-template": "error", + "rest-spread-spacing": [ + "error", + "never" + ], + "sort-imports": [ + "error", + { + "ignoreCase": true, + "ignoreDeclarationSort": true + } + ], + "template-curly-spacing": [ + "error", + "never" + ], + "yield-star-spacing": [ + "error", + "before" + ] + } +} diff --git a/parser/package.json b/parser/package.json index 49e7bd2e3..7ff184f4f 100644 --- a/parser/package.json +++ b/parser/package.json @@ -5,6 +5,8 @@ "main": "src/index.ts", "scripts": { "test": "jest --passWithNoTests", + "eslint:fix": "eslint --fix '**/*.ts'", + "eslint:check": "eslint '**/*.ts'", "prettier:fix": "prettier --write '**/*.{t,j}s'", "prettier:check": "prettier --check '**/*.{t,j}s'", "build": "tsc" @@ -25,6 +27,11 @@ "devDependencies": { "@types/jest": "^26.0.14", "@types/node": "^14.11.9", + "@typescript-eslint/eslint-plugin": "^4.4.1", + "@typescript-eslint/parser": "^4.4.1", + "eslint": "^7.11.0", + "eslint-config-prettier": "^6.10.0", + "eslint-plugin-prettier": "^3.1.2", "jest": "^26.5.3", "prettier": "^2.1.2", "ts-jest": "^26.4.1", diff --git a/parser/spec/lexer.spec.ts b/parser/spec/lexer.spec.ts index 59d4b89be..11b4cbb75 100644 --- a/parser/spec/lexer.spec.ts +++ b/parser/spec/lexer.spec.ts @@ -24,6 +24,40 @@ import { TypeKeywordToken, } from "../src/token"; +function itLexes(source: string, expectedTokens: Token[]) { + test(`lexes ${JSON.stringify(source)} as [${expectedTokens.join(", ")}]`, () => { + const lexer = new Lexer(source); + + let tokens: Token[] = []; + + for (;;) { + const token = lexer.nextToken(); + + if (!token) { + break; + } + + tokens.push(token); + } + + tokens = tokens.map((token, i) => (expectedTokens[i] instanceof IdentifierToken ? token.maybeAsIdentifier() : token)); + + expect(tokens.join(", ")).toEqual(expectedTokens.join(", ")); + }); +} + +function itDoesntLex(source: string, message: string) { + test(`doesn't lex ${JSON.stringify(source)}`, () => { + const lexer = new Lexer(source); + + expect(() => { + while (lexer.nextToken()) { + // + } + }).toThrowError(message); + }); +} + describe(Lexer, () => { itLexes("", []); @@ -72,7 +106,7 @@ describe(Lexer, () => { itLexes(primitive, [new IdentifierToken(primitive)]); - itLexes(primitive + "a", [new IdentifierToken(primitive + "a")]); + itLexes(`${primitive}a`, [new IdentifierToken(`${primitive}a`)]); } itLexes("err", [new IdentifierToken("err")]); @@ -211,29 +245,3 @@ describe(Lexer, () => { itLexes("@\\\n cc ", [new AnnotationToken("cc")]); }); - -function itLexes(source: string, expectedTokens: Token[]) { - test(`lexes ${JSON.stringify(source)} as [${expectedTokens.join(", ")}]`, () => { - const lexer = new Lexer(source); - - let tokens: Token[] = []; - let token: Token | null; - while ((token = lexer.nextToken())) { - tokens.push(token); - } - - tokens = tokens.map((token, i) => (expectedTokens[i] instanceof IdentifierToken ? token.maybeAsIdentifier() : token)); - - expect(tokens.join(", ")).toEqual(expectedTokens.join(", ")); - }); -} - -function itDoesntLex(source: string, message: string) { - test(`doesn't lex ${JSON.stringify(source)}`, () => { - const lexer = new Lexer(source); - - expect(() => { - while (lexer.nextToken()) {} - }).toThrowError(message); - }); -} diff --git a/parser/spec/parser.spec.ts b/parser/spec/parser.spec.ts index 4a1c3ba42..1e7fe747c 100644 --- a/parser/spec/parser.spec.ts +++ b/parser/spec/parser.spec.ts @@ -2,6 +2,21 @@ import { AstJson, astToJson, jsonToAst } from "../src/json"; import { Lexer } from "../src/lexer"; import { Parser } from "../src/parser"; +function expectParses(source: string, json: AstJson, warnings: string[] = []) { + const parser = new Parser(new Lexer(source)); + const ast = parser.parse(); + + expect(ast.warnings).toEqual(warnings); + expect(astToJson(ast)).toEqual(json); + expect(astToJson(ast)).toEqual(astToJson(jsonToAst(astToJson(ast)))); +} + +function expectDoesntParse(source: string, message: string) { + const parser = new Parser(new Lexer(source)); + + expect(() => parser.parse()).toThrowError(message); +} + describe(Parser, () => { for (const p of Lexer.PRIMITIVES) { test(`handles primitive type '${p}'`, () => { @@ -12,6 +27,7 @@ describe(Parser, () => { } `, { + annotations: {}, errors: ["Fatal"], functionTable: {}, typeTable: { @@ -19,7 +35,6 @@ describe(Parser, () => { foo: p, }, }, - annotations: {}, }, ); }); @@ -32,6 +47,7 @@ describe(Parser, () => { fn getBaz(): ${p}[] `, { + annotations: {}, errors: ["Fatal"], functionTable: { [p === "bool" ? "isFoo" : "getFoo"]: { @@ -48,7 +64,6 @@ describe(Parser, () => { }, }, typeTable: {}, - annotations: {}, }, ["Keyword 'get' is deprecated at -:2:21. Use 'fn' instead.", "Keyword 'get' is deprecated at -:3:21. Use 'fn' instead."], ); @@ -64,6 +79,7 @@ describe(Parser, () => { } `, { + annotations: {}, errors: ["Fatal"], functionTable: {}, typeTable: { @@ -71,7 +87,6 @@ describe(Parser, () => { [kw]: "int", }, }, - annotations: {}, }, ); }); @@ -88,6 +103,7 @@ describe(Parser, () => { } `, { + annotations: {}, errors: ["Fatal"], functionTable: {}, typeTable: { @@ -98,7 +114,6 @@ describe(Parser, () => { ddddd: "uint[][][]??[]???[][]", }, }, - annotations: {}, }, ); }); @@ -116,6 +131,7 @@ describe(Parser, () => { type Other enum { aa bb } `, { + annotations: {}, errors: ["Fatal"], functionTable: {}, typeTable: { @@ -126,7 +142,6 @@ describe(Parser, () => { FooStatus: ["c", "a", "zzz"], Other: ["aa", "bb"], }, - annotations: {}, }, ); }); @@ -139,10 +154,10 @@ describe(Parser, () => { error FooBar `, { + annotations: {}, errors: ["Foo", "Bar", "FooBar", "Fatal"], functionTable: {}, typeTable: {}, - annotations: {}, }, ); }); @@ -161,6 +176,7 @@ describe(Parser, () => { fn getBaz(): Baz `, { + annotations: {}, errors: ["Foo", "Bar", "Fatal"], functionTable: { getBaz: { @@ -174,7 +190,6 @@ describe(Parser, () => { b: "int", }, }, - annotations: {}, }, ); }); @@ -241,6 +256,7 @@ describe(Parser, () => { } `, { + annotations: {}, errors: ["Fatal"], functionTable: {}, typeTable: { @@ -252,7 +268,6 @@ describe(Parser, () => { bb: "int", }, }, - annotations: {}, }, ); }); @@ -267,12 +282,13 @@ describe(Parser, () => { function doIt(foo: int, bar: Bar): string `, { + annotations: {}, errors: ["Fatal"], functionTable: { doIt: { args: { - foo: "int", bar: "Bar", + foo: "int", }, ret: "string", }, @@ -282,7 +298,6 @@ describe(Parser, () => { aa: "string", }, }, - annotations: {}, }, ["Keyword 'function' is deprecated at -:6:17. Use 'fn' instead."], ); @@ -298,24 +313,24 @@ describe(Parser, () => { fn doIt(foo: int, bar: float): string `, { + annotations: { + "fn.doIt": [ + { type: "description", value: "does it" }, + { type: "description", value: "and does another thing too" }, + ], + "fn.doIt.bar": [{ type: "description", value: "Represents the number of things" }], + }, errors: ["Fatal"], functionTable: { doIt: { args: { - foo: "int", bar: "float", + foo: "int", }, ret: "string", }, }, typeTable: {}, - annotations: { - "fn.doIt": [ - { type: "description", value: "does it" }, - { type: "description", value: "and does another thing too" }, - ], - "fn.doIt.bar": [{ type: "description", value: "Represents the number of things" }], - }, }, ); @@ -330,6 +345,12 @@ describe(Parser, () => { fn doIt2(): int `, { + annotations: { + "fn.doIt": [ + { type: "throws", value: "NotFound" }, + { type: "throws", value: "InvalidArgument" }, + ], + }, errors: ["NotFound", "InvalidArgument", "Fatal"], functionTable: { doIt: { @@ -342,12 +363,6 @@ describe(Parser, () => { }, }, typeTable: {}, - annotations: { - "fn.doIt": [ - { type: "throws", value: "NotFound" }, - { type: "throws", value: "InvalidArgument" }, - ], - }, }, ); }); @@ -362,6 +377,9 @@ describe(Parser, () => { } `, { + annotations: { + "type.Foo.x": [{ type: "description", value: "foobar" }], + }, errors: ["Fatal"], functionTable: {}, typeTable: { @@ -370,9 +388,6 @@ describe(Parser, () => { y: "string", }, }, - annotations: { - "type.Foo.x": [{ type: "description", value: "foobar" }], - }, }, ); }); @@ -390,51 +405,30 @@ describe(Parser, () => { fn userCount(since: date?, until: date?): uint `, { - errors: ["Fatal"], - functionTable: { - name: { - args: { - id: "string", - }, - ret: "string", - }, - foo: { - args: {}, - ret: "string", - }, - userCount: { - args: { - since: "date?", - until: "date?", - }, - ret: "uint", - }, - }, - typeTable: {}, annotations: { - "fn.name": [ + "fn.foo": [ { type: "rest", value: { + bodyVariable: null, + headers: [], method: "GET", - path: "/users/{id}/name", - pathVariables: ["id"], + path: "/foo", + pathVariables: [], queryVariables: [], - headers: [], - bodyVariable: null, }, }, ], - "fn.foo": [ + "fn.name": [ { type: "rest", value: { + bodyVariable: null, + headers: [], method: "GET", - path: "/foo", - pathVariables: [], + path: "/users/{id}/name", + pathVariables: ["id"], queryVariables: [], - headers: [], - bodyVariable: null, }, }, ], @@ -442,16 +436,37 @@ describe(Parser, () => { { type: "rest", value: { + bodyVariable: null, + headers: [], method: "GET", path: "/users/count", pathVariables: [], queryVariables: ["since", "until"], - headers: [], - bodyVariable: null, }, }, ], }, + errors: ["Fatal"], + functionTable: { + foo: { + args: {}, + ret: "string", + }, + name: { + args: { + id: "string", + }, + ret: "string", + }, + userCount: { + args: { + since: "date?", + until: "date?", + }, + ret: "uint", + }, + }, + typeTable: {}, }, ); @@ -461,34 +476,34 @@ describe(Parser, () => { fn getMessages(token: base64, chatId: uuid, since: datetime?, until: datetime?): string[] `, { - errors: ["Fatal"], - functionTable: { - getMessages: { - args: { - token: "base64", - chatId: "uuid", - since: "datetime?", - until: "datetime?", - }, - ret: "string[]", - }, - }, - typeTable: {}, annotations: { "fn.getMessages": [ { type: "rest", value: { + bodyVariable: null, + headers: [["x-token", "token"]], method: "GET", path: "/chats/{chatId}/messages", pathVariables: ["chatId"], queryVariables: ["since", "until"], - headers: [["x-token", "token"]], - bodyVariable: null, }, }, ], }, + errors: ["Fatal"], + functionTable: { + getMessages: { + args: { + chatId: "uuid", + since: "datetime?", + token: "base64", + until: "datetime?", + }, + ret: "string[]", + }, + }, + typeTable: {}, }, ); @@ -498,37 +513,37 @@ describe(Parser, () => { fn getPosts(userAgent: string, lang: string, token: base64): uuid `, { - errors: ["Fatal"], - functionTable: { - getPosts: { - args: { - userAgent: "string", - lang: "string", - token: "base64", - }, - ret: "uuid", - }, - }, - typeTable: {}, annotations: { "fn.getPosts": [ { type: "rest", value: { - method: "GET", - path: "/posts", - pathVariables: [], - queryVariables: [], + bodyVariable: null, headers: [ ["user-agent", "userAgent"], ["accept-language", "lang"], ["x-token", "token"], ], - bodyVariable: null, + method: "GET", + path: "/posts", + pathVariables: [], + queryVariables: [], }, }, ], }, + errors: ["Fatal"], + functionTable: { + getPosts: { + args: { + lang: "string", + token: "base64", + userAgent: "string", + }, + ret: "uuid", + }, + }, + typeTable: {}, }, ); @@ -546,6 +561,21 @@ describe(Parser, () => { fn createNewUser(user: NewUser): User `, { + annotations: { + "fn.createNewUser": [ + { + type: "rest", + value: { + bodyVariable: "user", + headers: [], + method: "POST", + path: "/users", + pathVariables: [], + queryVariables: [], + }, + }, + ], + }, errors: ["Fatal"], functionTable: { createNewUser: { @@ -563,21 +593,6 @@ describe(Parser, () => { id: "uuid", }, }, - annotations: { - "fn.createNewUser": [ - { - type: "rest", - value: { - method: "POST", - path: "/users", - pathVariables: [], - queryVariables: [], - headers: [], - bodyVariable: "user", - }, - }, - ], - }, }, ); @@ -591,35 +606,35 @@ describe(Parser, () => { @rest GET /things/{kind} fn countThings(kind: Kind): uint - `, + `, { - errors: ["Fatal"], - functionTable: { - countThings: { - args: { - kind: "Kind", - }, - ret: "uint", - }, - }, - typeTable: { - Kind: ["first", "second", "third"], - }, annotations: { "fn.countThings": [ { type: "rest", value: { + bodyVariable: null, + headers: [], method: "GET", path: "/things/{kind}", pathVariables: ["kind"], queryVariables: [], - headers: [], - bodyVariable: null, }, }, ], }, + errors: ["Fatal"], + functionTable: { + countThings: { + args: { + kind: "Kind", + }, + ret: "uint", + }, + }, + typeTable: { + Kind: ["first", "second", "third"], + }, }, ); @@ -696,17 +711,3 @@ describe(Parser, () => { ); }); }); - -function expectParses(source: string, json: AstJson, warnings: string[] = []) { - const parser = new Parser(new Lexer(source)); - const ast = parser.parse(); - - expect(ast.warnings).toEqual(warnings); - expect(astToJson(ast)).toEqual(json); - expect(astToJson(ast)).toEqual(astToJson(jsonToAst(astToJson(ast)))); -} - -function expectDoesntParse(source: string, message: string) { - const parser = new Parser(new Lexer(source)); - expect(() => parser.parse()).toThrowError(message); -} diff --git a/parser/src/ast.ts b/parser/src/ast.ts index e506daabe..191d07dae 100644 --- a/parser/src/ast.ts +++ b/parser/src/ast.ts @@ -2,7 +2,9 @@ import { Token, TokenLocation } from "./token"; export class AstRoot { structTypes: StructType[] = []; + enumTypes: EnumType[] = []; + warnings: string[] = []; constructor(public typeDefinitions: TypeDefinition[] = [], public operations: Operation[] = [], public errors: string[] = []) {} @@ -29,8 +31,9 @@ export abstract class AstNode { export abstract class Type extends AstNode { abstract get name(): string; - toJSON() { + toJSON(): any { const json: any = { ...this }; + delete json.name; return json; } @@ -105,8 +108,9 @@ export class OptionalType extends Type { constructor(public base: Type) { super(); } - get name() { - return this.base.name + "?"; + + get name(): string { + return `${this.base.name}?`; } } @@ -114,13 +118,15 @@ export class ArrayType extends Type { constructor(public base: Type) { super(); } - get name() { - return this.base.name + "[]"; + + get name(): string { + return `${this.base.name}[]`; } } export class EnumValue extends AstNode { annotations: Annotation[] = []; + constructor(public value: string) { super(); } @@ -128,6 +134,7 @@ export class EnumValue extends AstNode { export class EnumType extends Type { name!: string; + constructor(public values: EnumValue[]) { super(); } @@ -135,6 +142,7 @@ export class EnumType extends Type { export class StructType extends Type { name!: string; + constructor(public fields: Field[], public spreads: TypeReference[]) { super(); } @@ -142,6 +150,7 @@ export class StructType extends Type { export class TypeDefinition extends AstNode { annotations: Annotation[] = []; + constructor(public name: string, public type: Type) { super(); } @@ -149,6 +158,7 @@ export class TypeDefinition extends AstNode { export class TypeReference extends Type { type!: Type; + constructor(public name: string) { super(); } @@ -156,6 +166,7 @@ export class TypeReference extends Type { export class Field extends AstNode { annotations: Annotation[] = []; + constructor(public name: string, public type: Type, public secret = false) { super(); } @@ -163,18 +174,19 @@ export class Field extends AstNode { export abstract class Operation extends AstNode { annotations: Annotation[] = []; + constructor(public name: string, public args: Field[], public returnType: Type) { super(); } - get prettyName() { + get prettyName(): string { return this.name; } } export class GetOperation extends Operation { - get prettyName() { - return this.returnType instanceof BoolPrimitiveType ? this.name : "get" + this.name[0].toUpperCase() + this.name.slice(1); + get prettyName(): string { + return this.returnType instanceof BoolPrimitiveType ? this.name : `get${this.name[0].toUpperCase()}${this.name.slice(1)}`; } } diff --git a/parser/src/compatibility/index.ts b/parser/src/compatibility/index.ts index b10e70572..19ed8dad9 100644 --- a/parser/src/compatibility/index.ts +++ b/parser/src/compatibility/index.ts @@ -26,8 +26,10 @@ import { XmlPrimitiveType, } from "../ast"; -// 1 -> Old version -// 2 -> New version +/* + * 1 -> Old version + * 2 -> New version + */ function checkClientToServer(path: string, issues: string[], t1: Type, t2: Type) { if (t1 instanceof TypeReference) { @@ -63,7 +65,8 @@ function checkClientToServer(path: string, issues: string[], t1: Type, t2: Type) if (t1 instanceof StructType && t2 instanceof StructType) { for (const field2 of t2.fields) { - const field1 = t1.fields.find(field1 => field1.name === field2.name); + const field1 = t1.fields.find(x => x.name === field2.name); + if (!field1) { if (field2.type instanceof OptionalType) { continue; @@ -72,8 +75,10 @@ function checkClientToServer(path: string, issues: string[], t1: Type, t2: Type) continue; } } + checkClientToServer(`${path}.${field1.name}`, issues, field1.type, field2.type); } + return; } @@ -110,6 +115,7 @@ function checkClientToServer(path: string, issues: string[], t1: Type, t2: Type) issues.push(`The enum at ${path} used to accept the value "${value.value}" that doesn't exist now. Clients that send it will fail.`); } } + return; } @@ -154,7 +160,8 @@ function checkServerToClient(path: string, issues: string[], t1: Type, t2: Type) if (t1 instanceof StructType && t2 instanceof StructType) { for (const field1 of t1.fields) { - const field2 = t2.fields.find(field2 => field2.name === field1.name); + const field2 = t2.fields.find(x => x.name === field1.name); + if (!field2) { if (field1.type instanceof OptionalType) { continue; @@ -163,8 +170,10 @@ function checkServerToClient(path: string, issues: string[], t1: Type, t2: Type) continue; } } + checkServerToClient(`${path}.${field1.name}`, issues, field1.type, field2.type); } + return; } @@ -201,6 +210,7 @@ function checkServerToClient(path: string, issues: string[], t1: Type, t2: Type) issues.push(`The enum at ${path} now has the value "${value.value}" that didn't exist before. Client will crash if it receives it`); } } + return; } @@ -211,18 +221,21 @@ function checkServerToClient(path: string, issues: string[], t1: Type, t2: Type) issues.push(`${path} was ${t1.name} and now it is ${t2.name}. They are not compatible.`); } -export function compatibilityIssues(ast1: AstRoot, ast2: AstRoot) { +export function compatibilityIssues(ast1: AstRoot, ast2: AstRoot): string[] { const issues: string[] = []; for (const op1 of ast1.operations) { - const op2 = ast2.operations.find(op2 => op2.prettyName === op1.prettyName); + const op2 = ast2.operations.find(x => x.prettyName === op1.prettyName); + if (!op2) { issues.push(`function ${op1.prettyName} used to exist, but it's now missing. Add it back.`); continue; } + checkServerToClient(`${op1.prettyName}.ret`, issues, op1.returnType, op2.returnType); for (const arg2 of op2.args) { - const arg1 = op1.args.find(arg1 => arg1.name === arg2.name); + const arg1 = op1.args.find(x => x.name === arg2.name); + if (!arg1) { if (arg2.type instanceof OptionalType) { continue; @@ -231,6 +244,7 @@ export function compatibilityIssues(ast1: AstRoot, ast2: AstRoot) { continue; } } + checkClientToServer(`${op1.prettyName}.args.${arg1.name}`, issues, arg1.type, arg2.type); } } diff --git a/parser/src/json.ts b/parser/src/json.ts index 5b8656dc1..b48ec3291 100644 --- a/parser/src/json.ts +++ b/parser/src/json.ts @@ -50,14 +50,19 @@ export function astToJson(ast: AstRoot): AstJson { const typeTable: TypeTable = {}; for (const { name, fields } of ast.structTypes) { - const obj: any = (typeTable[name] = {}); + typeTable[name] = {}; + const obj: any = typeTable[name]; + for (const field of fields) { obj[field.name] = field.type.name; for (const ann of field.annotations) { if (ann instanceof DescriptionAnnotation) { const target = `type.${name}.${field.name}`; - const list = (annotations[target] = annotations[target] || []); + + annotations[target] = []; + const list = annotations[target]; + list.push({ type: "description", value: ann.text }); } } @@ -72,56 +77,66 @@ export function astToJson(ast: AstRoot): AstJson { for (const op of ast.operations) { const args: any = {}; + for (const arg of op.args) { args[arg.name] = arg.type.name; for (const ann of arg.annotations) { if (ann instanceof DescriptionAnnotation) { const target = `fn.${op.prettyName}.${arg.name}`; - const list = (annotations[target] = annotations[target] || []); + + annotations[target] = []; + const list = annotations[target]; + list.push({ type: "description", value: ann.text }); } } } + functionTable[op.prettyName] = { args, ret: op.returnType.name, }; for (const ann of op.annotations) { const target = `fn.${op.prettyName}`; - const list = (annotations[target] = annotations[target] || []); + + annotations[target] = []; + const list = annotations[target]; + if (ann instanceof DescriptionAnnotation) { list.push({ type: "description", value: ann.text }); } + if (ann instanceof ThrowsAnnotation) { list.push({ type: "throws", value: ann.error }); } + if (ann instanceof RestAnnotation) { list.push({ type: "rest", value: { + bodyVariable: ann.bodyVariable, + headers: [...ann.headers.entries()], method: ann.method, path: ann.path, pathVariables: ann.pathVariables, queryVariables: ann.queryVariables, - headers: [...ann.headers.entries()], - bodyVariable: ann.bodyVariable, }, }); } } } - const errors = ast.errors; + const { errors } = ast; return { - typeTable, - functionTable, - errors, annotations, + errors, + functionTable, + typeTable, }; } -export function jsonToAst(json: AstJson) { +export function jsonToAst(json: AstJson): AstRoot { const operations: Operation[] = []; const typeDefinition: TypeDefinition[] = []; const errors: string[] = json.errors || []; @@ -129,59 +144,69 @@ export function jsonToAst(json: AstJson) { function processType(description: TypeDescription, typeName?: string): Type { if (typeof description === "string") { const primitiveClass = primitiveToAstClass.get(description); + if (primitiveClass) { return new primitiveClass(); } else if (description.endsWith("?")) { return new OptionalType(processType(description.slice(0, description.length - 1))); } else if (description.endsWith("[]")) { return new ArrayType(processType(description.slice(0, description.length - 2))); - } else { - return new TypeReference(description); } + + return new TypeReference(description); } else if (Array.isArray(description)) { return new EnumType(description.map(v => new EnumValue(v))); - } else { - const fields: Field[] = []; - for (const fieldName of Object.keys(description)) { - const field = new Field(fieldName, processType(description[fieldName])); - if (typeName) { - const target = `type.${typeName}.${fieldName}`; - for (const annotationJson of json.annotations[target] || []) { - if (annotationJson.type === "description") { - field.annotations.push(new DescriptionAnnotation(annotationJson.value)); - } + } + + const fields: Field[] = []; + + for (const fieldName of Object.keys(description)) { + const field = new Field(fieldName, processType(description[fieldName])); + + if (typeName) { + const target = `type.${typeName}.${fieldName}`; + + for (const annotationJson of json.annotations[target] || []) { + if (annotationJson.type === "description") { + field.annotations.push(new DescriptionAnnotation(annotationJson.value)); } } - fields.push(field); } - return new StructType(fields, []); + + fields.push(field); } + + return new StructType(fields, []); } - for (const typeName in json.typeTable) { - const type = processType(json.typeTable[typeName], typeName); + for (const [typeName, description] of Object.entries(json.typeTable)) { + const type = processType(description, typeName); + if (typeName === "ErrorType" && type instanceof EnumType) { errors.push(...type.values.map(v => v.value)); continue; } + typeDefinition.push(new TypeDefinition(typeName, type)); } - for (const functionName in json.functionTable) { - const func = json.functionTable[functionName]; + for (const [functionName, func] of Object.entries(json.functionTable)) { const args = Object.keys(func.args).map(argName => { const field = new Field(argName, processType(func.args[argName])); const target = `fn.${functionName}.${argName}`; + for (const annotationJson of json.annotations[target] || []) { if (annotationJson.type === "description") { field.annotations.push(new DescriptionAnnotation(annotationJson.value)); } } + return field; }); const op = new FunctionOperation(functionName, args, processType(func.ret)); const target = `fn.${functionName}`; + for (const annotationJson of json.annotations[target] || []) { if (annotationJson.type === "description") { op.annotations.push(new DescriptionAnnotation(annotationJson.value)); @@ -189,6 +214,7 @@ export function jsonToAst(json: AstJson) { op.annotations.push(new ThrowsAnnotation(annotationJson.value)); } else if (annotationJson.type === "rest") { const { method, path, pathVariables, queryVariables, headers, bodyVariable } = annotationJson.value; + op.annotations.push(new RestAnnotation(method, path, pathVariables, queryVariables, new Map(headers), bodyVariable)); } } @@ -197,6 +223,7 @@ export function jsonToAst(json: AstJson) { } const ast = new AstRoot(typeDefinition, operations, [...new Set(errors)]); + analyse(ast); return ast; } diff --git a/parser/src/lexer.ts b/parser/src/lexer.ts index 3b8a272d4..dacfb4052 100644 --- a/parser/src/lexer.ts +++ b/parser/src/lexer.ts @@ -55,10 +55,15 @@ export class Lexer { public static readonly KEYWORDS = new Set([...Lexer.PRIMITIVES, "error", "enum", "type", "import", "get", "function", "fn", "true", "false"]); private startPos = 0; + private startLine = 1; + private startColumn = 1; + private pos = 0; + private line = 1; + private column = 1; constructor(private readonly source: string, public readonly filename: string = "-") {} @@ -95,7 +100,7 @@ export class Lexer { case "/": switch (this.nextChar()) { case "/": - while (true) { + for (;;) { switch (this.nextChar()) { case "\0": return null; @@ -104,21 +109,30 @@ export class Lexer { this.column = 1; this.line++; return this.nextToken(); + default: + break; } } + case "*": - outerWhile: while (true) { + // eslint-disable-next-line no-labels + outerWhile: for (;;) { switch (this.nextChar()) { case "\0": + // eslint-disable-next-line no-labels break outerWhile; case "\n": this.column = 0; this.line++; break; case "*": - while (this.nextChar() === "*") {} + while (this.nextChar() === "*") { + // + } + switch (this.currentChar()) { case "\0": + // eslint-disable-next-line no-labels break outerWhile; case "\n": this.column = 0; @@ -127,10 +141,22 @@ export class Lexer { case "/": this.nextChar(); return this.nextToken(); + default: + break; } + + break; + + default: + break; } } + + break; + default: + break; } + break; case "{": this.nextChar(); @@ -175,7 +201,11 @@ export class Lexer { token = new ArraySymbolToken(); break; } + + default: + break; } + break; case ".": switch (this.nextChar()) { @@ -186,30 +216,50 @@ export class Lexer { token = new SpreadSymbolToken(); break; } + + default: + break; } + + break; } + + default: + break; } + break; - case "@": + case "@": { let body = "\\"; let pos = this.startPos + 1; + while (body[body.length - 1] === "\\") { body = body.slice(0, body.length - 1).trim(); - while (!["\0", "\n"].includes(this.nextChar())) {} - body = (body + " " + this.source.substring(pos, this.pos).trim()).trim(); + while (!["\0", "\n"].includes(this.nextChar())) { + // + } + + body = `${body} ${this.source.substring(pos, this.pos).trim()}`.trim(); pos = this.pos + 1; } + token = new AnnotationToken(body.trim()); break; + } + case '"': { const chars = []; - outerLoop: while (true) { + + // eslint-disable-next-line no-labels + outerLoop: for (;;) { switch (this.nextChar()) { case "\0": + // eslint-disable-next-line no-labels break outerLoop; case "\\": switch (this.nextChar()) { case "\0": + // eslint-disable-next-line no-labels break outerLoop; case "n": chars.push("\n"); @@ -221,20 +271,28 @@ export class Lexer { chars.push(this.currentChar()); break; } + break; case '"': this.nextChar(); token = new StringLiteralToken(chars.join("")); + // eslint-disable-next-line no-labels break outerLoop; default: chars.push(this.currentChar()); break; } } + + break; } + default: { - if (this.currentChar().match(/[a-zA-Z_]/)) { - while (this.nextChar().match(/[a-zA-Z0-9_]/)) {} + if (this.currentChar().match(/[a-zA-Z_]/u)) { + while (this.nextChar().match(/[a-zA-Z0-9_]/u)) { + // + } + const ident = this.source.substring(this.startPos, this.pos); switch (ident) { @@ -277,12 +335,12 @@ export class Lexer { token.location.line = this.startLine; token.location.column = this.startColumn; return token; + } + + if (this.currentChar() === "\0") { + throw new LexerError(`Unexpected end of file at ${this.filename}`); } else { - if (this.currentChar() === "\0") { - throw new LexerError(`Unexpected end of file at ${this.filename}`); - } else { - throw new LexerError(`Unexpected character ${JSON.stringify(this.currentChar())} at ${this.filename}:${this.line}:${this.column}`); - } + throw new LexerError(`Unexpected character ${JSON.stringify(this.currentChar())} at ${this.filename}:${this.line}:${this.column}`); } } } diff --git a/parser/src/parser.ts b/parser/src/parser.ts index 2a0e5d965..3cdbb6549 100644 --- a/parser/src/parser.ts +++ b/parser/src/parser.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-loop-func */ import { readFileSync } from "fs"; import { dirname, resolve } from "path"; import { @@ -72,14 +73,18 @@ interface MultiExpectMatcher { export class Parser { private readonly lexers: Lexer[]; + private token: Token | null = null; + private annotations: Annotation[] = []; + private warnings: string[] = []; constructor(source: Lexer | string) { if (!(source instanceof Lexer)) { source = new Lexer(readFileSync(source).toString(), source); } + this.lexers = [source]; this.nextToken(); } @@ -89,9 +94,9 @@ export class Parser { this.token = this.lexers[this.lexers.length - 1].nextToken(); if (this.token) { return; - } else { - this.lexers.pop(); } + + this.lexers.pop(); } } @@ -112,10 +117,11 @@ export class Parser { if (tokenName in matcher) { return (matcher as any)[tokenName](this.token); - } else if ("IdentifierToken" in matcher) { + } else if (matcher.IdentifierToken) { const tokenAsIdent = this.token.maybeAsIdentifier(); + if (tokenAsIdent instanceof IdentifierToken) { - return matcher["IdentifierToken"]!(tokenAsIdent); + return matcher.IdentifierToken(tokenAsIdent); } } @@ -127,7 +133,9 @@ export class Parser { } private expect(x: typeof IdentifierToken): IdentifierToken; + private expect(x: typeof StringLiteralToken): StringLiteralToken; + private expect(type: any): Token { if (this.token === null) { throw new ParserError(`Expected ${(type as any).name.replace("Token", "")}, but found end of file`); @@ -136,50 +144,55 @@ export class Parser { } else { if (type === IdentifierToken) { const tokenAsIdent = this.token.maybeAsIdentifier(); + if (tokenAsIdent instanceof IdentifierToken) { return tokenAsIdent; } } + throw new ParserError(`Expected ${(type as any).name.replace("Token", "")} at ${this.token.location}, but found ${this.token}`); } } - parse() { + parse(): AstRoot { const operations: Operation[] = []; const typeDefinition: TypeDefinition[] = []; const errors: string[] = []; + this.warnings = []; while (this.token) { this.acceptAnnotations(); this.multiExpect({ - ImportKeywordToken: () => { + ErrorKeywordToken: () => { this.checkCannotHaveAnnotationsHere(); this.nextToken(); - const path = this.expect(StringLiteralToken).value; - const resolvedPath = resolve(dirname(this.currentFileName!), path + ".sdkgen"); - this.lexers.push(new Lexer(readFileSync(resolvedPath).toString(), resolvedPath)); + errors.push(this.expect(IdentifierToken).value); this.nextToken(); }, - TypeKeywordToken: () => { - typeDefinition.push(this.parseTypeDefinition()); - }, - GetKeywordToken: () => { + FunctionKeywordToken: () => { operations.push(this.parseOperation()); }, - FunctionKeywordToken: () => { + GetKeywordToken: () => { operations.push(this.parseOperation()); }, - ErrorKeywordToken: () => { + ImportKeywordToken: () => { this.checkCannotHaveAnnotationsHere(); this.nextToken(); - errors.push(this.expect(IdentifierToken).value); + const pathToken = this.expect(StringLiteralToken); + const resolvedPath = resolve(dirname(pathToken.location.filename), `${pathToken.value}.sdkgen`); + + this.lexers.push(new Lexer(readFileSync(resolvedPath).toString(), resolvedPath)); this.nextToken(); }, + TypeKeywordToken: () => { + typeDefinition.push(this.parseTypeDefinition()); + }, }); } const ast = new AstRoot(typeDefinition, operations, errors); + ast.warnings = this.warnings; analyse(ast); return ast; @@ -189,6 +202,7 @@ export class Parser { while (this.token instanceof AnnotationToken) { const words = this.token.value.split(" "); const body = this.token.value.slice(words[0].length).trim(); + switch (words[0]) { case "description": this.annotations.push(new DescriptionAnnotation(body).at(this.token)); @@ -207,10 +221,12 @@ export class Parser { } catch (error) { throw new ParserError(`${error.message} at ${this.token.location}`); } + break; default: throw new ParserError(`Unknown annotation '${words[0]}' at ${this.token.location}`); } + this.nextToken(); } } @@ -223,34 +239,40 @@ export class Parser { private parseTypeDefinition(): TypeDefinition { const typeToken = this.expect(TypeKeywordToken); + this.nextToken(); - const name = this.expect(IdentifierToken).value; - if (!name[0].match(/[A-Z]/)) { - throw new ParserError( - `The custom type name must start with an uppercase letter, but found '${JSON.stringify(name)}' at ${this.token!.location}`, - ); + const nameToken = this.expect(IdentifierToken); + const name = nameToken.value; + + if (!name[0].match(/[A-Z]/u)) { + throw new ParserError(`The custom type name must start with an uppercase letter, but found '${JSON.stringify(name)}' at ${nameToken.location}`); } + this.nextToken(); - const annotations = this.annotations; + const { annotations } = this; + this.annotations = []; const type = this.parseType(); const definitions = new TypeDefinition(name, type).at(typeToken); + definitions.annotations = annotations; return definitions; } private parseOperation(): Operation { - let annotations = this.annotations; + let { annotations } = this; + this.annotations = []; const openingToken: GetKeywordToken | FunctionKeywordToken = this.multiExpect({ - GetKeywordToken: token => token, FunctionKeywordToken: token => token, + GetKeywordToken: token => token, }); + this.nextToken(); if (["get", "function"].includes(openingToken.maybeAsIdentifier().value)) { @@ -258,6 +280,7 @@ export class Parser { } const name = this.expect(IdentifierToken).value; + this.nextToken(); this.expect(ParensOpenSymbolToken); @@ -268,9 +291,11 @@ export class Parser { while (this.token && this.token.maybeAsIdentifier() instanceof IdentifierToken) { const field = this.parseField(); + if (argNames.has(field.name)) { throw new ParserError(`Cannot redeclare argument '${field.name}'`); } + argNames.add(field.name); args.push(field); @@ -283,19 +308,24 @@ export class Parser { for (const annotation of annotations) { if (annotation instanceof ArgDescriptionAnnotation) { - const arg = args.find(arg => arg.name === annotation.argName); + const arg = args.find(x => x.name === annotation.argName); + if (!arg) { throw new ParserError(`Argument '${annotation.argName}' not found, at ${annotation.location}`); } + arg.annotations.push(new DescriptionAnnotation(annotation.text).atLocation(annotation.location)); } } + annotations = annotations.filter(ann => !(ann instanceof ArgDescriptionAnnotation)); const parensCloseToken = this.expect(ParensCloseSymbolToken); + this.nextToken(); let returnType = new VoidPrimitiveType().at(parensCloseToken); + if (this.token instanceof ColonSymbolToken) { this.nextToken(); returnType = this.parseType(); @@ -311,6 +341,7 @@ export class Parser { private parseEnum(): EnumType { this.checkCannotHaveAnnotationsHere(); const enumToken = this.expect(EnumKeywordToken); + this.nextToken(); this.expect(CurlyOpenSymbolToken); @@ -319,21 +350,23 @@ export class Parser { const enumType = new EnumType([]).at(enumToken); let finished = false; + while (!finished) { this.acceptAnnotations(); this.multiExpect({ + CurlyCloseSymbolToken: () => { + this.checkCannotHaveAnnotationsHere(); + this.nextToken(); + finished = true; + }, IdentifierToken: token => { const enumValue = new EnumValue(token.value).at(token); + enumValue.annotations = this.annotations; this.annotations = []; enumType.values.push(enumValue); this.nextToken(); }, - CurlyCloseSymbolToken: () => { - this.checkCannotHaveAnnotationsHere(); - this.nextToken(); - finished = true; - }, }); } @@ -342,16 +375,19 @@ export class Parser { private parseField(): Field { const nameToken = this.expect(IdentifierToken); + this.nextToken(); this.expect(ColonSymbolToken); this.nextToken(); - const annotations = this.annotations; + const { annotations } = this; + this.annotations = []; const type = this.parseType(); const field = new Field(nameToken.value, type).at(nameToken); + field.annotations = annotations; while (this.token instanceof ExclamationMarkSymbolToken) { @@ -363,6 +399,7 @@ export class Parser { default: throw new ParserError(`Unknown field mark !${this.token.value} at ${this.token.location}`); } + this.nextToken(); } @@ -371,6 +408,7 @@ export class Parser { private parseStruct(): StructType { const openingToken = this.expect(CurlyOpenSymbolToken); + this.nextToken(); const fields: Field[] = []; @@ -378,14 +416,22 @@ export class Parser { const fieldNames = new Set(); let finished = false; + while (!finished) { this.acceptAnnotations(); this.multiExpect({ + CurlyCloseSymbolToken: () => { + this.checkCannotHaveAnnotationsHere(); + this.nextToken(); + finished = true; + }, IdentifierToken: () => { const field = this.parseField(); + if (fieldNames.has(field.name)) { throw new ParserError(`Cannot redeclare field '${field.name}'`); } + fieldNames.add(field.name); fields.push(field); }, @@ -393,17 +439,14 @@ export class Parser { this.checkCannotHaveAnnotationsHere(); this.nextToken(); const identToken = this.expect(IdentifierToken); + this.nextToken(); - if (!identToken.value[0].match(/[A-Z]/)) { + if (!identToken.value[0].match(/[A-Z]/u)) { throw new ParserError(`Expected a type but found '${JSON.stringify(identToken.value)}' at ${identToken.location}`); } + spreads.push(new TypeReference(identToken.value).at(identToken)); }, - CurlyCloseSymbolToken: () => { - this.checkCannotHaveAnnotationsHere(); - this.nextToken(); - finished = true; - }, }); } @@ -417,16 +460,21 @@ export class Parser { EnumKeywordToken: () => this.parseEnum(), IdentifierToken: token => { this.nextToken(); - if (!token.value[0].match(/[A-Z]/)) { + if (!token.value[0].match(/[A-Z]/u)) { throw new ParserError(`Expected a type but found '${JSON.stringify(token.value)}' at ${token.location}`); } + return new TypeReference(token.value).at(token); }, PrimitiveTypeToken: token => { this.nextToken(); const primitiveClass = primitiveToAstClass.get(token.value); - if (primitiveClass) return new primitiveClass().at(token); - else throw new ParserError(`BUG! Should handle primitive ${token.value}`); + + if (primitiveClass) { + return new primitiveClass().at(token); + } + + throw new ParserError(`BUG! Should handle primitive ${token.value}`); }, }); diff --git a/parser/src/restparser.ts b/parser/src/restparser.ts index d885f1b4b..25a55da25 100644 --- a/parser/src/restparser.ts +++ b/parser/src/restparser.ts @@ -1,7 +1,48 @@ import { parse as pathParse } from "path"; import { RestAnnotation } from "./ast"; -export function parseRestAnnotation(text: string) { +function scanHeaders(text: string) { + const headerRegex = /\[header (?
[\w-]+): \{(?\w+)\}\]/gu; + const headers = new Map(); + + let match: RegExpExecArray | null; + + while ((match = headerRegex.exec(text)) !== null) { + if (match.groups?.header && match.groups?.name) { + headers.set(match.groups.header, match.groups.name); + } + } + + return headers; +} + +function scanBody(text: string) { + const bodyRegex = /\[body \{(?\w+)\}\]/u; + const match = text.match(bodyRegex); + + if (match && match.groups?.name) { + return match.groups.name; + } + + return null; +} + +function scanVariables(text: string) { + const variableRegex = /\{(?\w+)\}/gu; + const variables: string[] = []; + + let match: RegExpExecArray | null; + + while ((match = variableRegex.exec(text)) !== null) { + if (match.groups?.name) { + variables.push(match.groups.name); + } + } + + return variables; +} + +export function parseRestAnnotation(text: string): RestAnnotation { const fragments = text.split(" "); const method = fragments[0].toUpperCase(); @@ -10,6 +51,7 @@ export function parseRestAnnotation(text: string) { } const parsedPath = pathParse(fragments[1]); + if (parsedPath.root !== "/") { throw new Error(`Invalid path`); } @@ -19,13 +61,17 @@ export function parseRestAnnotation(text: string) { } let queryVariables: string[] = []; + if (parsedPath.base.includes("?")) { const [base, ...queryArray] = parsedPath.base.split("?"); + parsedPath.base = base; const query = queryArray.join("?"); - if (!query.match(/^\{\w+\}(&\{\w+\})*$/)) { + + if (!query.match(/^\{\w+\}(?:&\{\w+\})*$/u)) { throw new Error(`Invalid querystring on path`); } + queryVariables = scanVariables(query); } @@ -38,37 +84,3 @@ export function parseRestAnnotation(text: string) { return new RestAnnotation(method, path, pathVariables, queryVariables, headers, bodyVariable); } - -function scanHeaders(text: string) { - const headerRegex = /\[header ([\w-]+): \{(\w+)\}\]/gu; - const headers = new Map(); - - let match: RegExpExecArray | null; - while ((match = headerRegex.exec(text)) !== null) { - headers.set(match[1], match[2]); - } - - return headers; -} - -function scanBody(text: string) { - const bodyRegex = /\[body \{(\w+)\}\]/u; - const match = text.match(bodyRegex); - if (match) { - return match[1]; - } - - return null; -} - -function scanVariables(text: string) { - const variableRegex = /\{(\w+)\}/gu; - const variables: string[] = []; - - let match: RegExpExecArray | null; - while ((match = variableRegex.exec(text)) !== null) { - variables.push(match[1]); - } - - return variables; -} diff --git a/parser/src/semantic/01_check_multiple_declaration.ts b/parser/src/semantic/01_check_multiple_declaration.ts index c70bf1434..9d3ae3386 100644 --- a/parser/src/semantic/01_check_multiple_declaration.ts +++ b/parser/src/semantic/01_check_multiple_declaration.ts @@ -5,12 +5,14 @@ import { Visitor } from "./visitor"; export class CheckMultipleDeclarationVisitor extends Visitor { nameToType = new Map(); - visit(node: AstNode) { + visit(node: AstNode): void { if (node instanceof TypeDefinition) { const previousType = this.nameToType.get(node.name); + if (previousType && JSON.stringify(previousType) !== JSON.stringify(node.type)) { throw new SemanticError(`Type '${node.name}' at ${node.location} is defined multiple times (also at ${previousType.location})`); } + this.nameToType.set(node.name, node.type); } diff --git a/parser/src/semantic/02_match_type_definitions.ts b/parser/src/semantic/02_match_type_definitions.ts index 5a687be07..207293f12 100644 --- a/parser/src/semantic/02_match_type_definitions.ts +++ b/parser/src/semantic/02_match_type_definitions.ts @@ -3,12 +3,14 @@ import { SemanticError } from "./analyser"; import { Visitor } from "./visitor"; export class MatchTypeDefinitionsVisitor extends Visitor { - visit(node: AstNode) { + visit(node: AstNode): void { if (node instanceof TypeReference) { const definition = this.root.typeDefinitions.find(t => t.name === node.name); + if (definition === undefined) { throw new SemanticError(`Could not find type '${node.name}' at ${node.location}`); } + node.type = definition.type; } diff --git a/parser/src/semantic/03_check_no_recursive_types.ts b/parser/src/semantic/03_check_no_recursive_types.ts index 9bb60db57..a6c4c82eb 100644 --- a/parser/src/semantic/03_check_no_recursive_types.ts +++ b/parser/src/semantic/03_check_no_recursive_types.ts @@ -4,9 +4,10 @@ import { Visitor } from "./visitor"; export class CheckNoRecursiveTypesVisitor extends Visitor { path: string[] = []; + rootType: Type | undefined; - visit(node: AstNode) { + visit(node: AstNode): void { if (node instanceof TypeDefinition) { this.path = [node.name]; this.rootType = node.type; @@ -21,6 +22,7 @@ export class CheckNoRecursiveTypesVisitor extends Visitor { if (this.rootType === node.type) { throw new SemanticError(`Detected type recursion: ${this.path.join(".")} at ${node.location}`); } + this.visit(node.type); super.visit(node); } else { diff --git a/parser/src/semantic/04_check_dont_return_secret.ts b/parser/src/semantic/04_check_dont_return_secret.ts index aff0fccea..ed6de08ad 100644 --- a/parser/src/semantic/04_check_dont_return_secret.ts +++ b/parser/src/semantic/04_check_dont_return_secret.ts @@ -4,12 +4,13 @@ import { Visitor } from "./visitor"; export class CheckDontReturnSecretVisitor extends Visitor { isInReturn = false; + path: string[] = []; - visit(node: AstNode) { + visit(node: AstNode): void { if (node instanceof Operation) { this.isInReturn = true; - this.path.push(node.name + "(...)"); + this.path.push(`${node.name}(...)`); this.visit(node.returnType); this.path.pop(); this.isInReturn = false; @@ -20,6 +21,7 @@ export class CheckDontReturnSecretVisitor extends Visitor { if (this.isInReturn && node.secret) { throw new SemanticError(`Can't return a secret value at ${this.path.join(".")} at ${node.location}`); } + super.visit(node); this.path.pop(); } else { diff --git a/parser/src/semantic/05_check_naming_for_getters_returning_bool.ts b/parser/src/semantic/05_check_naming_for_getters_returning_bool.ts index fe58c8884..3ac357626 100644 --- a/parser/src/semantic/05_check_naming_for_getters_returning_bool.ts +++ b/parser/src/semantic/05_check_naming_for_getters_returning_bool.ts @@ -3,12 +3,13 @@ import { SemanticError } from "./analyser"; import { Visitor } from "./visitor"; export class CheckNamingForGettersReturningBoolVisitor extends Visitor { - visit(node: AstNode) { + visit(node: AstNode): void { super.visit(node); if (node instanceof GetOperation) { const returnsBool = node.returnType instanceof BoolPrimitiveType; - const hasBoolNaming = !!node.name.match(/^(is|has|can|may|should)/); + const hasBoolNaming = Boolean(node.name.match(/^(?:is|has|can|may|should)/u)); + if (returnsBool && !hasBoolNaming) { throw new SemanticError(`Get operation '${node.name}' at ${node.location} returns bool but isn't named accordingly`); } else if (!returnsBool && hasBoolNaming) { diff --git a/parser/src/semantic/06_give_struct_and_enum_names.ts b/parser/src/semantic/06_give_struct_and_enum_names.ts index ddbbddcc6..b6ffb15ae 100644 --- a/parser/src/semantic/06_give_struct_and_enum_names.ts +++ b/parser/src/semantic/06_give_struct_and_enum_names.ts @@ -4,9 +4,10 @@ import { Visitor } from "./visitor"; export class GiveStructAndEnumNamesVisitor extends Visitor { path: string[] = []; + names = new Map(); - visit(node: AstNode) { + visit(node: AstNode): void { if (node instanceof TypeDefinition) { this.path = [node.name]; super.visit(node); @@ -20,6 +21,7 @@ export class GiveStructAndEnumNamesVisitor extends Visitor { } else if (node instanceof StructType || node instanceof EnumType) { node.name = this.path.map(s => s[0].toUpperCase() + s.slice(1)).join(""); const previous = this.names.get(node.name); + if (previous && JSON.stringify(previous.type) !== JSON.stringify(node)) { throw new SemanticError( `The name of the type '${this.path.join(".")}' at ${node.location} will conflict with '${previous.path.join(".")}' at ${ @@ -27,7 +29,8 @@ export class GiveStructAndEnumNamesVisitor extends Visitor { }`, ); } - this.names.set(node.name, { type: node, path: [...this.path] }); + + this.names.set(node.name, { path: [...this.path], type: node }); super.visit(node); } else { super.visit(node); diff --git a/parser/src/semantic/07_check_empty_struct_or_enum.ts b/parser/src/semantic/07_check_empty_struct_or_enum.ts index b77e3c64d..ed22cf45a 100644 --- a/parser/src/semantic/07_check_empty_struct_or_enum.ts +++ b/parser/src/semantic/07_check_empty_struct_or_enum.ts @@ -3,15 +3,19 @@ import { SemanticError } from "./analyser"; import { Visitor } from "./visitor"; export class CheckEmptyStructOrEnumVisitor extends Visitor { - visit(node: AstNode) { + visit(node: AstNode): void { super.visit(node); if (node instanceof EnumType) { - if (node.values.length === 0) throw new SemanticError(`Enum '${node.name}' at ${node.location} is empty`); + if (node.values.length === 0) { + throw new SemanticError(`Enum '${node.name}' at ${node.location} is empty`); + } } if (node instanceof StructType) { - if (node.fields.length === 0 && node.spreads.length === 0) throw new SemanticError(`Struct '${node.name}' at ${node.location} is empty`); + if (node.fields.length === 0 && node.spreads.length === 0) { + throw new SemanticError(`Struct '${node.name}' at ${node.location} is empty`); + } } } } diff --git a/parser/src/semantic/08_collect_struct_and_enum_types.ts b/parser/src/semantic/08_collect_struct_and_enum_types.ts index c4904e98c..5f4ac374e 100644 --- a/parser/src/semantic/08_collect_struct_and_enum_types.ts +++ b/parser/src/semantic/08_collect_struct_and_enum_types.ts @@ -2,7 +2,7 @@ import { AstNode, EnumType, StructType } from "../ast"; import { Visitor } from "./visitor"; export class CollectStructAndEnumTypesVisitor extends Visitor { - visit(node: AstNode) { + visit(node: AstNode): void { super.visit(node); if (node instanceof StructType) { diff --git a/parser/src/semantic/09_apply_struct_spreads.ts b/parser/src/semantic/09_apply_struct_spreads.ts index b03913eaf..073ea3e19 100644 --- a/parser/src/semantic/09_apply_struct_spreads.ts +++ b/parser/src/semantic/09_apply_struct_spreads.ts @@ -3,13 +3,18 @@ import { SemanticError } from "./analyser"; import { Visitor } from "./visitor"; export class ApplyStructSpreadsVisitor extends Visitor { - // Here we may visit the same struct multiple times - // We must make sure we only process each one once + /* + * Here we may visit the same struct multiple times + * We must make sure we only process each one once + */ processed = new Set(); - visit(node: AstNode) { + visit(node: AstNode): void { if (node instanceof StructType) { - if (this.processed.has(node)) return; + if (this.processed.has(node)) { + return; + } + this.processed.add(node); } @@ -23,10 +28,11 @@ export class ApplyStructSpreadsVisitor extends Visitor { ); } - this.visit(other); // recursion! + this.visit(other); // Recursion! for (const otherField of other.fields) { const existingIdx = node.fields.findIndex(f => f.name === otherField.name); + if (existingIdx >= 0) { node.fields[existingIdx] = otherField; } else { diff --git a/parser/src/semantic/10_validate_annotations.ts b/parser/src/semantic/10_validate_annotations.ts index 4a9d7e63f..92951fdf8 100644 --- a/parser/src/semantic/10_validate_annotations.ts +++ b/parser/src/semantic/10_validate_annotations.ts @@ -30,30 +30,32 @@ import { import { SemanticError } from "./analyser"; import { Visitor } from "./visitor"; -const REST_ENCODABLE_TYPES: Function[] = [ - BoolPrimitiveType, - IntPrimitiveType, - UIntPrimitiveType, - BigIntPrimitiveType, - FloatPrimitiveType, - StringPrimitiveType, - DatePrimitiveType, - DateTimePrimitiveType, - MoneyPrimitiveType, - CpfPrimitiveType, - CnpjPrimitiveType, - UuidPrimitiveType, - HexPrimitiveType, - Base64PrimitiveType, - EnumType, -]; +function isRestEncodable(type: Type) { + return ( + type instanceof BoolPrimitiveType || + type instanceof IntPrimitiveType || + type instanceof UIntPrimitiveType || + type instanceof BigIntPrimitiveType || + type instanceof FloatPrimitiveType || + type instanceof StringPrimitiveType || + type instanceof DatePrimitiveType || + type instanceof DateTimePrimitiveType || + type instanceof MoneyPrimitiveType || + type instanceof CpfPrimitiveType || + type instanceof CnpjPrimitiveType || + type instanceof UuidPrimitiveType || + type instanceof HexPrimitiveType || + type instanceof Base64PrimitiveType || + type instanceof EnumType + ); +} function extractRealType(type: Type): Type { return type instanceof TypeReference ? extractRealType(type.type) : type; } export class ValidateAnnotationsVisitor extends Visitor { - visit(node: AstNode) { + visit(node: AstNode): void { if (node instanceof EnumValue) { for (const annotation of node.annotations) { if (annotation instanceof DescriptionAnnotation) { @@ -88,12 +90,14 @@ export class ValidateAnnotationsVisitor extends Visitor { } } else if (annotation instanceof RestAnnotation) { const allVariables = [...annotation.pathVariables, ...annotation.queryVariables, ...annotation.headers.values()]; + if (allVariables.length !== new Set(allVariables).size) { throw new SemanticError(`Arguments must appear only once for rest annotation at ${annotation.location}`); } for (const name of allVariables) { - const arg = node.args.find(arg => arg.name === name); + const arg = node.args.find(x => x.name === name); + if (!arg) { throw new SemanticError(`Argument '${name}' not found at ${annotation.location}`); } @@ -104,7 +108,7 @@ export class ValidateAnnotationsVisitor extends Visitor { const baseType = arg.type instanceof OptionalType ? arg.type.base : arg.type; - if (!REST_ENCODABLE_TYPES.includes(extractRealType(baseType).constructor)) { + if (!isRestEncodable(extractRealType(baseType))) { throw new SemanticError(`Argument '${name}' can't have type '${arg.type.name}' for rest annotation at ${annotation.location}`); } } diff --git a/parser/src/semantic/analyser.ts b/parser/src/semantic/analyser.ts index ae7d6727e..957d8efd5 100644 --- a/parser/src/semantic/analyser.ts +++ b/parser/src/semantic/analyser.ts @@ -12,7 +12,7 @@ import { ValidateAnnotationsVisitor } from "./10_validate_annotations"; export class SemanticError extends Error {} -export function analyse(root: AstRoot) { +export function analyse(root: AstRoot): void { root.errors.push("Fatal"); root.errors = [...new Set(root.errors)]; diff --git a/parser/src/semantic/visitor.ts b/parser/src/semantic/visitor.ts index 7d8ef4bfa..87ddae3ef 100644 --- a/parser/src/semantic/visitor.ts +++ b/parser/src/semantic/visitor.ts @@ -3,33 +3,7 @@ import { ArrayType, AstNode, AstRoot, Field, Operation, OptionalType, StructType export abstract class Visitor { constructor(protected root: AstRoot) {} - // TODO: Use this dependency graph to avoid relying on explicit order - - // dependencies: Visitor[] = []; - // processed = false; - - // dependOn(otherVisitor: Visitor) { - // if (otherVisitor.hasDependencyOn(this)) { - // throw new Error("Cyclic dependency between visitors."); - // } - - // if (this.processed) { - // throw new Error("Visitor already executed. Can't add dependency.") - // } - - // this.dependencies.push(otherVisitor); - // } - - // hasDependencyOn(otherVisitor: Visitor): boolean { - // return this.dependencies.includes(otherVisitor) || this.dependencies.some(v => v.hasDependencyOn(otherVisitor)); - // } - - process() { - // if (this.processed) return; - // for (const dependency of this.dependencies) - // dependency.process(root); - // this.processed = true; - + process(): void { for (const typeDefinition of this.root.typeDefinitions) { this.visit(typeDefinition); } @@ -39,11 +13,12 @@ export abstract class Visitor { } } - visit(node: AstNode) { + visit(node: AstNode): void { if (node instanceof Operation) { for (const arg of node.args) { this.visit(arg); } + this.visit(node.returnType); } else if (node instanceof Field || node instanceof TypeDefinition) { this.visit(node.type); diff --git a/parser/src/token.ts b/parser/src/token.ts index c5fa6635f..0174d5d24 100644 --- a/parser/src/token.ts +++ b/parser/src/token.ts @@ -1,9 +1,11 @@ export class TokenLocation { public filename = "?"; + public line = 0; + public column = 0; - toString() { + toString(): string { return `${this.filename}:${this.line}:${this.column}`; } } @@ -19,8 +21,9 @@ export class Token { return this; } - toString() { + toString(): string { const name = (this.constructor as any).name.replace("Token", ""); + return this.value === "" ? name : `${name}(${JSON.stringify(this.value)})`; } } @@ -41,55 +44,55 @@ export class SpreadSymbolToken extends Token {} export class AnnotationToken extends Token {} export class ImportKeywordToken extends Token { - maybeAsIdentifier() { + maybeAsIdentifier(): IdentifierToken { return new IdentifierToken("import"); } } export class TypeKeywordToken extends Token { - maybeAsIdentifier() { + maybeAsIdentifier(): IdentifierToken { return new IdentifierToken("type"); } } export class EnumKeywordToken extends Token { - maybeAsIdentifier() { + maybeAsIdentifier(): IdentifierToken { return new IdentifierToken("enum"); } } export class GetKeywordToken extends Token { - maybeAsIdentifier() { + maybeAsIdentifier(): IdentifierToken { return new IdentifierToken("get"); } } export class FunctionKeywordToken extends Token { - maybeAsIdentifier() { + maybeAsIdentifier(): IdentifierToken { return new IdentifierToken(this.value); } } export class ErrorKeywordToken extends Token { - maybeAsIdentifier() { + maybeAsIdentifier(): IdentifierToken { return new IdentifierToken("error"); } } export class TrueKeywordToken extends Token { - maybeAsIdentifier() { + maybeAsIdentifier(): IdentifierToken { return new IdentifierToken("true"); } } export class FalseKeywordToken extends Token { - maybeAsIdentifier() { + maybeAsIdentifier(): IdentifierToken { return new IdentifierToken("false"); } } export class PrimitiveTypeToken extends Token { - maybeAsIdentifier() { + maybeAsIdentifier(): IdentifierToken { return new IdentifierToken(this.value); } } From e4b3f88336696cbe9fa32769734dca471042bd2c Mon Sep 17 00:00:00 2001 From: Guilherme Bernal Date: Sat, 17 Oct 2020 16:53:43 +0000 Subject: [PATCH 03/10] fix: spec --- node-runtime/spec/rest/rest.spec.ts | 4 +- node-runtime/spec/simple/simple.spec.ts | 4 +- node-runtime/src/swagger.ts | 284 ++++++++++++------------ node-runtime/src/test-wrapper.ts | 4 +- parser/src/json.ts | 6 +- 5 files changed, 151 insertions(+), 151 deletions(-) diff --git a/node-runtime/spec/rest/rest.spec.ts b/node-runtime/spec/rest/rest.spec.ts index cf26aed4f..06ffb2adb 100644 --- a/node-runtime/spec/rest/rest.spec.ts +++ b/node-runtime/spec/rest/rest.spec.ts @@ -8,7 +8,7 @@ import { Context, SdkgenHttpServer } from "../../src"; const ast = new Parser(`${__dirname}/api.sdkgen`).parse(); -writeFileSync(`${__dirname}/api.ts`, generateNodeServerSource(ast, {}).replace("@sdkgen/node-runtime", "../../src")); +writeFileSync(`${__dirname}/api.ts`, generateNodeServerSource(ast).replace("@sdkgen/node-runtime", "../../src")); const { api } = require(`${__dirname}/api.ts`); unlinkSync(`${__dirname}/api.ts`); @@ -56,7 +56,7 @@ api.fn.getHtml = async (ctx: Context, args: {}) => { return "

Hello world!

"; }; -writeFileSync(`${__dirname}/nodeClient.ts`, generateNodeClientSource(ast, {}).replace("@sdkgen/node-runtime", "../../src")); +writeFileSync(`${__dirname}/nodeClient.ts`, generateNodeClientSource(ast).replace("@sdkgen/node-runtime", "../../src")); const { ApiClient: NodeApiClient } = require(`${__dirname}/nodeClient.ts`); unlinkSync(`${__dirname}/nodeClient.ts`); diff --git a/node-runtime/spec/simple/simple.spec.ts b/node-runtime/spec/simple/simple.spec.ts index ea43b26fa..55338ebd0 100644 --- a/node-runtime/spec/simple/simple.spec.ts +++ b/node-runtime/spec/simple/simple.spec.ts @@ -7,7 +7,7 @@ import { Context, SdkgenHttpServer } from "../../src"; const ast = new Parser(`${__dirname}/api.sdkgen`).parse(); -writeFileSync(`${__dirname}/api.ts`, generateNodeServerSource(ast, {}).replace("@sdkgen/node-runtime", "../../src")); +writeFileSync(`${__dirname}/api.ts`, generateNodeServerSource(ast).replace("@sdkgen/node-runtime", "../../src")); const { api } = require(`${__dirname}/api.ts`); unlinkSync(`${__dirname}/api.ts`); @@ -31,7 +31,7 @@ api.fn.identity = async (ctx: Context & { aaa: boolean }, { types }: { types: an const { ApiClient: NodeLegacyApiClient } = require(`${__dirname}/legacyNodeClient.ts`); const nodeLegacyClient = new NodeLegacyApiClient("http://localhost:8000"); -writeFileSync(`${__dirname}/nodeClient.ts`, generateNodeClientSource(ast, {}).replace("@sdkgen/node-runtime", "../../src")); +writeFileSync(`${__dirname}/nodeClient.ts`, generateNodeClientSource(ast).replace("@sdkgen/node-runtime", "../../src")); const { ApiClient: NodeApiClient } = require(`${__dirname}/nodeClient.ts`); unlinkSync(`${__dirname}/nodeClient.ts`); diff --git a/node-runtime/src/swagger.ts b/node-runtime/src/swagger.ts index cd273a1f8..b5cd269e2 100644 --- a/node-runtime/src/swagger.ts +++ b/node-runtime/src/swagger.ts @@ -31,7 +31,117 @@ import { SdkgenHttpServer } from "./http-server"; const swaggerUiAssetPath = getSwaggerUiAssetPath(); -export function setupSwagger(server: SdkgenHttpServer) { +function objectFromEntries(entries: Array<[string, T]>): { [key: string]: T } { + return Object.assign({}, ...Array.from(entries, ([k, v]) => ({ [k]: v }))); +} + +function typeToSchema(definitions: any, type: Type): any { + if (type instanceof EnumType) { + return { + enum: type.values.map(x => x.value), + type: "string", + }; + } else if (type instanceof StructType) { + return { + properties: objectFromEntries( + type.fields.map(field => [ + field.name, + { + description: + field.annotations + .filter(x => x instanceof DescriptionAnnotation) + .map(x => (x as DescriptionAnnotation).text) + .join(" ") || undefined, + ...typeToSchema(definitions, field.type), + }, + ]), + ), + required: type.fields.filter(f => !(f.type instanceof OptionalType)).map(f => f.name), + type: "object", + }; + } else if ( + type instanceof StringPrimitiveType || + type instanceof UuidPrimitiveType || + type instanceof HexPrimitiveType || + type instanceof HtmlPrimitiveType || + type instanceof Base64PrimitiveType + ) { + return { + type: "string", + }; + } else if (type instanceof UrlPrimitiveType) { + return { + format: "uri", + type: "string", + }; + } else if (type instanceof DatePrimitiveType) { + return { + format: "date", + type: "string", + }; + } else if (type instanceof DateTimePrimitiveType) { + return { + format: "date-time", + type: "string", + }; + } else if (type instanceof CpfPrimitiveType) { + return { + type: "string", + }; + } else if (type instanceof CnpjPrimitiveType) { + return { + type: "string", + }; + } else if (type instanceof BoolPrimitiveType) { + return { + type: "boolean", + }; + } else if (type instanceof BytesPrimitiveType) { + return { + format: "byte", + type: "string", + }; + } else if (type instanceof IntPrimitiveType) { + return { + format: "int32", + type: "integer", + }; + } else if (type instanceof UIntPrimitiveType) { + return { + format: "int32", + minimum: 0, + type: "integer", + }; + } else if (type instanceof MoneyPrimitiveType) { + return { + format: "int64", + type: "integer", + }; + } else if (type instanceof FloatPrimitiveType) { + return { + type: "number", + }; + } else if (type instanceof OptionalType) { + return { + oneOf: [typeToSchema(definitions, type.base), { type: "null" }], + }; + } else if (type instanceof ArrayType) { + return { + items: typeToSchema(definitions, type.base), + type: "array", + }; + } else if (type instanceof TypeReference) { + if (!definitions[type.name]) { + definitions[type.name] = typeToSchema(definitions, type.type); + } + + return { $ref: `#/definitions/${type.name}` }; + } + + throw new Error(`Unhandled type ${type.constructor.name}`); +} + +export function setupSwagger(server: SdkgenHttpServer): void { server.addHttpHandler("GET", "/swagger", (req, res) => { if (!server.introspection) { res.statusCode = 404; @@ -146,43 +256,36 @@ export function setupSwagger(server: SdkgenHttpServer x instanceof DescriptionAnnotation) - .map(x => (x as DescriptionAnnotation).text) - .join(" ") || undefined, - tags: ["REST Endpoints"], operationId: op.name, parameters: [ ...ann.pathVariables.map(name => ({ - name, - location: "path", arg: op.args.find(arg => arg.name === name)!, + location: "path", + name, })), ...ann.queryVariables.map(name => ({ - name, - location: "query", arg: op.args.find(arg => arg.name === name)!, + location: "query", + name, })), ...[...ann.headers.entries()].map(([header, name]) => ({ - name: header, - location: "header", arg: op.args.find(arg => arg.name === name)!, + location: "header", + name: header, })), ].map(({ name, location, arg }) => ({ - name, - in: location, - required: !(arg.type instanceof OptionalType), - schema: typeToSchema(definitions, arg.type), description: arg.annotations .filter(x => x instanceof DescriptionAnnotation) .map(x => (x as DescriptionAnnotation).text) .join(" ") || undefined, + in: location, + name, + required: !(arg.type instanceof OptionalType), + schema: typeToSchema(definitions, arg.type), })), requestBody: ann.bodyVariable ? { - required: !(op.args.find(arg => arg.name === ann.bodyVariable)!.type instanceof OptionalType), content: { ...(() => { const bodyType = op.args.find(arg => arg.name === ann.bodyVariable)!.type; @@ -213,14 +316,16 @@ export function setupSwagger(server: SdkgenHttpServer arg.name === ann.bodyVariable)!.type), }, }, + required: !(op.args.find(arg => arg.name === ann.bodyVariable)!.type instanceof OptionalType), } : undefined, responses: { ...(op.returnType instanceof OptionalType || op.returnType instanceof VoidPrimitiveType ? { [ann.method === "GET" ? "404" : "204"]: {} } : {}), - ...(!(op.returnType instanceof VoidPrimitiveType) - ? { + ...(op.returnType instanceof VoidPrimitiveType + ? {} + : { 200: { content: { ...(() => { @@ -250,29 +355,34 @@ export function setupSwagger(server: SdkgenHttpServer x instanceof DescriptionAnnotation) + .map(x => (x as DescriptionAnnotation).text) + .join(" ") || undefined, + tags: ["REST Endpoints"], }; } } @@ -280,13 +390,13 @@ export function setupSwagger(server: SdkgenHttpServer(server: SdkgenHttpServer(entries: Array<[string, T]>): { [key: string]: T } { - return Object.assign({}, ...Array.from(entries, ([k, v]) => ({ [k]: v }))); -} - -function typeToSchema(definitions: any, type: Type): any { - if (type instanceof EnumType) { - return { - type: "string", - enum: type.values.map(x => x.value), - }; - } else if (type instanceof StructType) { - return { - type: "object", - required: type.fields.filter(f => !(f.type instanceof OptionalType)).map(f => f.name), - properties: objectFromEntries( - type.fields.map(field => [ - field.name, - { - description: - field.annotations - .filter(x => x instanceof DescriptionAnnotation) - .map(x => (x as DescriptionAnnotation).text) - .join(" ") || undefined, - ...typeToSchema(definitions, field.type), - }, - ]), - ), - }; - } else if ( - type instanceof StringPrimitiveType || - type instanceof UuidPrimitiveType || - type instanceof HexPrimitiveType || - type instanceof HtmlPrimitiveType || - type instanceof Base64PrimitiveType - ) { - return { - type: "string", - }; - } else if (type instanceof UrlPrimitiveType) { - return { - format: "uri", - type: "string", - }; - } else if (type instanceof DatePrimitiveType) { - return { - format: "date", - type: "string", - }; - } else if (type instanceof DateTimePrimitiveType) { - return { - format: "date-time", - type: "string", - }; - } else if (type instanceof CpfPrimitiveType) { - return { - type: "string", - }; - } else if (type instanceof CnpjPrimitiveType) { - return { - type: "string", - }; - } else if (type instanceof BoolPrimitiveType) { - return { - type: "boolean", - }; - } else if (type instanceof BytesPrimitiveType) { - return { - format: "byte", - type: "string", - }; - } else if (type instanceof IntPrimitiveType) { - return { - type: "integer", - format: "int32", - }; - } else if (type instanceof UIntPrimitiveType) { - return { - type: "integer", - format: "int32", - minimum: 0, - }; - } else if (type instanceof MoneyPrimitiveType) { - return { - type: "integer", - format: "int64", - }; - } else if (type instanceof FloatPrimitiveType) { - return { - type: "number", - }; - } else if (type instanceof OptionalType) { - return { - oneOf: [typeToSchema(definitions, type.base), { type: "null" }], - }; - } else if (type instanceof ArrayType) { - return { - type: "array", - items: typeToSchema(definitions, type.base), - }; - } else if (type instanceof TypeReference) { - if (!definitions[type.name]) { - definitions[type.name] = typeToSchema(definitions, type.type); - } - - return { $ref: `#/definitions/${type.name}` }; - } - - throw new Error(`Unhandled type ${type.constructor.name}`); -} diff --git a/node-runtime/src/test-wrapper.ts b/node-runtime/src/test-wrapper.ts index 8dd9e5684..7cdbb0390 100644 --- a/node-runtime/src/test-wrapper.ts +++ b/node-runtime/src/test-wrapper.ts @@ -22,21 +22,21 @@ export function apiTestWrapper>(api: T): T { ctx.request && ctx.request.deviceInfo ? ctx.request.deviceInfo : { + fingerprint: null, id: randomBytes(16).toString("hex"), language: null, platform: null, timezone: null, type: ctx.request && ctx.request.deviceInfo && ctx.request.deviceInfo.type ? ctx.request.deviceInfo.type : "test", version: null, - fingerprint: null, }, extra: ctx.request && ctx.request.extra ? ctx.request.extra : {}, + files: ctx.request && ctx.request.files ? ctx.request.files : [], headers: ctx.request && ctx.request.headers ? ctx.request.headers : {}, id: randomBytes(16).toString("hex"), ip: ctx.request && ctx.request.ip ? ctx.request.ip : "0.0.0.0", name: functionName, version: 3, - files: ctx.request && ctx.request.files ? ctx.request.files : [], }; let reply = await api.hook.onRequestStart(ctx); diff --git a/parser/src/json.ts b/parser/src/json.ts index b48ec3291..a5d6cf702 100644 --- a/parser/src/json.ts +++ b/parser/src/json.ts @@ -60,7 +60,7 @@ export function astToJson(ast: AstRoot): AstJson { if (ann instanceof DescriptionAnnotation) { const target = `type.${name}.${field.name}`; - annotations[target] = []; + annotations[target] ||= []; const list = annotations[target]; list.push({ type: "description", value: ann.text }); @@ -84,7 +84,7 @@ export function astToJson(ast: AstRoot): AstJson { if (ann instanceof DescriptionAnnotation) { const target = `fn.${op.prettyName}.${arg.name}`; - annotations[target] = []; + annotations[target] ||= []; const list = annotations[target]; list.push({ type: "description", value: ann.text }); @@ -99,7 +99,7 @@ export function astToJson(ast: AstRoot): AstJson { for (const ann of op.annotations) { const target = `fn.${op.prettyName}`; - annotations[target] = []; + annotations[target] ||= []; const list = annotations[target]; if (ann instanceof DescriptionAnnotation) { From be76ea76792945c5c1f76ecaeb671f52db5298d5 Mon Sep 17 00:00:00 2001 From: Guilherme Bernal Date: Sun, 18 Oct 2020 23:09:33 +0000 Subject: [PATCH 04/10] feat: add eslint to node-runtime --- .github/workflows/test.yml | 2 + node-runtime/.eslintrc.json | 4 +- node-runtime/package.json | 4 +- node-runtime/spec/error.spec.ts | 2 +- node-runtime/spec/rest/rest.spec.ts | 114 +++--- node-runtime/src/api-config.ts | 2 +- node-runtime/src/encode-decode.ts | 20 +- node-runtime/src/error.ts | 4 +- node-runtime/src/http-client.ts | 4 +- node-runtime/src/http-server.ts | 531 ++++++++++++++-------------- node-runtime/src/swagger.ts | 7 +- 11 files changed, 355 insertions(+), 339 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a6acfec99..8e71b64a5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -103,6 +103,8 @@ jobs: working-directory: ./node-runtime - run: npm run prettier:check working-directory: ./node-runtime + - run: npm run eslint:check + working-directory: ./node-runtime - run: npm run build working-directory: ./node-runtime - run: npm test diff --git a/node-runtime/.eslintrc.json b/node-runtime/.eslintrc.json index 73626649c..9c2b6a1a8 100644 --- a/node-runtime/.eslintrc.json +++ b/node-runtime/.eslintrc.json @@ -35,7 +35,6 @@ } ], "@typescript-eslint/brace-style": "error", - "@typescript-eslint/camelcase": "warn", "@typescript-eslint/consistent-type-definitions": [ "error", "interface" @@ -49,9 +48,11 @@ ], "@typescript-eslint/interface-name-prefix": "off", "@typescript-eslint/member-delimiter-style": "off", + "@typescript-eslint/no-empty-interface": "off", "@typescript-eslint/no-unnecessary-qualifier": "error", "@typescript-eslint/no-unnecessary-type-arguments": "error", "@typescript-eslint/no-unused-expressions": "error", + "@typescript-eslint/no-use-before-define": "error", "@typescript-eslint/no-useless-constructor": "error", "@typescript-eslint/prefer-for-of": "error", "@typescript-eslint/promise-function-async": "error", @@ -134,7 +135,6 @@ "no-throw-literal": "error", "no-undef-init": "error", "no-unmodified-loop-condition": "error", - "no-use-before-define": "error", "no-useless-call": "error", "no-useless-concat": "error", "no-void": "error", diff --git a/node-runtime/package.json b/node-runtime/package.json index fe6c059bf..819a8d4c5 100644 --- a/node-runtime/package.json +++ b/node-runtime/package.json @@ -5,8 +5,8 @@ "main": "src/index.ts", "scripts": { "test": "TZ=UTC jest --passWithNoTests", - "eslint:fix": "eslint --fix '**/*.{t,j}s'", - "eslint:check": "eslint '**/*.{t,j}s'", + "eslint:fix": "eslint --fix '**/*.ts'", + "eslint:check": "eslint '**/*.ts'", "prettier:fix": "prettier --write '**/*.{t,j}s'", "prettier:check": "prettier --check '**/*.{t,j}s'", "build": "tsc" diff --git a/node-runtime/spec/error.spec.ts b/node-runtime/spec/error.spec.ts index 1c5fae8d1..4ee5d1e21 100644 --- a/node-runtime/spec/error.spec.ts +++ b/node-runtime/spec/error.spec.ts @@ -8,6 +8,6 @@ describe("Error", () => { expect(err1.message).toEqual("the error message"); expect(err1.type).toEqual("TestError"); - expect(JSON.parse(JSON.stringify(err1))).toEqual({ type: "TestError", message: "the error message" }); + expect(JSON.parse(JSON.stringify(err1))).toEqual({ message: "the error message", type: "TestError" }); }); }); diff --git a/node-runtime/spec/rest/rest.spec.ts b/node-runtime/spec/rest/rest.spec.ts index 06ffb2adb..1c90b53e9 100644 --- a/node-runtime/spec/rest/rest.spec.ts +++ b/node-runtime/spec/rest/rest.spec.ts @@ -18,7 +18,7 @@ api.fn.add = async (ctx: Context, { first, second }: { first: number; second: st }; api.fn.maybe = async (ctx: Context, { bin }: { bin: string | null }) => { - return bin !== null ? Buffer.from(bin, "hex") : null; + return bin === null ? null : Buffer.from(bin, "hex"); }; api.fn.hex = async (ctx: Context, { bin }: { bin: Buffer }) => { @@ -27,13 +27,13 @@ api.fn.hex = async (ctx: Context, { bin }: { bin: Buffer }) => { api.fn.obj = async (ctx: Context, { obj }: { obj: { val: number } }) => { if (obj.val === 0) { - throw "Value is zero ~ spec error"; + throw new Error("Value is zero ~ spec error"); } return obj; }; -function readAllStream(stream: Readable) { +async function readAllStream(stream: Readable) { return new Promise((resolve, reject) => { const chunks: Buffer[] = []; @@ -43,16 +43,16 @@ function readAllStream(stream: Readable) { }); } -api.fn.uploadFile = async (ctx: Context, args: {}) => { +api.fn.uploadFile = async (ctx: Context) => { return Promise.all( ctx.request.files.map(async ({ name, contents }) => ({ - name, data: await readAllStream(contents), + name, })), ); }; -api.fn.getHtml = async (ctx: Context, args: {}) => { +api.fn.getHtml = async () => { return "

Hello world!

"; }; @@ -83,104 +83,104 @@ describe("Rest API", () => { path: string; result: string; data?: string | Buffer; - headers?: object; + headers?: Record; statusCode?: number; - resultHeaders?: object; + resultHeaders?: Record; }> = [ { method: "GET", path: "/add1/1/aa", result: "1aa" }, { method: "GET", path: "/add1/1/aa/", result: "1aa" }, { - method: "GET", - path: "/add1/1/aa", - result: '"1aa"', headers: { accept: "application/json", }, + method: "GET", + path: "/add1/1/aa", + result: '"1aa"', }, { method: "GET", path: "/add2&second=aa&first=1", result: "", statusCode: 404 }, { method: "GET", path: "/add2?second=aa&first=1", result: "1aa" }, { method: "GET", path: "/add2?first=1&second=aa", result: "1aa" }, { + headers: { + "x-second": "aa", + }, method: "GET", path: "/add3?first=1", result: "1aa", + }, + { headers: { + accept: "application/json", "x-second": "aa", }, - }, - { method: "GET", path: "/add3?first=1", result: '"1aa"', + }, + { + data: "1", headers: { - accept: "application/json", "x-second": "aa", }, - }, - { method: "POST", path: "/add4", result: "1aa", - headers: { - "x-second": "aa", - }, - data: "1", }, { - method: "POST", - path: "/add4", - result: '"1aa"', + data: "1", headers: { accept: "application/json", "x-second": "aa", }, - data: "1", + method: "POST", + path: "/add4", + result: '"1aa"', }, { - method: "POST", - path: "/add5", - result: "1aa", + data: "aa", headers: { "x-first": "1", }, - data: "aa", - }, - { method: "POST", path: "/add5", result: "1aa", + }, + { + data: '"aa"', headers: { "content-type": "application/json", "x-first": "1", }, - data: '"aa"', - }, - { method: "POST", path: "/add5", - result: '"1aa"', + result: "1aa", + }, + { + data: '"aa"', headers: { accept: "application/json", "content-type": "application/json", "x-first": "1", }, - data: '"aa"', + method: "POST", + path: "/add5", + result: '"1aa"', }, { method: "POST", path: "/add6?second=aa&first=1", result: "1aa" }, { method: "POST", path: "/add6?first=1&second=aa", result: "1aa" }, { + data: "second=aa&first=1", + headers: { "content-type": "application/x-www-form-urlencoded" }, method: "POST", path: "/add6", result: "1aa", - data: "second=aa&first=1", - headers: { "content-type": "application/x-www-form-urlencoded" }, }, { + data: "first=1&second=aa", + headers: { "content-type": "application/x-www-form-urlencoded" }, method: "POST", path: "/add6", result: "1aa", - data: "first=1&second=aa", - headers: { "content-type": "application/x-www-form-urlencoded" }, }, { method: "GET", @@ -205,12 +205,12 @@ describe("Rest API", () => { }, }, { - method: "GET", - path: "/maybe?bin=61", - result: '"YQ=="', headers: { accept: "application/json", }, + method: "GET", + path: "/maybe?bin=61", + result: '"YQ=="', }, { method: "POST", @@ -219,47 +219,47 @@ describe("Rest API", () => { statusCode: 204, }, { + data: "61", method: "POST", path: "/maybe", result: "a", resultHeaders: { "content-type": "application/octet-stream", }, - data: "61", }, { + data: "a", method: "POST", path: "/hex", result: "61", - data: "a", }, { + data: `{"val":15}`, method: "POST", path: "/obj", result: `{"val":15}`, - data: `{"val":15}`, resultHeaders: { "content-type": "application/json", }, }, { + data: `{"val":0}`, method: "POST", path: "/obj", result: `{"message":"Value is zero ~ spec error","type":"Fatal"}`, - data: `{"val":0}`, - statusCode: 400, resultHeaders: { "content-type": "application/json", }, + statusCode: 400, }, { method: "POST", path: "/upload", result: `[]`, - statusCode: 200, resultHeaders: { "content-type": "application/json", }, + statusCode: 200, }, { method: "GET", @@ -274,17 +274,17 @@ describe("Rest API", () => { form.append("file", Buffer.from("Hello"), "test.txt"); return { + data: form.getBuffer(), + headers: { + ...form.getHeaders(), + }, method: "POST" as const, path: "/upload", result: `[{"name":"test.txt","data":"SGVsbG8="}]`, - statusCode: 200, resultHeaders: { "content-type": "application/json", }, - data: form.getBuffer(), - headers: { - ...form.getHeaders(), - }, + statusCode: 200, }; })(), ]; @@ -292,11 +292,11 @@ describe("Rest API", () => { for (const { method, path, result, headers, data, statusCode, resultHeaders } of table) { test(`${method} ${path}${headers ? ` with headers ${JSON.stringify(headers)}` : ""}`, async () => { const response = await axios.request({ - url: `http://localhost:8001${path}`, - method, - transformResponse: [data => data], - headers, data, + headers, + method, + transformResponse: [x => x], + url: `http://localhost:8001${path}`, validateStatus: () => true, }); diff --git a/node-runtime/src/api-config.ts b/node-runtime/src/api-config.ts index 751a9adcd..e1d6b039e 100644 --- a/node-runtime/src/api-config.ts +++ b/node-runtime/src/api-config.ts @@ -1,7 +1,7 @@ import { AstJson } from "@sdkgen/parser"; import { Context, ContextReply } from "./context"; -export abstract class BaseApiConfig { +export abstract class BaseApiConfig { astJson!: AstJson; fn: { diff --git a/node-runtime/src/encode-decode.ts b/node-runtime/src/encode-decode.ts index a265edbbd..c12cf8051 100644 --- a/node-runtime/src/encode-decode.ts +++ b/node-runtime/src/encode-decode.ts @@ -106,24 +106,24 @@ function simpleEncodeDecode(path: string, type: string, value: any) { throw new Error(`Unknown type '${type}' at '${path}'`); } -export function encode(typeTable: TypeTable, path: string, type: TypeDescription, value: any): any { +export function encode(typeTable: TypeTable, path: string, type: TypeDescription, value: unknown): any { if (typeof type === "string" && !type.endsWith("?") && type !== "void" && (value === null || value === undefined)) { throw new Error(`Invalid type at '${path}', cannot be null`); } else if (Array.isArray(type)) { - if (!type.includes(value)) { + if (typeof value !== "string" || !type.includes(value)) { throw new ParseError(path, type, value); } return value; } else if (typeof type === "object") { - if (typeof value !== "object" || value === undefined) { + if (typeof value !== "object" || value === undefined || value === null) { throw new ParseError(path, type, value); } const obj: any = {}; for (const key of Object.keys(type)) { - obj[key] = encode(typeTable, `${path}.${key}`, type[key], value[key]); + obj[key] = encode(typeTable, `${path}.${key}`, type[key], (value as Record)[key]); } return obj; @@ -166,7 +166,7 @@ export function encode(typeTable: TypeTable, path: string, type: TypeDescription return CNPJ.strip(value); } else if (type === "date") { - if (!(value instanceof Date) && !(typeof value === "string" && value.match(/^[0-9]{4}-[01][0-9]-[0123][0-9]$/))) { + if (!(value instanceof Date) && !(typeof value === "string" && value.match(/^[0-9]{4}-[01][0-9]-[0123][0-9]$/u))) { throw new ParseError(path, type, value); } @@ -176,7 +176,7 @@ export function encode(typeTable: TypeTable, path: string, type: TypeDescription !(value instanceof Date) && !( typeof value === "string" && - value.match(/^[0-9]{4}-[01][0-9]-[0123][0-9]T[012][0-9]:[0123456][0-9]:[0123456][0-9](?:\.[0-9]{1,6})?(?:Z|[+-][012][0-9]:[0123456][0-9])?$/) + value.match(/^[0-9]{4}-[01][0-9]-[0123][0-9]T[012][0-9]:[0123456][0-9]:[0123456][0-9](?:\.[0-9]{1,6})?(?:Z|[+-][012][0-9]:[0123456][0-9])?$/u) ) ) { throw new ParseError(path, type, value); @@ -194,11 +194,11 @@ export function encode(typeTable: TypeTable, path: string, type: TypeDescription } } -export function decode(typeTable: TypeTable, path: string, type: TypeDescription, value: any): any { +export function decode(typeTable: TypeTable, path: string, type: TypeDescription, value: unknown): any { if (typeof type === "string" && !type.endsWith("?") && type !== "void" && (value === null || value === undefined)) { throw new Error(`Invalid type at '${path}', cannot be null`); } else if (Array.isArray(type)) { - if (!type.includes(value)) { + if (typeof value !== "string" || !type.includes(value)) { throw new ParseError(path, type, value); } @@ -211,7 +211,7 @@ export function decode(typeTable: TypeTable, path: string, type: TypeDescription const obj: any = {}; for (const key of Object.keys(type)) { - obj[key] = decode(typeTable, `${path}.${key}`, type[key], value[key]); + obj[key] = decode(typeTable, `${path}.${key}`, type[key], (value as Record)[key]); } return obj; @@ -242,7 +242,7 @@ export function decode(typeTable: TypeTable, path: string, type: TypeDescription return buffer; } else if (type === "bigint") { - if (typeof value !== "number" && (typeof value !== "string" || !value.match(/^-?[0-9]+$/))) { + if (typeof value !== "number" && (typeof value !== "string" || !value.match(/^-?[0-9]+$/u))) { throw new ParseError(path, type, value); } diff --git a/node-runtime/src/error.ts b/node-runtime/src/error.ts index 75126d62a..a5072b0e7 100644 --- a/node-runtime/src/error.ts +++ b/node-runtime/src/error.ts @@ -1,9 +1,9 @@ export class SdkgenError extends Error { - get type() { + get type(): string { return this.constructor.name; } - public toJSON() { + public toJSON(): { message: string; type: string } { return { message: this.message, type: this.type, diff --git a/node-runtime/src/http-client.ts b/node-runtime/src/http-client.ts index c865d1157..39f23219d 100644 --- a/node-runtime/src/http-client.ts +++ b/node-runtime/src/http-client.ts @@ -21,7 +21,7 @@ export class SdkgenHttpClient { this.baseUrl = new URL(baseUrl); } - async makeRequest(ctx: Context | null, functionName: string, args: unknown) { + async makeRequest(ctx: Context | null, functionName: string, args: unknown): Promise { const func = this.astJson.functionTable[functionName]; if (!func) { @@ -32,7 +32,7 @@ export class SdkgenHttpClient { args: encode(this.astJson.typeTable, `${functionName}.args`, func.args, args), deviceInfo: ctx && ctx.request ? ctx.request.deviceInfo : { id: hostname(), type: "node" }, extra: { - ...[...this.extra.entries()].reduce<{ [key: string]: unknown }>((obj, [key, value]) => ((obj[key] = value), obj), {}), + ...this.extra, ...(ctx && ctx.request ? ctx.request.extra : {}), }, name: functionName, diff --git a/node-runtime/src/http-server.ts b/node-runtime/src/http-server.ts index 6b8c1cfe3..01a1e343d 100644 --- a/node-runtime/src/http-server.ts +++ b/node-runtime/src/http-server.ts @@ -41,7 +41,7 @@ import { Context, ContextReply, ContextRequest } from "./context"; import { decode, encode } from "./encode-decode"; import { setupSwagger } from "./swagger"; -export class SdkgenHttpServer { +export class SdkgenHttpServer { public httpServer: Server; private headers = new Map(); @@ -131,11 +131,11 @@ export class SdkgenHttpServer { }); } - ignoreUrlPrefix(urlPrefix: string) { + ignoreUrlPrefix(urlPrefix: string): void { this.ignoredUrlPrefix = urlPrefix; } - listen(port = 8000) { + listen(port = 8000): void { this.httpServer.listen(port, () => { const addr = this.httpServer.address(); const addrString = addr === null ? "???" : typeof addr === "string" ? addr : `${addr.address}:${addr.port}`; @@ -144,7 +144,7 @@ export class SdkgenHttpServer { }); } - close() { + async close(): Promise { return promisify(this.httpServer.close.bind(this.httpServer))(); } @@ -154,7 +154,7 @@ export class SdkgenHttpServer { this.addHeader("Access-Control-Max-Age", "86400"); } - addHeader(header: string, value: string) { + addHeader(header: string, value: string): void { const cleanHeader = header.toLowerCase().trim(); const existing = this.headers.get(cleanHeader); @@ -167,7 +167,7 @@ export class SdkgenHttpServer { } } - addHttpHandler(method: string, matcher: string | RegExp, handler: (req: IncomingMessage, res: ServerResponse, body: Buffer) => void) { + addHttpHandler(method: string, matcher: string | RegExp, handler: (req: IncomingMessage, res: ServerResponse, body: Buffer) => void): void { this.handlers.push({ handler, matcher, method }); } @@ -201,316 +201,325 @@ export class SdkgenHttpServer { private attachRestHandlers() { function escapeRegExp(str: string) { - return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + return str.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); } let hasSwagger = false; for (const op of this.ast.operations) { for (const ann of op.annotations) { - if (ann instanceof RestAnnotation) { - if (!hasSwagger) { - setupSwagger(this); - hasSwagger = true; - } + if (!(ann instanceof RestAnnotation)) { + continue; + } - const pathFragments = ann.path.split(/\{\w+\}/u); + if (!hasSwagger) { + setupSwagger(this); + hasSwagger = true; + } - let pathRegex = "^"; + const pathFragments = ann.path.split(/\{\w+\}/u); - for (let i = 0; i < pathFragments.length; ++i) { - if (i > 0) { - pathRegex += "(.+?)"; - } + let pathRegex = "^"; - pathRegex += escapeRegExp(pathFragments[i]); + for (let i = 0; i < pathFragments.length; ++i) { + if (i > 0) { + pathRegex += "(.+?)"; } - pathRegex += "/?$"; + pathRegex += escapeRegExp(pathFragments[i]); + } - for (const header of ann.headers.keys()) { - this.addHeader("Access-Control-Allow-Headers", header); - } + pathRegex += "/?$"; - this.addHttpHandler(ann.method, new RegExp(pathRegex), async (req, res, body) => { - try { - const args: any = {}; - const files: ContextRequest["files"] = []; + for (const header of ann.headers.keys()) { + this.addHeader("Access-Control-Allow-Headers", header); + } - const { pathname, query } = parseUrl(req.url || ""); - const match = pathname?.match(pathRegex); + this.addHttpHandler(ann.method, new RegExp(pathRegex, "u"), async (req, res, body) => { + try { + const args: any = {}; + const files: ContextRequest["files"] = []; - if (!match) { - res.statusCode = 404; - return; - } + const { pathname, query } = parseUrl(req.url || ""); + const match = pathname?.match(pathRegex); - const simpleArgs = new Map(); + if (!match) { + res.statusCode = 404; + return; + } - for (let i = 0; i < ann.pathVariables.length; ++i) { - const argName = ann.pathVariables[i]; - const argValue = match[i + 1]; + const simpleArgs = new Map(); - simpleArgs.set(argName, argValue); - } + for (let i = 0; i < ann.pathVariables.length; ++i) { + const argName = ann.pathVariables[i]; + const argValue = match[i + 1]; - const parsedQuery = query ? parseQuerystring(query) : {}; + simpleArgs.set(argName, argValue); + } - for (const argName of ann.queryVariables) { - const argValue = parsedQuery[argName] ?? null; + const parsedQuery = query ? parseQuerystring(query) : {}; - simpleArgs.set(argName, Array.isArray(argValue) ? argValue.join("") : argValue); - } + for (const argName of ann.queryVariables) { + const argValue = parsedQuery[argName] ?? null; - for (const [headerName, argName] of ann.headers) { - const argValue = req.headers[headerName] ?? null; + simpleArgs.set(argName, Array.isArray(argValue) ? argValue.join("") : argValue); + } - simpleArgs.set(argName, Array.isArray(argValue) ? argValue.join("") : argValue); - } + for (const [headerName, argName] of ann.headers) { + const argValue = req.headers[headerName] ?? null; - if (!ann.bodyVariable && req.headers["content-type"]?.match(/^application\/x-www-form-urlencoded/iu)) { - const parsedQuery = parseQuerystring(body.toString()); + simpleArgs.set(argName, Array.isArray(argValue) ? argValue.join("") : argValue); + } - for (const argName of ann.queryVariables) { - const argValue = parsedQuery[argName] ?? null; + if (!ann.bodyVariable && req.headers["content-type"]?.match(/^application\/x-www-form-urlencoded/iu)) { + const parsedBody = parseQuerystring(body.toString()); - simpleArgs.set(argName, Array.isArray(argValue) ? argValue.join("") : argValue); - } - } else if (!ann.bodyVariable && req.headers["content-type"]?.match(/^multipart\/form-data/iu)) { - const busboy = new Busboy({ headers: req.headers }); - const filePromises: Array> = []; + for (const argName of ann.queryVariables) { + const argValue = parsedBody[argName] ?? null; - busboy.on("field", (field, value) => { - if (ann.queryVariables.includes(field)) { - simpleArgs.set(field, `${value}`); - } - }); + simpleArgs.set(argName, Array.isArray(argValue) ? argValue.join("") : argValue); + } + } else if (!ann.bodyVariable && req.headers["content-type"]?.match(/^multipart\/form-data/iu)) { + const busboy = new Busboy({ headers: req.headers }); + const filePromises: Array> = []; - busboy.on("file", (field, file, name, encoding, mimetype) => { - const fileName = randomBytes(32).toString("hex"); - const writeStream = createWriteStream(fileName); + busboy.on("field", (field, value) => { + if (ann.queryVariables.includes(field)) { + simpleArgs.set(field, `${value}`); + } + }); - filePromises.push( - new Promise((resolve, reject) => { - writeStream.on("error", reject); + busboy.on("file", (field, file, name) => { + const fileName = randomBytes(32).toString("hex"); + const writeStream = createWriteStream(fileName); - writeStream.on("open", () => { - files.push({ name, contents: createReadStream(fileName) }); + filePromises.push( + new Promise((resolve, reject) => { + writeStream.on("error", reject); - unlink(fileName, err => { - if (err) { - reject(err); - writeStream.end(); - } - }); + writeStream.on("open", () => { + files.push({ contents: createReadStream(fileName), name }); - writeStream.on("close", resolve); - file.pipe(writeStream); + unlink(fileName, err => { + if (err) { + reject(err); + writeStream.end(); + } }); - }), - ); - }); - - await new Promise((resolve, reject) => { - busboy.on("finish", resolve); - busboy.on("error", reject); - busboy.write(body); - }); - - await Promise.all(filePromises); - } else if (ann.bodyVariable) { - const argName = ann.bodyVariable; - let { type } = op.args.find(arg => arg.name === argName)!; - - if (req.headers["content-type"] === "application/json") { - args[argName] = JSON.parse(body.toString()); - } else { - let solved = false; - if (type instanceof OptionalType) { - if (body.length === 0) { - args[argName] = null; - solved = true; - } else { - type = type.base; - } - } + writeStream.on("close", resolve); + file.pipe(writeStream); + }); + }), + ); + }); - if (!solved) { - if ( - type instanceof BoolPrimitiveType || - type instanceof IntPrimitiveType || - type instanceof UIntPrimitiveType || - type instanceof FloatPrimitiveType || - type instanceof StringPrimitiveType || - type instanceof DatePrimitiveType || - type instanceof DateTimePrimitiveType || - type instanceof MoneyPrimitiveType || - type instanceof BigIntPrimitiveType || - type instanceof CpfPrimitiveType || - type instanceof CnpjPrimitiveType || - type instanceof UuidPrimitiveType || - type instanceof HexPrimitiveType || - type instanceof Base64PrimitiveType - ) { - simpleArgs.set(argName, body.toString()); - } else if (type instanceof BytesPrimitiveType) { - args[argName] = body.toString("base64"); - } else { - args[argName] = JSON.parse(body.toString()); - } - } - } - } + await new Promise((resolve, reject) => { + busboy.on("finish", resolve); + busboy.on("error", reject); + busboy.write(body); + }); - for (const [argName, argValue] of simpleArgs) { - let { type } = op.args.find(arg => arg.name === argName)!; + await Promise.all(filePromises); + } else if (ann.bodyVariable) { + const argName = ann.bodyVariable; + const arg = op.args.find(x => x.name === argName); + + if (req.headers["content-type"] === "application/json") { + args[argName] = JSON.parse(body.toString()); + } else if (arg) { + let { type } = arg; + let solved = false; if (type instanceof OptionalType) { - if (argValue === null) { + if (body.length === 0) { args[argName] = null; - continue; + solved = true; } else { type = type.base; } - } else if (argValue === null) { - args[argName] = argValue; - continue; } - if (type instanceof BoolPrimitiveType) { - if (argValue === "true") { - args[argName] = true; - } else if (argValue === "false") { - args[argName] = false; + if (!solved) { + if ( + type instanceof BoolPrimitiveType || + type instanceof IntPrimitiveType || + type instanceof UIntPrimitiveType || + type instanceof FloatPrimitiveType || + type instanceof StringPrimitiveType || + type instanceof DatePrimitiveType || + type instanceof DateTimePrimitiveType || + type instanceof MoneyPrimitiveType || + type instanceof BigIntPrimitiveType || + type instanceof CpfPrimitiveType || + type instanceof CnpjPrimitiveType || + type instanceof UuidPrimitiveType || + type instanceof HexPrimitiveType || + type instanceof Base64PrimitiveType + ) { + simpleArgs.set(argName, body.toString()); + } else if (type instanceof BytesPrimitiveType) { + args[argName] = body.toString("base64"); } else { - args[argName] = argValue; + args[argName] = JSON.parse(body.toString()); } - } else if (type instanceof UIntPrimitiveType || type instanceof IntPrimitiveType || type instanceof MoneyPrimitiveType) { - args[argName] = parseInt(argValue, 10); - } else if (type instanceof FloatPrimitiveType) { - args[argName] = parseFloat(argValue); - } else { - args[argName] = argValue; } } + } - const ip = getClientIp(req); + for (const [argName, argValue] of simpleArgs) { + const arg = op.args.find(x => x.name === argName); - if (!ip) { - throw new Error("Couldn't determine client IP"); + if (!arg) { + continue; } - const request: ContextRequest = { - name: op.name, - ip, - headers: req.headers, - id: randomBytes(16).toString("hex"), - version: 3, - deviceInfo: { - id: randomBytes(16).toString("hex"), - type: "rest", - platform: null, - language: null, - timezone: null, - version: null, - fingerprint: null, - }, - extra: {}, - args, - files, - }; - - this.executeRequest(request, (ctx, reply) => { - try { - if (reply.error) { - res.statusCode = 400; - res.setHeader("content-type", "application/json"); - res.write(JSON.stringify(this.makeResponseError(reply.error))); - res.end(); - return; - } + let { type } = arg; - if (req.headers.accept === "application/json") { - res.setHeader("content-type", "application/json"); - res.write(JSON.stringify(reply.result)); - res.end(); - } else { - let type = op.returnType; + if (type instanceof OptionalType) { + if (argValue === null) { + args[argName] = null; + continue; + } else { + type = type.base; + } + } else if (argValue === null) { + args[argName] = argValue; + continue; + } - if (type instanceof OptionalType) { - if (reply.result === null) { - res.statusCode = ann.method === "GET" ? 404 : 204; - res.end(); - return; - } + if (type instanceof BoolPrimitiveType) { + if (argValue === "true") { + args[argName] = true; + } else if (argValue === "false") { + args[argName] = false; + } else { + args[argName] = argValue; + } + } else if (type instanceof UIntPrimitiveType || type instanceof IntPrimitiveType || type instanceof MoneyPrimitiveType) { + args[argName] = parseInt(argValue, 10); + } else if (type instanceof FloatPrimitiveType) { + args[argName] = parseFloat(argValue); + } else { + args[argName] = argValue; + } + } - type = type.base; - } + const ip = getClientIp(req); - if ( - type instanceof BoolPrimitiveType || - type instanceof IntPrimitiveType || - type instanceof UIntPrimitiveType || - type instanceof FloatPrimitiveType || - type instanceof StringPrimitiveType || - type instanceof DatePrimitiveType || - type instanceof DateTimePrimitiveType || - type instanceof MoneyPrimitiveType || - type instanceof BigIntPrimitiveType || - type instanceof CpfPrimitiveType || - type instanceof CnpjPrimitiveType || - type instanceof UuidPrimitiveType || - type instanceof HexPrimitiveType || - type instanceof Base64PrimitiveType - ) { - res.setHeader("content-type", "text/plain"); - res.write(`${reply.result}`); - res.end(); - } else if (type instanceof HtmlPrimitiveType) { - res.setHeader("content-type", "text/html"); - res.write(`${reply.result}`); - res.end(); - } else if (type instanceof BytesPrimitiveType) { - const buffer = Buffer.from(reply.result, "base64"); - - FileType.fromBuffer(buffer) - .then(fileType => { - res.setHeader("content-type", fileType?.mime ?? "application/octet-stream"); - }) - .catch(err => { - console.error(err); - res.setHeader("content-type", "application/octet-stream"); - }) - .then(() => { - res.write(buffer); - res.end(); - }); - } else { - res.setHeader("content-type", "application/json"); - res.write(JSON.stringify(reply.result)); + if (!ip) { + throw new Error("Couldn't determine client IP"); + } + + const request: ContextRequest = { + args, + deviceInfo: { + fingerprint: null, + id: randomBytes(16).toString("hex"), + language: null, + platform: null, + timezone: null, + type: "rest", + version: null, + }, + extra: {}, + files, + headers: req.headers, + id: randomBytes(16).toString("hex"), + ip, + name: op.name, + version: 3, + }; + + this.executeRequest(request, (ctx, reply) => { + try { + if (reply.error) { + res.statusCode = 400; + res.setHeader("content-type", "application/json"); + res.write(JSON.stringify(this.makeResponseError(reply.error))); + res.end(); + return; + } + + if (req.headers.accept === "application/json") { + res.setHeader("content-type", "application/json"); + res.write(JSON.stringify(reply.result)); + res.end(); + } else { + let type = op.returnType; + + if (type instanceof OptionalType) { + if (reply.result === null) { + res.statusCode = ann.method === "GET" ? 404 : 204; res.end(); + return; } - } - } catch (error) { - console.error(error); - if (!res.headersSent) { - res.statusCode = 500; + + type = type.base; } - res.end(); + if ( + type instanceof BoolPrimitiveType || + type instanceof IntPrimitiveType || + type instanceof UIntPrimitiveType || + type instanceof FloatPrimitiveType || + type instanceof StringPrimitiveType || + type instanceof DatePrimitiveType || + type instanceof DateTimePrimitiveType || + type instanceof MoneyPrimitiveType || + type instanceof BigIntPrimitiveType || + type instanceof CpfPrimitiveType || + type instanceof CnpjPrimitiveType || + type instanceof UuidPrimitiveType || + type instanceof HexPrimitiveType || + type instanceof Base64PrimitiveType + ) { + res.setHeader("content-type", "text/plain"); + res.write(`${reply.result}`); + res.end(); + } else if (type instanceof HtmlPrimitiveType) { + res.setHeader("content-type", "text/html"); + res.write(`${reply.result}`); + res.end(); + } else if (type instanceof BytesPrimitiveType) { + const buffer = Buffer.from(reply.result, "base64"); + + FileType.fromBuffer(buffer) + .then(fileType => { + res.setHeader("content-type", fileType?.mime ?? "application/octet-stream"); + }) + .catch(err => { + console.error(err); + res.setHeader("content-type", "application/octet-stream"); + }) + .then(() => { + res.write(buffer); + res.end(); + }); + } else { + res.setHeader("content-type", "application/json"); + res.write(JSON.stringify(reply.result)); + res.end(); + } + } + } catch (error) { + console.error(error); + if (!res.headersSent) { + res.statusCode = 500; } - }); - } catch (error) { - console.error(error); - if (!res.headersSent) { - res.statusCode = 500; - } - res.end(); + res.end(); + } + }); + } catch (error) { + console.error(error); + if (!res.headersSent) { + res.statusCode = 500; } - }); - } + + res.end(); + } + }); } } } @@ -736,13 +745,13 @@ export class SdkgenHttpServer { Request: { args: "json", device: { + fingerprint: "string?", id: "string?", language: "string?", platform: "json?", timezone: "string?", type: "string?", version: "string?", - fingerprint: "string?", }, id: "string", name: "string", @@ -758,21 +767,21 @@ export class SdkgenHttpServer { return { args: parsed.args, deviceInfo: { + fingerprint: parsed.device.fingerprint, id: deviceId, language: parsed.device.language, platform: parsed.device.platform, timezone: parsed.device.timezone, type: parsed.device.type || parsed.device.platform || "", version: parsed.device.version, - fingerprint: parsed.device.fingerprint, }, extra: {}, + files: [], headers: req.headers, id: `${deviceId}-${parsed.id}`, ip, name: parsed.name, version: 1, - files: [], }; } @@ -782,6 +791,7 @@ export class SdkgenHttpServer { { Request: { args: "json", + deviceFingerprint: "string?", deviceId: "string", info: { browserUserAgent: "string?", @@ -792,7 +802,6 @@ export class SdkgenHttpServer { partnerId: "string?", requestId: "string?", sessionId: "string?", - deviceFingerprint: "string?", }, }, "root", @@ -803,6 +812,7 @@ export class SdkgenHttpServer { return { args: parsed.args, deviceInfo: { + fingerprint: parsed.deviceFingerprint, id: parsed.deviceId, language: parsed.info.language, platform: { @@ -811,18 +821,17 @@ export class SdkgenHttpServer { timezone: null, type: parsed.info.type, version: "", - fingerprint: parsed.deviceFingerprint, }, extra: { partnerId: parsed.partnerId, sessionId: parsed.sessionId, }, + files: [], headers: req.headers, id: `${parsed.deviceId}-${parsed.requestId || randomBytes(16).toString("hex")}`, ip, name: parsed.name, version: 2, - files: [], }; } @@ -831,13 +840,13 @@ export class SdkgenHttpServer { const parsed = decode( { DeviceInfo: { + fingerprint: "string?", id: "string?", language: "string?", platform: "json?", timezone: "string?", type: "string?", version: "string?", - fingerprint: "string?", }, Request: { args: "json", @@ -858,21 +867,21 @@ export class SdkgenHttpServer { return { args: parsed.args, deviceInfo: { + fingerprint: deviceInfo.fingerprint, id: deviceId, language: deviceInfo.language, platform: parsed.platform ? { ...parsed.platform } : {}, timezone: deviceInfo.timezone, type: deviceInfo.type || "api", version: deviceInfo.version, - fingerprint: deviceInfo.fingerprint, }, extra: parsed.extra ? { ...parsed.extra } : {}, + files: [], headers: req.headers, id: `${deviceId}-${parsed.requestId || randomBytes(16).toString("hex")}`, ip, name: parsed.name, version: 3, - files: [], }; } diff --git a/node-runtime/src/swagger.ts b/node-runtime/src/swagger.ts index b5cd269e2..bf3b4d977 100644 --- a/node-runtime/src/swagger.ts +++ b/node-runtime/src/swagger.ts @@ -259,16 +259,19 @@ export function setupSwagger(server: SdkgenHttpServer ({ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion arg: op.args.find(arg => arg.name === name)!, location: "path", name, })), ...ann.queryVariables.map(name => ({ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion arg: op.args.find(arg => arg.name === name)!, location: "query", name, })), ...[...ann.headers.entries()].map(([header, name]) => ({ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion arg: op.args.find(arg => arg.name === name)!, location: "header", name: header, @@ -288,7 +291,7 @@ export function setupSwagger(server: SdkgenHttpServer { - const bodyType = op.args.find(arg => arg.name === ann.bodyVariable)!.type; + const bodyType = op.args.find(arg => arg.name === ann.bodyVariable)?.type; return bodyType instanceof BoolPrimitiveType || bodyType instanceof IntPrimitiveType || @@ -313,9 +316,11 @@ export function setupSwagger(server: SdkgenHttpServer arg.name === ann.bodyVariable)!.type), }, }, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion required: !(op.args.find(arg => arg.name === ann.bodyVariable)!.type instanceof OptionalType), } : undefined, From 44d44e406b6df4125dd6efdfb2d01c6ca9a7ddec Mon Sep 17 00:00:00 2001 From: Guilherme Bernal Date: Sun, 18 Oct 2020 23:33:24 +0000 Subject: [PATCH 05/10] feat: add eslint on missing projects --- .github/workflows/test.yml | 10 + browser-runtime/.eslintrc.json | 423 +++++++++++++++++++++++++ browser-runtime/package.json | 7 + browser-runtime/src/encode-decode.ts | 101 ++++-- browser-runtime/src/error.ts | 4 +- browser-runtime/src/http-client.ts | 66 ++-- cli/.eslintrc.json | 423 +++++++++++++++++++++++++ cli/package.json | 7 + cli/src/build.ts | 26 +- cli/src/compatibility.ts | 10 +- cli/src/index.ts | 2 +- csharp-generator/.eslintrc.json | 423 +++++++++++++++++++++++++ csharp-generator/package.json | 7 + csharp-generator/src/csharp-server.ts | 14 +- csharp-generator/src/helpers.ts | 404 ++++++++++++----------- dart-generator/.eslintrc.json | 423 +++++++++++++++++++++++++ dart-generator/package.json | 7 + dart-generator/src/dart-client.ts | 10 +- dart-generator/src/helpers.ts | 49 +-- kotlin-generator/.eslintrc.json | 423 +++++++++++++++++++++++++ kotlin-generator/package.json | 7 + kotlin-generator/src/android-client.ts | 12 +- kotlin-generator/src/helpers.ts | 183 +++++------ node-runtime/src/http-server.ts | 2 +- 24 files changed, 2664 insertions(+), 379 deletions(-) create mode 100644 browser-runtime/.eslintrc.json create mode 100644 cli/.eslintrc.json create mode 100644 csharp-generator/.eslintrc.json create mode 100644 dart-generator/.eslintrc.json create mode 100644 kotlin-generator/.eslintrc.json diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8e71b64a5..7020213af 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,6 +15,8 @@ jobs: working-directory: ./browser-runtime - run: npm run prettier:check working-directory: ./browser-runtime + - run: npm run eslint:check + working-directory: ./browser-runtime - run: npm run build working-directory: ./browser-runtime - run: npm test @@ -34,6 +36,8 @@ jobs: working-directory: ./cli - run: npm run prettier:check working-directory: ./cli + - run: npm run eslint:check + working-directory: ./cli - run: npm run build working-directory: ./cli - run: npm test @@ -53,6 +57,8 @@ jobs: working-directory: ./dart-generator - run: npm run prettier:check working-directory: ./dart-generator + - run: npm run eslint:check + working-directory: ./dart-generator - run: npm run build working-directory: ./dart-generator - run: npm test @@ -185,6 +191,8 @@ jobs: working-directory: ./csharp-generator - run: npm run prettier:check working-directory: ./csharp-generator + - run: npm run eslint:check + working-directory: ./csharp-generator - run: npm run build working-directory: ./csharp-generator - run: npm test @@ -217,6 +225,8 @@ jobs: working-directory: ./kotlin-generator - run: npm run prettier:check working-directory: ./kotlin-generator + - run: npm run eslint:check + working-directory: ./kotlin-generator - run: npm run build working-directory: ./kotlin-generator - run: npm test diff --git a/browser-runtime/.eslintrc.json b/browser-runtime/.eslintrc.json new file mode 100644 index 000000000..9c2b6a1a8 --- /dev/null +++ b/browser-runtime/.eslintrc.json @@ -0,0 +1,423 @@ +{ + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended" + ], + "env": { + "node": true, + "es2017": true, + "jest": true + }, + "globals": { + "BigInt": true + }, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2019, + "project": "tsconfig.json", + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint/eslint-plugin", + "prettier" + ], + "rules": { + "prettier/prettier": "error", + "spaced-comment": [ + "error", + "always" + ], + "@typescript-eslint/array-type": [ + "error", + { + "default": "array-simple" + } + ], + "@typescript-eslint/brace-style": "error", + "@typescript-eslint/consistent-type-definitions": [ + "error", + "interface" + ], + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-var-requires": "off", + "@typescript-eslint/func-call-spacing": [ + "error", + "never" + ], + "@typescript-eslint/interface-name-prefix": "off", + "@typescript-eslint/member-delimiter-style": "off", + "@typescript-eslint/no-empty-interface": "off", + "@typescript-eslint/no-unnecessary-qualifier": "error", + "@typescript-eslint/no-unnecessary-type-arguments": "error", + "@typescript-eslint/no-unused-expressions": "error", + "@typescript-eslint/no-use-before-define": "error", + "@typescript-eslint/no-useless-constructor": "error", + "@typescript-eslint/prefer-for-of": "error", + "@typescript-eslint/promise-function-async": "error", + "@typescript-eslint/require-array-sort-compare": "error", + "@typescript-eslint/restrict-plus-operands": "error", + "@typescript-eslint/semi": [ + "error", + "always", + { + "omitLastInOneLineBlock": false + } + ], + "array-callback-return": "error", + "block-scoped-var": "error", + "consistent-return": "error", + "curly": "error", + "default-case": "error", + "default-param-last": "error", + "dot-notation": "error", + "eqeqeq": [ + "error", + "always" + ], + "global-require": "error", + "guard-for-in": "error", + "no-alert": "error", + "no-buffer-constructor": "error", + "no-caller": "error", + "no-div-regex": "error", + "no-else-return": "error", + "no-empty-function": [ + "error", + { + "allow": [ + "constructors" + ] + } + ], + "no-eq-null": "error", + "no-eval": "error", + "no-extend-native": "error", + "no-extra-bind": "error", + "no-floating-decimal": "error", + "no-implicit-coercion": "error", + "no-implied-eval": "error", + "no-import-assign": "error", + "no-invalid-this": "error", + "no-iterator": "error", + "no-label-var": "error", + "no-labels": "error", + "no-lone-blocks": "error", + "no-loop-func": "error", + "no-multi-spaces": "error", + "no-multi-str": "error", + "no-new-func": "error", + "no-new-wrappers": "error", + "no-new": "error", + "no-octal-escape": "error", + "no-path-concat": "error", + "no-process-exit": "error", + "no-proto": "error", + "no-prototype-builtins": "off", + "no-restricted-modules": [ + "error", + "moment" + ], + "no-return-assign": "error", + "no-return-await": "error", + "no-script-url": "error", + "no-self-compare": "error", + "no-sequences": "error", + "no-shadow": "error", + "no-sync": [ + "error", + { + "allowAtRootLevel": true + } + ], + "no-template-curly-in-string": "error", + "no-throw-literal": "error", + "no-undef-init": "error", + "no-unmodified-loop-condition": "error", + "no-useless-call": "error", + "no-useless-concat": "error", + "no-void": "error", + "prefer-named-capture-group": "warn", + "prefer-regex-literals": "error", + "radix": "error", + "require-unicode-regexp": "error", + "strict": "error", + "wrap-iife": [ + "error", + "any" + ], + "yoda": "error", + "array-bracket-spacing": [ + "error", + "never" + ], + "array-element-newline": [ + "error", + "consistent" + ], + "block-spacing": "error", + "camelcase": "warn", + "capitalized-comments": [ + "error", + "always" + ], + "comma-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "comma-style": [ + "error", + "last" + ], + "computed-property-spacing": [ + "error", + "never" + ], + "eol-last": [ + "error", + "always" + ], + "func-name-matching": [ + "error", + "always" + ], + "func-names": [ + "error", + "as-needed" + ], + "func-style": [ + "error", + "declaration" + ], + "function-call-argument-newline": [ + "error", + "consistent" + ], + "jsx-quotes": [ + "error", + "prefer-double" + ], + "key-spacing": [ + "error", + { + "beforeColon": false + } + ], + "keyword-spacing": [ + "error", + { + "before": true, + "after": true + } + ], + "linebreak-style": [ + "error", + "unix" + ], + "lines-between-class-members": [ + "error", + "always" + ], + "max-depth": [ + "error", + 5 + ], + "max-nested-callbacks": [ + "error", + 10 + ], + "max-params": [ + "error", + 10 + ], + "max-statements-per-line": [ + "error", + { + "max": 2 + } + ], + "multiline-comment-style": [ + "error", + "starred-block" + ], + "new-parens": "error", + "newline-per-chained-call": [ + "error", + { + "ignoreChainWithDepth": 3 + } + ], + "no-array-constructor": "error", + "no-lonely-if": "error", + "no-multi-assign": "error", + "no-multiple-empty-lines": "error", + "no-negated-condition": "error", + "no-new-object": "error", + "no-tabs": "error", + "no-trailing-spaces": "error", + "no-unneeded-ternary": "error", + "no-whitespace-before-property": "error", + "object-curly-spacing": [ + "error", + "always" + ], + "one-var": [ + "error", + "never" + ], + "operator-assignment": [ + "error", + "always" + ], + "padded-blocks": [ + "error", + { + "classes": "never" + } + ], + "padding-line-between-statements": [ + "error", + { + "blankLine": "always", + "prev": [ + "const", + "let" + ], + "next": "*" + }, + { + "blankLine": "any", + "prev": [ + "const", + "let" + ], + "next": [ + "const", + "let", + "var" + ] + }, + { + "blankLine": "always", + "prev": "directive", + "next": "*" + }, + { + "blankLine": "any", + "prev": "directive", + "next": "directive" + }, + { + "blankLine": "always", + "prev": "break", + "next": "*" + }, + { + "blankLine": "always", + "prev": "block-like", + "next": "*" + } + ], + "prefer-object-spread": "error", + "quote-props": [ + "error", + "as-needed" + ], + "semi-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "semi-style": [ + "error", + "last" + ], + "sort-keys": "error", + "space-before-blocks": "error", + "space-in-parens": [ + "error", + "never" + ], + "space-infix-ops": [ + "error", + { + "int32Hint": false + } + ], + "space-unary-ops": "error", + "switch-colon-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "template-tag-spacing": [ + "error", + "never" + ], + "unicode-bom": [ + "error", + "never" + ], + "arrow-parens": [ + "error", + "as-needed" + ], + "arrow-spacing": [ + "error", + { + "before": true, + "after": true + } + ], + "generator-star-spacing": [ + "error", + { + "before": true, + "after": false + } + ], + "no-confusing-arrow": "error", + "no-duplicate-imports": "error", + "no-useless-computed-key": "error", + "no-useless-rename": "error", + "no-var": "error", + "object-shorthand": [ + "error", + "properties" + ], + "prefer-arrow-callback": "error", + "prefer-const": "error", + "prefer-destructuring": "error", + "prefer-numeric-literals": "error", + "prefer-rest-params": "error", + "prefer-spread": "error", + "prefer-template": "error", + "rest-spread-spacing": [ + "error", + "never" + ], + "sort-imports": [ + "error", + { + "ignoreCase": true, + "ignoreDeclarationSort": true + } + ], + "template-curly-spacing": [ + "error", + "never" + ], + "yield-star-spacing": [ + "error", + "before" + ] + } +} diff --git a/browser-runtime/package.json b/browser-runtime/package.json index 198a404ba..9c99ef123 100644 --- a/browser-runtime/package.json +++ b/browser-runtime/package.json @@ -5,6 +5,8 @@ "main": "src/index.ts", "scripts": { "test": "jest --passWithNoTests", + "eslint:fix": "eslint --fix '**/*.ts'", + "eslint:check": "eslint '**/*.ts'", "prettier:fix": "prettier --write '**/*.{t,j}s'", "prettier:check": "prettier --check '**/*.{t,j}s'", "build": "tsc" @@ -25,6 +27,11 @@ "devDependencies": { "@types/jest": "^26.0.14", "@types/node": "^14.11.9", + "@typescript-eslint/eslint-plugin": "^4.4.1", + "@typescript-eslint/parser": "^4.4.1", + "eslint": "^7.11.0", + "eslint-config-prettier": "^6.10.0", + "eslint-plugin-prettier": "^3.1.2", "jest": "^26.5.3", "prettier": "^2.1.2", "ts-jest": "^26.4.1", diff --git a/browser-runtime/src/encode-decode.ts b/browser-runtime/src/encode-decode.ts index 99a72ae0d..ca4747a13 100644 --- a/browser-runtime/src/encode-decode.ts +++ b/browser-runtime/src/encode-decode.ts @@ -21,57 +21,67 @@ function simpleEncodeDecode(path: string, type: string, value: any) { if (type === "json") { if (value === null || value === undefined) { return null; - } else { - return JSON.parse(JSON.stringify(value)); } + + return JSON.parse(JSON.stringify(value)); } else if (type === "bool") { if (typeof value !== "boolean") { throw new ParseError(path, type, value); } + return value; } else if (simpleStringTypes.indexOf(type) >= 0) { if (typeof value !== "string") { throw new ParseError(path, type, value); } + return value; } else if (type === "hex") { - if (typeof value !== "string" || !value.match(/^(?:[A-Fa-f0-9]{2})*$/)) { + if (typeof value !== "string" || !value.match(/^(?:[A-Fa-f0-9]{2})*$/u)) { throw new ParseError(path, type, value); } + return value.toLowerCase(); } else if (type === "uuid") { - if (typeof value !== "string" || !value.match(/^[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}$/)) { + if (typeof value !== "string" || !value.match(/^[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}$/u)) { throw new ParseError(path, type, value); } + return value.toLowerCase(); } else if (type === "base64") { if (typeof value !== "string" || Buffer.from(value, "base64").toString("base64") !== value) { throw new ParseError(path, type, value); } + return value; } else if (type === "int") { if (typeof value !== "number" || (value | 0) !== value) { throw new ParseError(path, type, value); } + return value; } else if (type === "uint") { if (typeof value !== "number" || (value | 0) !== value || value < 0) { throw new ParseError(path, type, value); } + return value; } else if (type === "float") { if (typeof value !== "number") { throw new ParseError(path, type, value); } + return value; } else if (type === "money") { if (typeof value !== "number" || !Number.isInteger(value)) { throw new ParseError(path, type, value); } + return value; } else if (type === "url") { let isValid = typeof value === "string"; - let url: URL; + let url!: URL; + if (isValid) { try { url = new URL(value); @@ -79,41 +89,51 @@ function simpleEncodeDecode(path: string, type: string, value: any) { isValid = false; } } + if (!isValid) { throw new ParseError(path, type, value); } - return url!.toString(); + + return url.toString(); } else if (type === "void") { return null; - } else { - throw new Error(`Unknown type '${type}' at '${path}'`); } + + throw new Error(`Unknown type '${type}' at '${path}'`); } -export function encode(typeTable: TypeTable, path: string, type: TypeDescription, value: any): any { +export function encode(typeTable: TypeTable, path: string, type: TypeDescription, value: unknown): any { if (typeof type === "string" && !type.endsWith("?") && type !== "void" && (value === null || value === undefined)) { throw new Error(`Invalid type at '${path}', cannot be null`); } else if (Array.isArray(type)) { - if (type.indexOf(value) < 0) { + if (typeof value !== "string" || type.indexOf(value) < 0) { throw new ParseError(path, type, value); } + return value; } else if (typeof type === "object") { if (typeof value !== "object" || value === undefined) { throw new ParseError(path, type, value); } + const obj: any = {}; - for (const key in type) { - obj[key] = encode(typeTable, `${path}.${key}`, type[key], value[key]); + + for (const key of Object.keys(type)) { + obj[key] = encode(typeTable, `${path}.${key}`, type[key], (value as Record)[key]); } + return obj; } else if (type.endsWith("?")) { - if (value === null || value === undefined) return null; - else return encode(typeTable, path, type.slice(0, type.length - 1), value); + if (value === null || value === undefined) { + return null; + } + + return encode(typeTable, path, type.slice(0, type.length - 1), value); } else if (type.endsWith("[]")) { if (!Array.isArray(value)) { throw new ParseError(path, type, value); } + return value.map((entry, index) => encode(typeTable, `${path}[${index}]`, type.slice(0, type.length - 2), entry)); } else if (simpleTypes.indexOf(type) >= 0) { return simpleEncodeDecode(path, type, value); @@ -121,72 +141,87 @@ export function encode(typeTable: TypeTable, path: string, type: TypeDescription if (!(value instanceof Buffer)) { throw new ParseError(path, type, value); } + return value.toString("base64"); } else if (type === "bigint") { if (!(typeof value === "bigint")) { throw new ParseError(path, type, value); } + return value.toString(); } else if (type === "cpf") { if (typeof value !== "string") { throw new ParseError(path, type, value); } + return value; } else if (type === "cnpj") { if (typeof value !== "string") { throw new ParseError(path, type, value); } + return value; } else if (type === "date") { - if (!(value instanceof Date) && !(typeof value === "string" && value.match(/^[0-9]{4}-[01][0-9]-[0123][0-9]$/))) { + if (!(value instanceof Date) && !(typeof value === "string" && value.match(/^[0-9]{4}-[01][0-9]-[0123][0-9]$/u))) { throw new ParseError(path, type, value); } + return typeof value === "string" ? new Date(value).toISOString().split("T")[0] : value.toISOString().split("T")[0]; } else if (type === "datetime") { if ( !(value instanceof Date) && !( typeof value === "string" && - value.match(/^[0-9]{4}-[01][0-9]-[0123][0-9]T[012][0-9]:[0123456][0-9]:[0123456][0-9](?:\.[0-9]{1,6})?(?:Z|[+-][012][0-9]:[0123456][0-9])?$/) + value.match(/^[0-9]{4}-[01][0-9]-[0123][0-9]T[012][0-9]:[0123456][0-9]:[0123456][0-9](?:\.[0-9]{1,6})?(?:Z|[+-][012][0-9]:[0123456][0-9])?$/u) ) ) { throw new ParseError(path, type, value); } + return (typeof value === "string" ? new Date(value) : value).toISOString().replace("Z", ""); } else { const resolved = typeTable[type]; + if (resolved) { return encode(typeTable, path, resolved, value); - } else { - throw new Error(`Unknown type '${type}' at '${path}'`); } + + throw new Error(`Unknown type '${type}' at '${path}'`); } } -export function decode(typeTable: TypeTable, path: string, type: TypeDescription, value: any): any { +export function decode(typeTable: TypeTable, path: string, type: TypeDescription, value: unknown): any { if (typeof type === "string" && !type.endsWith("?") && type !== "void" && (value === null || value === undefined)) { throw new Error(`Invalid type at '${path}', cannot be null`); } else if (Array.isArray(type)) { - if (type.indexOf(value) < 0) { + if (typeof value !== "string" || type.indexOf(value) < 0) { throw new ParseError(path, type, value); } + return value; } else if (typeof type === "object") { if (typeof value !== "object" || value === undefined) { throw new ParseError(path, type, value); } + const obj: any = {}; - for (const key in type) { - obj[key] = decode(typeTable, `${path}.${key}`, type[key], value[key]); + + for (const key of Object.keys(type)) { + obj[key] = decode(typeTable, `${path}.${key}`, type[key], (value as Record)[key]); } + return obj; } else if (type.endsWith("?")) { - if (value === null || value === undefined) return null; - else return decode(typeTable, path, type.slice(0, type.length - 1), value); + if (value === null || value === undefined) { + return null; + } + + return decode(typeTable, path, type.slice(0, type.length - 1), value); } else if (type.endsWith("[]")) { if (!Array.isArray(value)) { throw new ParseError(path, type, value); } + return value.map((entry, index) => decode(typeTable, `${path}[${index}]`, type.slice(0, type.length - 2), entry)); } else if (simpleTypes.indexOf(type) >= 0) { return simpleEncodeDecode(path, type, value); @@ -194,28 +229,34 @@ export function decode(typeTable: TypeTable, path: string, type: TypeDescription if (typeof value !== "string") { throw new ParseError(path, `${type} (base 64)`, value); } + const buffer = Buffer.from(value, "base64"); + if (buffer.toString("base64") !== value) { throw new ParseError(path, `${type} (base 64)`, value); } + return buffer; } else if (type === "bigint") { - if (typeof value !== "number" && (typeof value !== "string" || !value.match(/^-?[0-9]+$/))) { + if (typeof value !== "number" && (typeof value !== "string" || !value.match(/^-?[0-9]+$/u))) { throw new ParseError(path, type, value); } + return BigInt(value); } else if (type === "cpf") { if (typeof value !== "string") { throw new ParseError(path, type, value); } + return value; } else if (type === "cnpj") { if (typeof value !== "string") { throw new ParseError(path, type, value); } + return value; } else if (type === "date") { - if (typeof value !== "string" || !value.match(/^[0-9]{4}-[01][0-9]-[0123][0-9]$/)) { + if (typeof value !== "string" || !value.match(/^[0-9]{4}-[01][0-9]-[0123][0-9]$/u)) { throw new ParseError(path, type, value); } @@ -230,16 +271,18 @@ export function decode(typeTable: TypeTable, path: string, type: TypeDescription return date; } else if (type === "datetime") { - if (typeof value !== "string" || !value.match(/^[0-9]{4}-[01][0-9]-[0123][0-9]T[012][0-9]:[0123456][0-9]:[0123456][0-9](\.[0-9]{1,6})?Z?$/)) { + if (typeof value !== "string" || !value.match(/^[0-9]{4}-[01][0-9]-[0123][0-9]T[012][0-9]:[0123456][0-9]:[0123456][0-9](?:\.[0-9]{1,6})?Z?$/u)) { throw new ParseError(path, type, value); } + return new Date(`${value.endsWith("Z") ? value : value.concat("Z")}`); } else { const resolved = typeTable[type]; + if (resolved) { return decode(typeTable, path, resolved, value); - } else { - throw new Error(`Unknown type '${type}' at '${path}'`); } + + throw new Error(`Unknown type '${type}' at '${path}'`); } } diff --git a/browser-runtime/src/error.ts b/browser-runtime/src/error.ts index a821f2507..87cb9c71d 100644 --- a/browser-runtime/src/error.ts +++ b/browser-runtime/src/error.ts @@ -4,10 +4,10 @@ export abstract class SdkgenError extends Error { this.name = this.constructor.name; } - public toJSON() { + public toJSON(): { message: string; type: string } { return { - type: this.constructor.name, message: this.message, + type: this.constructor.name, }; } } diff --git a/browser-runtime/src/http-client.ts b/browser-runtime/src/http-client.ts index bf7486a83..a511c8add 100644 --- a/browser-runtime/src/http-client.ts +++ b/browser-runtime/src/http-client.ts @@ -7,70 +7,88 @@ interface ErrClasses { function randomBytesHex(len: number) { let hex = ""; - for (let i = 0; i < 2 * len; ++i) hex += "0123456789abcdef"[Math.floor(Math.random() * 16)]; + + for (let i = 0; i < 2 * len; ++i) { + hex += "0123456789abcdef"[Math.floor(Math.random() * 16)]; + } + return hex; } -let fallbackDeviceId = randomBytesHex(20); +const fallbackDeviceId = randomBytesHex(20); function getDeviceId() { try { let deviceId = localStorage.getItem("deviceId"); + if (!deviceId) { deviceId = fallbackDeviceId; localStorage.setItem("deviceId", deviceId); } + return deviceId; - } catch (e) {} + } catch (e) { + // + } + return fallbackDeviceId; } export class SdkgenHttpClient { private baseUrl: string; + extra = new Map(); - successHook: (result: any, name: string, args: any) => void = () => {}; - errorHook: (result: any, name: string, args: any) => void = () => {}; + successHook: (result: any, name: string, args: any) => void = () => undefined; + + errorHook: (result: any, name: string, args: any) => void = () => undefined; constructor(baseUrl: string, private astJson: AstJson, private errClasses: ErrClasses) { this.baseUrl = baseUrl; } - async makeRequest(functionName: string, args: any) { + async makeRequest(functionName: string, args: unknown): Promise { const func = this.astJson.functionTable[functionName]; + if (!func) { throw new Error(`Unknown function ${functionName}`); } + const thisScript = document.currentScript as HTMLScriptElement; const request = { - version: 3, - requestId: randomBytesHex(16), - name: functionName, args: encode(this.astJson.typeTable, `${functionName}.args`, func.args, args), - extra: { - ...this.extra, - }, deviceInfo: { id: getDeviceId(), - type: "web", - version: thisScript ? thisScript.src : "", language: navigator.language, platform: { browserUserAgent: navigator.userAgent, }, timezone: typeof Intl === "object" ? Intl.DateTimeFormat().resolvedOptions().timeZone : null, + type: "web", + version: thisScript ? thisScript.src : "", }, + extra: { + ...this.extra, + }, + name: functionName, + requestId: randomBytesHex(16), + version: 3, }; const encodedRet = await new Promise((resolve, reject) => { const req = new XMLHttpRequest(); - req.open("POST", this.baseUrl + "/" + functionName); + + req.open("POST", `${this.baseUrl}/${functionName}`); req.onreadystatechange = () => { - if (req.readyState !== 4) return; + if (req.readyState !== 4) { + return; + } + try { const response = JSON.parse(req.responseText); + try { if (response.error) { reject(response.error); @@ -80,25 +98,33 @@ export class SdkgenHttpClient { } } catch (e) { console.error(e); - const err = { type: "Fatal", message: e.toString() }; + const err = { message: e.toString(), type: "Fatal" }; + reject(err); this.errorHook(err, functionName, args); } } catch (e) { console.error(e); - const err = { type: "Fatal", message: `Falha de conexão com o servidor` }; + const err = { message: `Falha de conexão com o servidor`, type: "Fatal" }; + reject(err); this.errorHook(err, functionName, args); } }; + req.send(JSON.stringify(request)); }).catch(err => { const errClass = this.errClasses[err.type]; - if (errClass) throw new errClass(err.message); - else throw err; + + if (errClass) { + throw new errClass(err.message); + } else { + throw err; + } }); const ret = decode(this.astJson.typeTable, `${functionName}.ret`, func.ret, encodedRet); + this.successHook(ret, functionName, args); return ret; } diff --git a/cli/.eslintrc.json b/cli/.eslintrc.json new file mode 100644 index 000000000..9c2b6a1a8 --- /dev/null +++ b/cli/.eslintrc.json @@ -0,0 +1,423 @@ +{ + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended" + ], + "env": { + "node": true, + "es2017": true, + "jest": true + }, + "globals": { + "BigInt": true + }, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2019, + "project": "tsconfig.json", + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint/eslint-plugin", + "prettier" + ], + "rules": { + "prettier/prettier": "error", + "spaced-comment": [ + "error", + "always" + ], + "@typescript-eslint/array-type": [ + "error", + { + "default": "array-simple" + } + ], + "@typescript-eslint/brace-style": "error", + "@typescript-eslint/consistent-type-definitions": [ + "error", + "interface" + ], + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-var-requires": "off", + "@typescript-eslint/func-call-spacing": [ + "error", + "never" + ], + "@typescript-eslint/interface-name-prefix": "off", + "@typescript-eslint/member-delimiter-style": "off", + "@typescript-eslint/no-empty-interface": "off", + "@typescript-eslint/no-unnecessary-qualifier": "error", + "@typescript-eslint/no-unnecessary-type-arguments": "error", + "@typescript-eslint/no-unused-expressions": "error", + "@typescript-eslint/no-use-before-define": "error", + "@typescript-eslint/no-useless-constructor": "error", + "@typescript-eslint/prefer-for-of": "error", + "@typescript-eslint/promise-function-async": "error", + "@typescript-eslint/require-array-sort-compare": "error", + "@typescript-eslint/restrict-plus-operands": "error", + "@typescript-eslint/semi": [ + "error", + "always", + { + "omitLastInOneLineBlock": false + } + ], + "array-callback-return": "error", + "block-scoped-var": "error", + "consistent-return": "error", + "curly": "error", + "default-case": "error", + "default-param-last": "error", + "dot-notation": "error", + "eqeqeq": [ + "error", + "always" + ], + "global-require": "error", + "guard-for-in": "error", + "no-alert": "error", + "no-buffer-constructor": "error", + "no-caller": "error", + "no-div-regex": "error", + "no-else-return": "error", + "no-empty-function": [ + "error", + { + "allow": [ + "constructors" + ] + } + ], + "no-eq-null": "error", + "no-eval": "error", + "no-extend-native": "error", + "no-extra-bind": "error", + "no-floating-decimal": "error", + "no-implicit-coercion": "error", + "no-implied-eval": "error", + "no-import-assign": "error", + "no-invalid-this": "error", + "no-iterator": "error", + "no-label-var": "error", + "no-labels": "error", + "no-lone-blocks": "error", + "no-loop-func": "error", + "no-multi-spaces": "error", + "no-multi-str": "error", + "no-new-func": "error", + "no-new-wrappers": "error", + "no-new": "error", + "no-octal-escape": "error", + "no-path-concat": "error", + "no-process-exit": "error", + "no-proto": "error", + "no-prototype-builtins": "off", + "no-restricted-modules": [ + "error", + "moment" + ], + "no-return-assign": "error", + "no-return-await": "error", + "no-script-url": "error", + "no-self-compare": "error", + "no-sequences": "error", + "no-shadow": "error", + "no-sync": [ + "error", + { + "allowAtRootLevel": true + } + ], + "no-template-curly-in-string": "error", + "no-throw-literal": "error", + "no-undef-init": "error", + "no-unmodified-loop-condition": "error", + "no-useless-call": "error", + "no-useless-concat": "error", + "no-void": "error", + "prefer-named-capture-group": "warn", + "prefer-regex-literals": "error", + "radix": "error", + "require-unicode-regexp": "error", + "strict": "error", + "wrap-iife": [ + "error", + "any" + ], + "yoda": "error", + "array-bracket-spacing": [ + "error", + "never" + ], + "array-element-newline": [ + "error", + "consistent" + ], + "block-spacing": "error", + "camelcase": "warn", + "capitalized-comments": [ + "error", + "always" + ], + "comma-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "comma-style": [ + "error", + "last" + ], + "computed-property-spacing": [ + "error", + "never" + ], + "eol-last": [ + "error", + "always" + ], + "func-name-matching": [ + "error", + "always" + ], + "func-names": [ + "error", + "as-needed" + ], + "func-style": [ + "error", + "declaration" + ], + "function-call-argument-newline": [ + "error", + "consistent" + ], + "jsx-quotes": [ + "error", + "prefer-double" + ], + "key-spacing": [ + "error", + { + "beforeColon": false + } + ], + "keyword-spacing": [ + "error", + { + "before": true, + "after": true + } + ], + "linebreak-style": [ + "error", + "unix" + ], + "lines-between-class-members": [ + "error", + "always" + ], + "max-depth": [ + "error", + 5 + ], + "max-nested-callbacks": [ + "error", + 10 + ], + "max-params": [ + "error", + 10 + ], + "max-statements-per-line": [ + "error", + { + "max": 2 + } + ], + "multiline-comment-style": [ + "error", + "starred-block" + ], + "new-parens": "error", + "newline-per-chained-call": [ + "error", + { + "ignoreChainWithDepth": 3 + } + ], + "no-array-constructor": "error", + "no-lonely-if": "error", + "no-multi-assign": "error", + "no-multiple-empty-lines": "error", + "no-negated-condition": "error", + "no-new-object": "error", + "no-tabs": "error", + "no-trailing-spaces": "error", + "no-unneeded-ternary": "error", + "no-whitespace-before-property": "error", + "object-curly-spacing": [ + "error", + "always" + ], + "one-var": [ + "error", + "never" + ], + "operator-assignment": [ + "error", + "always" + ], + "padded-blocks": [ + "error", + { + "classes": "never" + } + ], + "padding-line-between-statements": [ + "error", + { + "blankLine": "always", + "prev": [ + "const", + "let" + ], + "next": "*" + }, + { + "blankLine": "any", + "prev": [ + "const", + "let" + ], + "next": [ + "const", + "let", + "var" + ] + }, + { + "blankLine": "always", + "prev": "directive", + "next": "*" + }, + { + "blankLine": "any", + "prev": "directive", + "next": "directive" + }, + { + "blankLine": "always", + "prev": "break", + "next": "*" + }, + { + "blankLine": "always", + "prev": "block-like", + "next": "*" + } + ], + "prefer-object-spread": "error", + "quote-props": [ + "error", + "as-needed" + ], + "semi-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "semi-style": [ + "error", + "last" + ], + "sort-keys": "error", + "space-before-blocks": "error", + "space-in-parens": [ + "error", + "never" + ], + "space-infix-ops": [ + "error", + { + "int32Hint": false + } + ], + "space-unary-ops": "error", + "switch-colon-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "template-tag-spacing": [ + "error", + "never" + ], + "unicode-bom": [ + "error", + "never" + ], + "arrow-parens": [ + "error", + "as-needed" + ], + "arrow-spacing": [ + "error", + { + "before": true, + "after": true + } + ], + "generator-star-spacing": [ + "error", + { + "before": true, + "after": false + } + ], + "no-confusing-arrow": "error", + "no-duplicate-imports": "error", + "no-useless-computed-key": "error", + "no-useless-rename": "error", + "no-var": "error", + "object-shorthand": [ + "error", + "properties" + ], + "prefer-arrow-callback": "error", + "prefer-const": "error", + "prefer-destructuring": "error", + "prefer-numeric-literals": "error", + "prefer-rest-params": "error", + "prefer-spread": "error", + "prefer-template": "error", + "rest-spread-spacing": [ + "error", + "never" + ], + "sort-imports": [ + "error", + { + "ignoreCase": true, + "ignoreDeclarationSort": true + } + ], + "template-curly-spacing": [ + "error", + "never" + ], + "yield-star-spacing": [ + "error", + "before" + ] + } +} diff --git a/cli/package.json b/cli/package.json index de9be2b23..c5ab6ceb4 100644 --- a/cli/package.json +++ b/cli/package.json @@ -7,6 +7,8 @@ }, "scripts": { "test": "jest --passWithNoTests", + "eslint:fix": "eslint --fix '**/*.ts'", + "eslint:check": "eslint '**/*.ts'", "prettier:fix": "prettier --write '**/*.{t,j}s'", "prettier:check": "prettier --check '**/*.{t,j}s'", "build": "tsc" @@ -29,6 +31,11 @@ "@types/command-line-usage": "^5.0.1", "@types/jest": "^26.0.14", "@types/node": "^14.11.9", + "@typescript-eslint/eslint-plugin": "^4.4.1", + "@typescript-eslint/parser": "^4.4.1", + "eslint": "^7.11.0", + "eslint-config-prettier": "^6.10.0", + "eslint-plugin-prettier": "^3.1.2", "jest": "^26.5.3", "prettier": "^2.1.2", "ts-jest": "^26.4.1", diff --git a/cli/src/build.ts b/cli/src/build.ts index 3e564b26f..a4fa890d5 100644 --- a/cli/src/build.ts +++ b/cli/src/build.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-process-exit */ import { generateCSharpServerSource } from "@sdkgen/csharp-generator"; import { generateDartClientSource } from "@sdkgen/dart-generator"; import { generateAndroidClientSource } from "@sdkgen/kotlin-generator"; @@ -13,13 +14,13 @@ import commandLineUsage from "command-line-usage"; import { writeFileSync } from "fs"; const optionDefinitions = [ - { name: "source", defaultOption: true, description: "Specifies the source file" }, - { name: "output", alias: "o", description: "Specifies the output file" }, - { name: "target", alias: "t", description: "Specifies the target platform and language" }, - { name: "help", alias: "h", type: Boolean, description: "Display this usage guide." }, + { defaultOption: true, description: "Specifies the source file", name: "source" }, + { alias: "o", description: "Specifies the output file", name: "output" }, + { alias: "t", description: "Specifies the target platform and language", name: "target" }, + { alias: "h", description: "Display this usage guide.", name: "help", type: Boolean }, ]; -export function buildCmd(argv: string[]) { +export function buildCmd(argv: string[]): void { const options: { source?: string; output?: string; @@ -32,8 +33,8 @@ export function buildCmd(argv: string[]) { console.log( commandLineUsage([ { - header: "Typical Example", content: "sdkgen src/api.sdkgen -o src/api.ts -t typescript_nodeserver", + header: "Typical Example", }, { header: "Options", @@ -73,30 +74,37 @@ export function buildCmd(argv: string[]) { writeFileSync(options.output, generateNodeServerSource(ast)); break; } + case "typescript_nodeclient": { writeFileSync(options.output, generateNodeClientSource(ast)); break; } + case "typescript_web": { writeFileSync(options.output, generateBrowserClientSource(ast)); break; } + case "typescript_interfaces": { writeFileSync(options.output, generateTypescriptInterfaces(ast)); break; } + case "flutter": { - writeFileSync(options.output, generateDartClientSource(ast, {})); + writeFileSync(options.output, generateDartClientSource(ast)); break; } + case "csharp_server": { - writeFileSync(options.output, generateCSharpServerSource(ast, {})); + writeFileSync(options.output, generateCSharpServerSource(ast)); break; } + case "kotlin_android": { - writeFileSync(options.output, generateAndroidClientSource(ast, {})); + writeFileSync(options.output, generateAndroidClientSource(ast)); break; } + default: { console.error(`Error: Unknown target '${options.target}'`); process.exit(1); diff --git a/cli/src/compatibility.ts b/cli/src/compatibility.ts index 013ff7749..a43b9a67a 100644 --- a/cli/src/compatibility.ts +++ b/cli/src/compatibility.ts @@ -1,27 +1,29 @@ +/* eslint-disable no-process-exit */ import { compatibilityIssues, Parser } from "@sdkgen/parser"; -export function compatibilityCmd(argv: string[]) { +export function compatibilityCmd(argv: string[]): void { const [source1, source2] = argv; if (!source1 || !source2) { console.error("Error: Need to specify two source files."); process.exit(1); - return; } if (argv.length > 2) { console.error("Error: Too many arguments."); process.exit(1); - return; } const ast1 = new Parser(source1).parse(); const ast2 = new Parser(source2).parse(); const issues = compatibilityIssues(ast1, ast2); + for (const issue of issues) { console.log(issue); } - if (issues.length) process.exit(1); + if (issues.length) { + process.exit(1); + } } diff --git a/cli/src/index.ts b/cli/src/index.ts index 40580ea24..e79dc6ce5 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -3,7 +3,7 @@ import commandLineArgs from "command-line-args"; import { buildCmd } from "./build"; import { compatibilityCmd } from "./compatibility"; -const mainDefinitions = [{ name: "command", defaultOption: true }]; +const mainDefinitions = [{ defaultOption: true, name: "command" }]; const mainOptions = commandLineArgs(mainDefinitions, { stopAtFirstUnknown: true }); const argv = mainOptions._unknown || []; diff --git a/csharp-generator/.eslintrc.json b/csharp-generator/.eslintrc.json new file mode 100644 index 000000000..9c2b6a1a8 --- /dev/null +++ b/csharp-generator/.eslintrc.json @@ -0,0 +1,423 @@ +{ + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended" + ], + "env": { + "node": true, + "es2017": true, + "jest": true + }, + "globals": { + "BigInt": true + }, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2019, + "project": "tsconfig.json", + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint/eslint-plugin", + "prettier" + ], + "rules": { + "prettier/prettier": "error", + "spaced-comment": [ + "error", + "always" + ], + "@typescript-eslint/array-type": [ + "error", + { + "default": "array-simple" + } + ], + "@typescript-eslint/brace-style": "error", + "@typescript-eslint/consistent-type-definitions": [ + "error", + "interface" + ], + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-var-requires": "off", + "@typescript-eslint/func-call-spacing": [ + "error", + "never" + ], + "@typescript-eslint/interface-name-prefix": "off", + "@typescript-eslint/member-delimiter-style": "off", + "@typescript-eslint/no-empty-interface": "off", + "@typescript-eslint/no-unnecessary-qualifier": "error", + "@typescript-eslint/no-unnecessary-type-arguments": "error", + "@typescript-eslint/no-unused-expressions": "error", + "@typescript-eslint/no-use-before-define": "error", + "@typescript-eslint/no-useless-constructor": "error", + "@typescript-eslint/prefer-for-of": "error", + "@typescript-eslint/promise-function-async": "error", + "@typescript-eslint/require-array-sort-compare": "error", + "@typescript-eslint/restrict-plus-operands": "error", + "@typescript-eslint/semi": [ + "error", + "always", + { + "omitLastInOneLineBlock": false + } + ], + "array-callback-return": "error", + "block-scoped-var": "error", + "consistent-return": "error", + "curly": "error", + "default-case": "error", + "default-param-last": "error", + "dot-notation": "error", + "eqeqeq": [ + "error", + "always" + ], + "global-require": "error", + "guard-for-in": "error", + "no-alert": "error", + "no-buffer-constructor": "error", + "no-caller": "error", + "no-div-regex": "error", + "no-else-return": "error", + "no-empty-function": [ + "error", + { + "allow": [ + "constructors" + ] + } + ], + "no-eq-null": "error", + "no-eval": "error", + "no-extend-native": "error", + "no-extra-bind": "error", + "no-floating-decimal": "error", + "no-implicit-coercion": "error", + "no-implied-eval": "error", + "no-import-assign": "error", + "no-invalid-this": "error", + "no-iterator": "error", + "no-label-var": "error", + "no-labels": "error", + "no-lone-blocks": "error", + "no-loop-func": "error", + "no-multi-spaces": "error", + "no-multi-str": "error", + "no-new-func": "error", + "no-new-wrappers": "error", + "no-new": "error", + "no-octal-escape": "error", + "no-path-concat": "error", + "no-process-exit": "error", + "no-proto": "error", + "no-prototype-builtins": "off", + "no-restricted-modules": [ + "error", + "moment" + ], + "no-return-assign": "error", + "no-return-await": "error", + "no-script-url": "error", + "no-self-compare": "error", + "no-sequences": "error", + "no-shadow": "error", + "no-sync": [ + "error", + { + "allowAtRootLevel": true + } + ], + "no-template-curly-in-string": "error", + "no-throw-literal": "error", + "no-undef-init": "error", + "no-unmodified-loop-condition": "error", + "no-useless-call": "error", + "no-useless-concat": "error", + "no-void": "error", + "prefer-named-capture-group": "warn", + "prefer-regex-literals": "error", + "radix": "error", + "require-unicode-regexp": "error", + "strict": "error", + "wrap-iife": [ + "error", + "any" + ], + "yoda": "error", + "array-bracket-spacing": [ + "error", + "never" + ], + "array-element-newline": [ + "error", + "consistent" + ], + "block-spacing": "error", + "camelcase": "warn", + "capitalized-comments": [ + "error", + "always" + ], + "comma-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "comma-style": [ + "error", + "last" + ], + "computed-property-spacing": [ + "error", + "never" + ], + "eol-last": [ + "error", + "always" + ], + "func-name-matching": [ + "error", + "always" + ], + "func-names": [ + "error", + "as-needed" + ], + "func-style": [ + "error", + "declaration" + ], + "function-call-argument-newline": [ + "error", + "consistent" + ], + "jsx-quotes": [ + "error", + "prefer-double" + ], + "key-spacing": [ + "error", + { + "beforeColon": false + } + ], + "keyword-spacing": [ + "error", + { + "before": true, + "after": true + } + ], + "linebreak-style": [ + "error", + "unix" + ], + "lines-between-class-members": [ + "error", + "always" + ], + "max-depth": [ + "error", + 5 + ], + "max-nested-callbacks": [ + "error", + 10 + ], + "max-params": [ + "error", + 10 + ], + "max-statements-per-line": [ + "error", + { + "max": 2 + } + ], + "multiline-comment-style": [ + "error", + "starred-block" + ], + "new-parens": "error", + "newline-per-chained-call": [ + "error", + { + "ignoreChainWithDepth": 3 + } + ], + "no-array-constructor": "error", + "no-lonely-if": "error", + "no-multi-assign": "error", + "no-multiple-empty-lines": "error", + "no-negated-condition": "error", + "no-new-object": "error", + "no-tabs": "error", + "no-trailing-spaces": "error", + "no-unneeded-ternary": "error", + "no-whitespace-before-property": "error", + "object-curly-spacing": [ + "error", + "always" + ], + "one-var": [ + "error", + "never" + ], + "operator-assignment": [ + "error", + "always" + ], + "padded-blocks": [ + "error", + { + "classes": "never" + } + ], + "padding-line-between-statements": [ + "error", + { + "blankLine": "always", + "prev": [ + "const", + "let" + ], + "next": "*" + }, + { + "blankLine": "any", + "prev": [ + "const", + "let" + ], + "next": [ + "const", + "let", + "var" + ] + }, + { + "blankLine": "always", + "prev": "directive", + "next": "*" + }, + { + "blankLine": "any", + "prev": "directive", + "next": "directive" + }, + { + "blankLine": "always", + "prev": "break", + "next": "*" + }, + { + "blankLine": "always", + "prev": "block-like", + "next": "*" + } + ], + "prefer-object-spread": "error", + "quote-props": [ + "error", + "as-needed" + ], + "semi-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "semi-style": [ + "error", + "last" + ], + "sort-keys": "error", + "space-before-blocks": "error", + "space-in-parens": [ + "error", + "never" + ], + "space-infix-ops": [ + "error", + { + "int32Hint": false + } + ], + "space-unary-ops": "error", + "switch-colon-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "template-tag-spacing": [ + "error", + "never" + ], + "unicode-bom": [ + "error", + "never" + ], + "arrow-parens": [ + "error", + "as-needed" + ], + "arrow-spacing": [ + "error", + { + "before": true, + "after": true + } + ], + "generator-star-spacing": [ + "error", + { + "before": true, + "after": false + } + ], + "no-confusing-arrow": "error", + "no-duplicate-imports": "error", + "no-useless-computed-key": "error", + "no-useless-rename": "error", + "no-var": "error", + "object-shorthand": [ + "error", + "properties" + ], + "prefer-arrow-callback": "error", + "prefer-const": "error", + "prefer-destructuring": "error", + "prefer-numeric-literals": "error", + "prefer-rest-params": "error", + "prefer-spread": "error", + "prefer-template": "error", + "rest-spread-spacing": [ + "error", + "never" + ], + "sort-imports": [ + "error", + { + "ignoreCase": true, + "ignoreDeclarationSort": true + } + ], + "template-curly-spacing": [ + "error", + "never" + ], + "yield-star-spacing": [ + "error", + "before" + ] + } +} diff --git a/csharp-generator/package.json b/csharp-generator/package.json index d7eb93384..e2364d1e0 100644 --- a/csharp-generator/package.json +++ b/csharp-generator/package.json @@ -5,6 +5,8 @@ "main": "src/index.ts", "scripts": { "test": "jest --passWithNoTests", + "eslint:fix": "eslint --fix '**/*.ts'", + "eslint:check": "eslint '**/*.ts'", "prettier:fix": "prettier --write '**/*.{t,j}s'", "prettier:check": "prettier --check '**/*.{t,j}s'", "build": "tsc" @@ -25,6 +27,11 @@ "devDependencies": { "@types/jest": "^26.0.14", "@types/node": "^14.11.9", + "@typescript-eslint/eslint-plugin": "^4.4.1", + "@typescript-eslint/parser": "^4.4.1", + "eslint": "^7.11.0", + "eslint-config-prettier": "^6.10.0", + "eslint-plugin-prettier": "^3.1.2", "jest": "^26.5.3", "prettier": "^2.1.2", "ts-jest": "^26.4.1", diff --git a/csharp-generator/src/csharp-server.ts b/csharp-generator/src/csharp-server.ts index abec70f79..8e6613374 100644 --- a/csharp-generator/src/csharp-server.ts +++ b/csharp-generator/src/csharp-server.ts @@ -1,9 +1,7 @@ import { AstRoot, astToJson, OptionalType, VoidPrimitiveType } from "@sdkgen/parser"; import { capitalize, decodeType, encodeType, generateEnum, generateStruct, generateTypeName, ident } from "./helpers"; -interface Options {} - -export function generateCSharpServerSource(ast: AstRoot, options: Options) { +export function generateCSharpServerSource(ast: AstRoot): string { let code = `using System; using System.Collections.Generic; using System.Globalization; @@ -16,8 +14,10 @@ namespace SdkgenGenerated { public abstract class Api : BaseApi {`; + for (const op of ast.operations) { const returnTypeAngle = op.returnType instanceof VoidPrimitiveType ? "" : `<${generateTypeName(op.returnType)}>`; + code += ` public virtual Task${returnTypeAngle} ${capitalize(op.name)}(${[ "Context ctx", @@ -28,6 +28,7 @@ namespace SdkgenGenerated } `; } + code += ` public async Task ExecuteFunction(Context context_, Utf8JsonWriter resultWriter_) { @@ -51,10 +52,11 @@ namespace SdkgenGenerated } ${generateTypeName(arg.type)} ${ident(arg.name)}; ${decodeType(arg.type, `${arg.name}Json_`, `"${op.name}().args.${arg.name}"`, ident(arg.name)).replace( - /\n/g, + /\n/gu, "\n ", )}`; } + if (op.returnType instanceof VoidPrimitiveType) { code += ` await ${capitalize(op.name)}(${["context_", ...op.args.map(arg => ident(arg.name))].join(", ")}); @@ -62,7 +64,7 @@ namespace SdkgenGenerated } else { code += ` var result_ = await ${capitalize(op.name)}(${["context_", ...op.args.map(arg => ident(arg.name))].join(", ")}); - ${encodeType(op.returnType, `result_`, `"${op.name}().ret"`).replace(/\n/g, "\n ")}`; + ${encodeType(op.returnType, `result_`, `"${op.name}().ret"`).replace(/\n/gu, "\n ")}`; } code += ` @@ -88,7 +90,7 @@ namespace SdkgenGenerated } code += ` - public string GetAstJson() => @"${JSON.stringify(astToJson(ast), null, 4).replace(/"/g, '""').replace(/\n/g, "\n ")}"; + public string GetAstJson() => @"${JSON.stringify(astToJson(ast), null, 4).replace(/"/gu, '""').replace(/\n/gu, "\n ")}"; } `; for (const error of ast.errors) { diff --git a/csharp-generator/src/helpers.ts b/csharp-generator/src/helpers.ts index 11d49c0f4..048d05a36 100644 --- a/csharp-generator/src/helpers.ts +++ b/csharp-generator/src/helpers.ts @@ -136,111 +136,62 @@ const needsTempVarForNullable: any[] = [ UIntPrimitiveType, ]; -export function ident(name: string) { +export function ident(name: string): string { return reservedWords.includes(name) ? `@${name}` : name; } -export function capitalize(name: string) { +export function capitalize(name: string): string { return name[0].toUpperCase() + name.slice(1); } -export function generateStruct(struct: StructType) { - return ` - public class ${struct.name} - {${struct.fields - .map( - field => ` - public ${generateTypeName(field.type)} ${capitalize(field.name)};`, - ) - .join("")} - public ${struct.name}(${struct.fields.map(field => `${generateTypeName(field.type)} ${ident(field.name)}`).join(", ")}) - {${struct.fields - .map( - field => ` - ${capitalize(field.name)} = ${ident(field.name)};`, - ) - .join("")} - } - } - - ${struct.name} Decode${struct.name}(JsonElement json_, string path_) - { - if (json_.ValueKind != JsonValueKind.Object) - { - throw new FatalException($"'{path_}' must be an object."); - }\n${struct.fields - .map( - field => ` JsonElement ${field.name}Json_; - if (!json_.TryGetProperty(${JSON.stringify(field.name)}, out ${field.name}Json_)) - { - ${ - field.type instanceof OptionalType - ? `${field.name}Json_ = new JsonElement();` - : `throw new FatalException($"'{path_}.${field.name}' must be set to a value of type ${field.type.name}.");` - } - } - ${generateTypeName(field.type)} ${ident(field.name)}; - ${decodeType(field.type, `${field.name}Json_`, `$"{path_}.${field.name}"`, ident(field.name)).replace(/\n/g, "\n ")}`, - ) - .join("\n")} - return new ${struct.name}(${struct.fields.map(field => ident(field.name)).join(", ")}); - } - - void Encode${struct.name}(${struct.name} obj_, Utf8JsonWriter resultWriter_, string path_) - { - resultWriter_.WriteStartObject(); - ${struct.fields - .map( - field => `resultWriter_.WritePropertyName(${JSON.stringify(field.name)}); - ${encodeType(field.type, `obj_.${capitalize(field.name)}`, `$"{path_}.${field.name}"`).replace(/\n/g, "\n ")}`, - ) - .join("\n ")} - resultWriter_.WriteEndObject(); - } -`; -} - -export function generateEnum(type: EnumType) { - return ` - public enum ${type.name} - {${type.values - .map( - ({ value }) => ` - ${capitalize(value)}`, - ) - .join(",\n ")} - } - - ${type.name} Decode${type.name}(JsonElement json_, string path_) - { - if (json_.ValueKind != JsonValueKind.String) - { - throw new FatalException($"'{path_}' must be a string."); - } - var value = json_.GetString();${type.values - .map( - ({ value }) => ` - if (value == "${value}") - { - return ${type.name}.${capitalize(value)}; - }`, - ) - .join("")} - throw new FatalException($"'{path_}' must be one of: (${type.values.map(({ value }) => `'${value}'`).join(", ")})."); - } - - void Encode${type.name}(${type.name} obj_, Utf8JsonWriter resultWriter_, string path_) - {${type.values - .map( - ({ value }) => ` - if (obj_ == ${type.name}.${capitalize(value)}) - { - resultWriter_.WriteStringValue("${value}"); - }`, - ) - .join("")} - } -`; +export function generateTypeName(type: Type): string { + switch (type.constructor) { + case StringPrimitiveType: + return "string"; + case IntPrimitiveType: + return "int"; + case UIntPrimitiveType: + return "uint"; + case FloatPrimitiveType: + return "double"; + case BigIntPrimitiveType: + return "BigInteger"; + case DatePrimitiveType: + case DateTimePrimitiveType: + return "DateTime"; + case BoolPrimitiveType: + return "bool"; + case BytesPrimitiveType: + return "byte[]"; + case MoneyPrimitiveType: + return "decimal"; + case CpfPrimitiveType: + case CnpjPrimitiveType: + case EmailPrimitiveType: + case HtmlPrimitiveType: + case UrlPrimitiveType: + case UuidPrimitiveType: + case HexPrimitiveType: + case Base64PrimitiveType: + case XmlPrimitiveType: + return "string"; + case VoidPrimitiveType: + return "void"; + case JsonPrimitiveType: + return "JsonElement"; + case OptionalType: + return `${generateTypeName((type as OptionalType).base)}?`; + case ArrayType: + return `List<${generateTypeName((type as ArrayType).base)}>`; + case StructType: + return type.name; + case EnumType: + return type.name; + case TypeReference: + return generateTypeName((type as TypeReference).type); + default: + throw new Error(`BUG: generateTypeName with ${type.constructor.name}`); + } } export function decodeType(type: Type, jsonElementVar: string, path: string, targetVar: string, suffix = 1, maybeNull = true): string { @@ -252,9 +203,10 @@ export function decodeType(type: Type, jsonElementVar: string, path: string, tar throw new FatalException($"'{${path}}' must be an integer"); } ` - .replace(/\n /g, "\n") + .replace(/\n {16}/gu, "\n") .trim(); } + case UIntPrimitiveType: { return ` if (${jsonElementVar}.ValueKind != JsonValueKind.Number || !${jsonElementVar}.TryGetUInt32(out ${targetVar})) @@ -262,9 +214,10 @@ export function decodeType(type: Type, jsonElementVar: string, path: string, tar throw new FatalException($"'{${path}}' must be an unsigned integer."); } ` - .replace(/\n /g, "\n") + .replace(/\n {16}/gu, "\n") .trim(); } + case MoneyPrimitiveType: { return ` if (${jsonElementVar}.ValueKind != JsonValueKind.Number || !${jsonElementVar}.TryGetDecimal(out ${targetVar}) || ${targetVar} % 1 != 0) @@ -273,9 +226,10 @@ export function decodeType(type: Type, jsonElementVar: string, path: string, tar } ${targetVar} /= 100; ` - .replace(/\n /g, "\n") + .replace(/\n {16}/gu, "\n") .trim(); } + case FloatPrimitiveType: { return ` if (${jsonElementVar}.ValueKind != JsonValueKind.Number || !${jsonElementVar}.TryGetDouble(out ${targetVar})) @@ -283,9 +237,10 @@ export function decodeType(type: Type, jsonElementVar: string, path: string, tar throw new FatalException($"'{${path}}' must be a floating-point number."); } ` - .replace(/\n /g, "\n") + .replace(/\n {16}/gu, "\n") .trim(); } + case BigIntPrimitiveType: { return ` if (${jsonElementVar}.ValueKind != JsonValueKind.String || !BigInteger.TryParse(${jsonElementVar}.GetString(), out ${targetVar})) @@ -293,9 +248,10 @@ export function decodeType(type: Type, jsonElementVar: string, path: string, tar throw new FatalException($"'{${path}}' must be an arbitrarily large integer in a string."); } ` - .replace(/\n /g, "\n") + .replace(/\n {16}/gu, "\n") .trim(); } + case StringPrimitiveType: { return ` if (${jsonElementVar}.ValueKind != JsonValueKind.String) @@ -304,9 +260,10 @@ export function decodeType(type: Type, jsonElementVar: string, path: string, tar } ${targetVar} = ${jsonElementVar}.GetString(); ` - .replace(/\n /g, "\n") + .replace(/\n {16}/gu, "\n") .trim(); } + case HtmlPrimitiveType: { // TODO: validate HTML return ` @@ -316,9 +273,10 @@ export function decodeType(type: Type, jsonElementVar: string, path: string, tar } ${targetVar} = ${jsonElementVar}.GetString(); ` - .replace(/\n /g, "\n") + .replace(/\n {16}/gu, "\n") .trim(); } + case CpfPrimitiveType: { // TODO: validate CPF return ` @@ -328,9 +286,10 @@ export function decodeType(type: Type, jsonElementVar: string, path: string, tar } ${targetVar} = ${jsonElementVar}.GetString(); ` - .replace(/\n /g, "\n") + .replace(/\n {16}/gu, "\n") .trim(); } + case CnpjPrimitiveType: { // TODO: validate CNPJ return ` @@ -340,9 +299,10 @@ export function decodeType(type: Type, jsonElementVar: string, path: string, tar } ${targetVar} = ${jsonElementVar}.GetString(); ` - .replace(/\n /g, "\n") + .replace(/\n {16}/gu, "\n") .trim(); } + case EmailPrimitiveType: { // TODO: validate Email return ` @@ -352,9 +312,10 @@ export function decodeType(type: Type, jsonElementVar: string, path: string, tar } ${targetVar} = ${jsonElementVar}.GetString(); ` - .replace(/\n /g, "\n") + .replace(/\n {16}/gu, "\n") .trim(); } + case UrlPrimitiveType: { // TODO: validate URL return ` @@ -364,9 +325,10 @@ export function decodeType(type: Type, jsonElementVar: string, path: string, tar } ${targetVar} = ${jsonElementVar}.GetString(); ` - .replace(/\n /g, "\n") + .replace(/\n {16}/gu, "\n") .trim(); } + case UuidPrimitiveType: { // TODO: validate UUID return ` @@ -376,9 +338,10 @@ export function decodeType(type: Type, jsonElementVar: string, path: string, tar } ${targetVar} = ${jsonElementVar}.GetString(); ` - .replace(/\n /g, "\n") + .replace(/\n {16}/gu, "\n") .trim(); } + case HexPrimitiveType: { // TODO: validate Hex return ` @@ -388,9 +351,10 @@ export function decodeType(type: Type, jsonElementVar: string, path: string, tar } ${targetVar} = ${jsonElementVar}.GetString(); ` - .replace(/\n /g, "\n") + .replace(/\n {16}/gu, "\n") .trim(); } + case Base64PrimitiveType: { // TODO: validate Base64 return ` @@ -400,9 +364,10 @@ export function decodeType(type: Type, jsonElementVar: string, path: string, tar } ${targetVar} = ${jsonElementVar}.GetString(); ` - .replace(/\n /g, "\n") + .replace(/\n {16}/gu, "\n") .trim(); } + case XmlPrimitiveType: { // TODO: validate XML return ` @@ -412,9 +377,10 @@ export function decodeType(type: Type, jsonElementVar: string, path: string, tar } ${targetVar} = ${jsonElementVar}.GetString(); ` - .replace(/\n /g, "\n") + .replace(/\n {16}/gu, "\n") .trim(); } + case BoolPrimitiveType: { return ` if (${jsonElementVar}.ValueKind != JsonValueKind.True && ${jsonElementVar}.ValueKind != JsonValueKind.False) @@ -423,9 +389,10 @@ export function decodeType(type: Type, jsonElementVar: string, path: string, tar } ${targetVar} = ${jsonElementVar}.GetBoolean(); ` - .replace(/\n /g, "\n") + .replace(/\n {16}/gu, "\n") .trim(); } + case BytesPrimitiveType: { return ` if (${jsonElementVar}.ValueKind != JsonValueKind.String) @@ -441,14 +408,16 @@ export function decodeType(type: Type, jsonElementVar: string, path: string, tar throw new FatalException($"'{${path}}' must be a base64 string."); } ` - .replace(/\n /g, "\n") + .replace(/\n {16}/gu, "\n") .trim(); } + case TypeReference: return decodeType((type as TypeReference).type, jsonElementVar, path, targetVar, suffix); case OptionalType: if (needsTempVarForNullable.includes((type as OptionalType).base.constructor)) { - const tempVar = targetVar.replace(/[^0-9a-zA-Z]/g, "") + "Tmp"; + const tempVar = `${targetVar.replace(/[^0-9a-zA-Z]/gu, "")}Tmp`; + return ` if (${jsonElementVar}.ValueKind == JsonValueKind.Null || ${jsonElementVar}.ValueKind == JsonValueKind.Undefined) { @@ -458,16 +427,17 @@ export function decodeType(type: Type, jsonElementVar: string, path: string, tar { ${generateTypeName((type as OptionalType).base)} ${tempVar}; ${decodeType((type as OptionalType).base, jsonElementVar, path, tempVar, suffix, false).replace( - /\n/g, + /\n/gu, "\n ", )} ${targetVar} = ${tempVar}; } ` - .replace(/\n /g, "\n") + .replace(/\n {20}/gu, "\n") .trim(); - } else { - return ` + } + + return ` if (${jsonElementVar}.ValueKind == JsonValueKind.Null || ${jsonElementVar}.ValueKind == JsonValueKind.Undefined) { ${targetVar} = null; @@ -475,14 +445,14 @@ export function decodeType(type: Type, jsonElementVar: string, path: string, tar else { ${decodeType((type as OptionalType).base, jsonElementVar, path, targetVar, suffix, false).replace( - /\n/g, + /\n/gu, "\n ", )} } ` - .replace(/\n /g, "\n") - .trim(); - } + .replace(/\n {20}/gu, "\n") + .trim(); + case EnumType: case StructType: return `${targetVar} = Decode${type.name}(${jsonElementVar}, ${path});`; @@ -495,11 +465,12 @@ export function decodeType(type: Type, jsonElementVar: string, path: string, tar } ${targetVar} = ${jsonElementVar}; ` - .replace(/\n /g, "\n") + .replace(/\n {16}/gu, "\n") .trim(); - } else { - return `${targetVar} = ${jsonElementVar};`; } + + return `${targetVar} = ${jsonElementVar};`; + case DateTimePrimitiveType: { return ` if (${jsonElementVar}.ValueKind != JsonValueKind.String || !(DateTime.TryParseExact(${jsonElementVar}.GetString(), "yyyy-MM-ddTHH:mm:ss.FFFFFFF", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out ${targetVar}) || DateTime.TryParseExact(${jsonElementVar}.GetString(), "yyyy-MM-ddTHH:mm:ss.FFFFFFF'Z'", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out ${targetVar}))) @@ -507,9 +478,10 @@ export function decodeType(type: Type, jsonElementVar: string, path: string, tar throw new FatalException($"'{${path}}' must be a datetime."); } ` - .replace(/\n /g, "\n") + .replace(/\n {16}/gu, "\n") .trim(); } + case DatePrimitiveType: { return ` if (${jsonElementVar}.ValueKind != JsonValueKind.String || !(DateTime.TryParseExact(${jsonElementVar}.GetString(), "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out ${targetVar}))) @@ -517,9 +489,10 @@ export function decodeType(type: Type, jsonElementVar: string, path: string, tar throw new FatalException($"'{${path}}' must be a date."); } ` - .replace(/\n /g, "\n") + .replace(/\n {16}/gu, "\n") .trim(); } + case ArrayType: { return ` if (${jsonElementVar}.ValueKind != JsonValueKind.Array) @@ -536,13 +509,14 @@ export function decodeType(type: Type, jsonElementVar: string, path: string, tar `$"{${path}}[{i${suffix}}]"`, `element${suffix}`, suffix + 1, - ).replace(/\n/g, "\n ")} + ).replace(/\n/gu, "\n ")} ${targetVar}.Add(element${suffix}); } ` - .replace(/\n /g, "\n") + .replace(/\n {16}/gu, "\n") .trim(); } + default: throw new Error(`BUG: decodeType with ${type.constructor.name}`); } @@ -553,29 +527,37 @@ export function encodeType(type: Type, valueVar: string, path: string, suffix = case StringPrimitiveType: { return `resultWriter_.WriteStringValue(${valueVar});`; } + case FloatPrimitiveType: case UIntPrimitiveType: case IntPrimitiveType: { return `resultWriter_.WriteNumberValue(${valueVar});`; } + case MoneyPrimitiveType: { return `resultWriter_.WriteNumberValue(Math.Round(${valueVar} * 100));`; } + case BigIntPrimitiveType: { return `resultWriter_.WriteStringValue(${valueVar}.ToString());`; } + case BoolPrimitiveType: { return `resultWriter_.WriteBooleanValue(${valueVar});`; } + case BytesPrimitiveType: { return `resultWriter_.WriteStringValue(Convert.ToBase64String(${valueVar}));`; } + case DateTimePrimitiveType: { return `resultWriter_.WriteStringValue(${valueVar}.ToString("yyyy-MM-ddTHH:mm:ss.FFFFFF'Z'"));`; } + case DatePrimitiveType: { return `resultWriter_.WriteStringValue(${valueVar}.ToString("yyyy-MM-dd"));`; } + // TODO: format those case CpfPrimitiveType: case CnpjPrimitiveType: @@ -585,12 +567,13 @@ export function encodeType(type: Type, valueVar: string, path: string, suffix = case UuidPrimitiveType: case Base64PrimitiveType: case HexPrimitiveType: - case Base64PrimitiveType: case XmlPrimitiveType: { return `resultWriter_.WriteStringValue(${valueVar});`; } + case OptionalType: { let realBaseType = (type as OptionalType).base; + while (realBaseType instanceof TypeReference) { realBaseType = realBaseType.type; } @@ -607,11 +590,12 @@ export function encodeType(type: Type, valueVar: string, path: string, suffix = typesWithNativeNullable.includes(realBaseType.constructor) ? valueVar : `${valueVar}.Value`, path, suffix, - ).replace(/\n/g, "\n ")} + ).replace(/\n/gu, "\n ")} }` - .replace(/\n /g, "\n") + .replace(/\n {16}/gu, "\n") .trim(); } + case TypeReference: return encodeType((type as TypeReference).type, valueVar, path, suffix); case EnumType: @@ -625,66 +609,116 @@ export function encodeType(type: Type, valueVar: string, path: string, suffix = for (var i${suffix} = 0; i${suffix} < ${valueVar}.Count; ++i${suffix}) { ${encodeType((type as ArrayType).base, `${valueVar}[i${suffix}]`, `$"{${path}}[{i${suffix}}]"`, suffix + 1).replace( - /\n/g, + /\n/gu, "\n ", )} } resultWriter_.WriteEndArray(); ` - .replace(/\n /g, "\n") + .replace(/\n {16}/gu, "\n") .trim(); } + default: throw new Error(`BUG: encodeType with ${type.constructor.name}`); } } -export function generateTypeName(type: Type): string { - switch (type.constructor) { - case StringPrimitiveType: - return "string"; - case IntPrimitiveType: - return "int"; - case UIntPrimitiveType: - return "uint"; - case FloatPrimitiveType: - return "double"; - case BigIntPrimitiveType: - return "BigInteger"; - case DatePrimitiveType: - case DateTimePrimitiveType: - return "DateTime"; - case BoolPrimitiveType: - return "bool"; - case BytesPrimitiveType: - return "byte[]"; - case MoneyPrimitiveType: - return "decimal"; - case CpfPrimitiveType: - case CnpjPrimitiveType: - case EmailPrimitiveType: - case HtmlPrimitiveType: - case UrlPrimitiveType: - case UuidPrimitiveType: - case HexPrimitiveType: - case Base64PrimitiveType: - case XmlPrimitiveType: - return "string"; - case VoidPrimitiveType: - return "void"; - case JsonPrimitiveType: - return "JsonElement"; - case OptionalType: - return generateTypeName((type as OptionalType).base) + "?"; - case ArrayType: - return `List<${generateTypeName((type as ArrayType).base)}>`; - case StructType: - return type.name; - case EnumType: - return type.name; - case TypeReference: - return generateTypeName((type as TypeReference).type); - default: - throw new Error(`BUG: generateTypeName with ${type.constructor.name}`); - } +export function generateStruct(struct: StructType): string { + return ` + public class ${struct.name} + {${struct.fields + .map( + field => ` + public ${generateTypeName(field.type)} ${capitalize(field.name)};`, + ) + .join("")} + public ${struct.name}(${struct.fields.map(field => `${generateTypeName(field.type)} ${ident(field.name)}`).join(", ")}) + {${struct.fields + .map( + field => ` + ${capitalize(field.name)} = ${ident(field.name)};`, + ) + .join("")} + } + } + + ${struct.name} Decode${struct.name}(JsonElement json_, string path_) + { + if (json_.ValueKind != JsonValueKind.Object) + { + throw new FatalException($"'{path_}' must be an object."); + }\n${struct.fields + .map( + field => ` JsonElement ${field.name}Json_; + if (!json_.TryGetProperty(${JSON.stringify(field.name)}, out ${field.name}Json_)) + { + ${ + field.type instanceof OptionalType + ? `${field.name}Json_ = new JsonElement();` + : `throw new FatalException($"'{path_}.${field.name}' must be set to a value of type ${field.type.name}.");` + } + } + ${generateTypeName(field.type)} ${ident(field.name)}; + ${decodeType(field.type, `${field.name}Json_`, `$"{path_}.${field.name}"`, ident(field.name)).replace(/\n/gu, "\n ")}`, + ) + .join("\n")} + return new ${struct.name}(${struct.fields.map(field => ident(field.name)).join(", ")}); + } + + void Encode${struct.name}(${struct.name} obj_, Utf8JsonWriter resultWriter_, string path_) + { + resultWriter_.WriteStartObject(); + ${struct.fields + .map( + field => `resultWriter_.WritePropertyName(${JSON.stringify(field.name)}); + ${encodeType(field.type, `obj_.${capitalize(field.name)}`, `$"{path_}.${field.name}"`).replace(/\n/gu, "\n ")}`, + ) + .join("\n ")} + resultWriter_.WriteEndObject(); + } +`; +} + +export function generateEnum(type: EnumType): string { + return ` + public enum ${type.name} + {${type.values + .map( + ({ value }) => ` + ${capitalize(value)}`, + ) + .join(",\n ")} + } + + ${type.name} Decode${type.name}(JsonElement json_, string path_) + { + if (json_.ValueKind != JsonValueKind.String) + { + throw new FatalException($"'{path_}' must be a string."); + } + var value = json_.GetString();${type.values + .map( + ({ value }) => ` + if (value == "${value}") + { + return ${type.name}.${capitalize(value)}; + }`, + ) + .join("")} + throw new FatalException($"'{path_}' must be one of: (${type.values.map(({ value }) => `'${value}'`).join(", ")})."); + } + + void Encode${type.name}(${type.name} obj_, Utf8JsonWriter resultWriter_, string path_) + {${type.values + .map( + ({ value }) => ` + if (obj_ == ${type.name}.${capitalize(value)}) + { + resultWriter_.WriteStringValue("${value}"); + }`, + ) + .join("")} + } +`; } diff --git a/dart-generator/.eslintrc.json b/dart-generator/.eslintrc.json new file mode 100644 index 000000000..9c2b6a1a8 --- /dev/null +++ b/dart-generator/.eslintrc.json @@ -0,0 +1,423 @@ +{ + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended" + ], + "env": { + "node": true, + "es2017": true, + "jest": true + }, + "globals": { + "BigInt": true + }, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2019, + "project": "tsconfig.json", + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint/eslint-plugin", + "prettier" + ], + "rules": { + "prettier/prettier": "error", + "spaced-comment": [ + "error", + "always" + ], + "@typescript-eslint/array-type": [ + "error", + { + "default": "array-simple" + } + ], + "@typescript-eslint/brace-style": "error", + "@typescript-eslint/consistent-type-definitions": [ + "error", + "interface" + ], + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-var-requires": "off", + "@typescript-eslint/func-call-spacing": [ + "error", + "never" + ], + "@typescript-eslint/interface-name-prefix": "off", + "@typescript-eslint/member-delimiter-style": "off", + "@typescript-eslint/no-empty-interface": "off", + "@typescript-eslint/no-unnecessary-qualifier": "error", + "@typescript-eslint/no-unnecessary-type-arguments": "error", + "@typescript-eslint/no-unused-expressions": "error", + "@typescript-eslint/no-use-before-define": "error", + "@typescript-eslint/no-useless-constructor": "error", + "@typescript-eslint/prefer-for-of": "error", + "@typescript-eslint/promise-function-async": "error", + "@typescript-eslint/require-array-sort-compare": "error", + "@typescript-eslint/restrict-plus-operands": "error", + "@typescript-eslint/semi": [ + "error", + "always", + { + "omitLastInOneLineBlock": false + } + ], + "array-callback-return": "error", + "block-scoped-var": "error", + "consistent-return": "error", + "curly": "error", + "default-case": "error", + "default-param-last": "error", + "dot-notation": "error", + "eqeqeq": [ + "error", + "always" + ], + "global-require": "error", + "guard-for-in": "error", + "no-alert": "error", + "no-buffer-constructor": "error", + "no-caller": "error", + "no-div-regex": "error", + "no-else-return": "error", + "no-empty-function": [ + "error", + { + "allow": [ + "constructors" + ] + } + ], + "no-eq-null": "error", + "no-eval": "error", + "no-extend-native": "error", + "no-extra-bind": "error", + "no-floating-decimal": "error", + "no-implicit-coercion": "error", + "no-implied-eval": "error", + "no-import-assign": "error", + "no-invalid-this": "error", + "no-iterator": "error", + "no-label-var": "error", + "no-labels": "error", + "no-lone-blocks": "error", + "no-loop-func": "error", + "no-multi-spaces": "error", + "no-multi-str": "error", + "no-new-func": "error", + "no-new-wrappers": "error", + "no-new": "error", + "no-octal-escape": "error", + "no-path-concat": "error", + "no-process-exit": "error", + "no-proto": "error", + "no-prototype-builtins": "off", + "no-restricted-modules": [ + "error", + "moment" + ], + "no-return-assign": "error", + "no-return-await": "error", + "no-script-url": "error", + "no-self-compare": "error", + "no-sequences": "error", + "no-shadow": "error", + "no-sync": [ + "error", + { + "allowAtRootLevel": true + } + ], + "no-template-curly-in-string": "error", + "no-throw-literal": "error", + "no-undef-init": "error", + "no-unmodified-loop-condition": "error", + "no-useless-call": "error", + "no-useless-concat": "error", + "no-void": "error", + "prefer-named-capture-group": "warn", + "prefer-regex-literals": "error", + "radix": "error", + "require-unicode-regexp": "error", + "strict": "error", + "wrap-iife": [ + "error", + "any" + ], + "yoda": "error", + "array-bracket-spacing": [ + "error", + "never" + ], + "array-element-newline": [ + "error", + "consistent" + ], + "block-spacing": "error", + "camelcase": "warn", + "capitalized-comments": [ + "error", + "always" + ], + "comma-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "comma-style": [ + "error", + "last" + ], + "computed-property-spacing": [ + "error", + "never" + ], + "eol-last": [ + "error", + "always" + ], + "func-name-matching": [ + "error", + "always" + ], + "func-names": [ + "error", + "as-needed" + ], + "func-style": [ + "error", + "declaration" + ], + "function-call-argument-newline": [ + "error", + "consistent" + ], + "jsx-quotes": [ + "error", + "prefer-double" + ], + "key-spacing": [ + "error", + { + "beforeColon": false + } + ], + "keyword-spacing": [ + "error", + { + "before": true, + "after": true + } + ], + "linebreak-style": [ + "error", + "unix" + ], + "lines-between-class-members": [ + "error", + "always" + ], + "max-depth": [ + "error", + 5 + ], + "max-nested-callbacks": [ + "error", + 10 + ], + "max-params": [ + "error", + 10 + ], + "max-statements-per-line": [ + "error", + { + "max": 2 + } + ], + "multiline-comment-style": [ + "error", + "starred-block" + ], + "new-parens": "error", + "newline-per-chained-call": [ + "error", + { + "ignoreChainWithDepth": 3 + } + ], + "no-array-constructor": "error", + "no-lonely-if": "error", + "no-multi-assign": "error", + "no-multiple-empty-lines": "error", + "no-negated-condition": "error", + "no-new-object": "error", + "no-tabs": "error", + "no-trailing-spaces": "error", + "no-unneeded-ternary": "error", + "no-whitespace-before-property": "error", + "object-curly-spacing": [ + "error", + "always" + ], + "one-var": [ + "error", + "never" + ], + "operator-assignment": [ + "error", + "always" + ], + "padded-blocks": [ + "error", + { + "classes": "never" + } + ], + "padding-line-between-statements": [ + "error", + { + "blankLine": "always", + "prev": [ + "const", + "let" + ], + "next": "*" + }, + { + "blankLine": "any", + "prev": [ + "const", + "let" + ], + "next": [ + "const", + "let", + "var" + ] + }, + { + "blankLine": "always", + "prev": "directive", + "next": "*" + }, + { + "blankLine": "any", + "prev": "directive", + "next": "directive" + }, + { + "blankLine": "always", + "prev": "break", + "next": "*" + }, + { + "blankLine": "always", + "prev": "block-like", + "next": "*" + } + ], + "prefer-object-spread": "error", + "quote-props": [ + "error", + "as-needed" + ], + "semi-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "semi-style": [ + "error", + "last" + ], + "sort-keys": "error", + "space-before-blocks": "error", + "space-in-parens": [ + "error", + "never" + ], + "space-infix-ops": [ + "error", + { + "int32Hint": false + } + ], + "space-unary-ops": "error", + "switch-colon-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "template-tag-spacing": [ + "error", + "never" + ], + "unicode-bom": [ + "error", + "never" + ], + "arrow-parens": [ + "error", + "as-needed" + ], + "arrow-spacing": [ + "error", + { + "before": true, + "after": true + } + ], + "generator-star-spacing": [ + "error", + { + "before": true, + "after": false + } + ], + "no-confusing-arrow": "error", + "no-duplicate-imports": "error", + "no-useless-computed-key": "error", + "no-useless-rename": "error", + "no-var": "error", + "object-shorthand": [ + "error", + "properties" + ], + "prefer-arrow-callback": "error", + "prefer-const": "error", + "prefer-destructuring": "error", + "prefer-numeric-literals": "error", + "prefer-rest-params": "error", + "prefer-spread": "error", + "prefer-template": "error", + "rest-spread-spacing": [ + "error", + "never" + ], + "sort-imports": [ + "error", + { + "ignoreCase": true, + "ignoreDeclarationSort": true + } + ], + "template-curly-spacing": [ + "error", + "never" + ], + "yield-star-spacing": [ + "error", + "before" + ] + } +} diff --git a/dart-generator/package.json b/dart-generator/package.json index 7a7a858d2..d3a4925f1 100644 --- a/dart-generator/package.json +++ b/dart-generator/package.json @@ -5,6 +5,8 @@ "main": "src/index.ts", "scripts": { "test": "jest --passWithNoTests", + "eslint:fix": "eslint --fix '**/*.ts'", + "eslint:check": "eslint '**/*.ts'", "prettier:fix": "prettier --write '**/*.{t,j}s'", "prettier:check": "prettier --check '**/*.{t,j}s'", "build": "tsc" @@ -25,6 +27,11 @@ "devDependencies": { "@types/jest": "^26.0.14", "@types/node": "^14.11.9", + "@typescript-eslint/eslint-plugin": "^4.4.1", + "@typescript-eslint/parser": "^4.4.1", + "eslint": "^7.11.0", + "eslint-config-prettier": "^6.10.0", + "eslint-plugin-prettier": "^3.1.2", "jest": "^26.5.3", "prettier": "^2.1.2", "ts-jest": "^26.4.1", diff --git a/dart-generator/src/dart-client.ts b/dart-generator/src/dart-client.ts index 52ce376d9..2aa9fe24a 100644 --- a/dart-generator/src/dart-client.ts +++ b/dart-generator/src/dart-client.ts @@ -1,9 +1,7 @@ import { AstRoot, VoidPrimitiveType } from "@sdkgen/parser"; import { cast, generateClass, generateEnum, generateErrorClass, generateTypeName } from "./helpers"; -interface Options {} - -export function generateDartClientSource(ast: AstRoot, options: Options) { +export function generateDartClientSource(ast: AstRoot): string { let code = ""; code += `import 'package:flutter/widgets.dart'; @@ -51,16 +49,19 @@ ${ast.operations for (const field of type.fields) { code += ` "${field.name}": "${field.type.name}",\n`; } + code += ` },\n`; code += ` (Map fields) => ${type.name}(\n`; for (const field of type.fields) { code += ` ${field.name}: ${cast(`fields["${field.name}"]`, field.type)},\n`; } + code += ` ),\n`; code += ` (${type.name} obj) => ({\n`; for (const field of type.fields) { code += ` "${field.name}": obj.${field.name},\n`; } + code += ` }),\n`; code += ` ),\n`; } @@ -79,14 +80,17 @@ ${ast.operations for (const arg of op.args) { code += ` "${arg.name}": "${arg.type.name}",\n`; } + code += ` }),\n`; } + code += `};\n\n`; code += `var _errTable = {\n`; for (const error of ast.errors) { code += ` "${error}": (msg) => ${error}(msg),\n`; } + code += `};\n`; return code; diff --git a/dart-generator/src/helpers.ts b/dart-generator/src/helpers.ts index d0b5c9d9b..be388507d 100644 --- a/dart-generator/src/helpers.ts +++ b/dart-generator/src/helpers.ts @@ -28,21 +28,15 @@ import { XmlPrimitiveType, } from "@sdkgen/parser"; -export function generateEnum(type: EnumType) { +export function generateEnum(type: EnumType): string { return `enum ${type.name} {\n ${type.values.map(x => x.value).join(",\n ")}\n}\n`; } -export function generateClass(type: StructType) { - return `class ${type.name} {\n ${type.fields - .map((field: any) => `${generateTypeName(field.type)} ${field.name};`) - .join("\n ")}\n\n${generateConstructor(type)}}\n`; -} - -///Generate the class constructor with the tag [@required] for non nullable types +// /Generate the class constructor with the tag [@required] for non nullable types function generateConstructor(type: StructType): string { const doubleSpace = " "; const fourSpaces = " "; - var str = `${doubleSpace}${type.name}({\n`; + let str = `${doubleSpace}${type.name}({\n`; type.fields.forEach((field: any) => { if (field.type instanceof OptionalType) { @@ -50,30 +44,17 @@ function generateConstructor(type: StructType): string { } else { str = str.concat(`${fourSpaces}@required `); } + str = str.concat(`this.${field.name},\n`); }); str = str.concat(`${doubleSpace}});\n`); return str; } -export function generateErrorClass(error: string) { +export function generateErrorClass(error: string): string { return `class ${error} extends SdkgenError {\n ${error}(msg) : super(msg);\n}\n`; } -export function cast(value: string, type: Type): string { - if (type instanceof OptionalType) { - return cast(value, (type as OptionalType).base); - } else if (type instanceof ArrayType) { - return `(${value} as List)?.map((e) => ${cast("e", (type as ArrayType).base)})?.toList()`; - } else if (type instanceof VoidPrimitiveType) { - return value; - } else if (type instanceof FloatPrimitiveType || type instanceof MoneyPrimitiveType) { - return `(${value} as num)?.toDouble()`; - } else { - return `${value} as ${generateTypeName(type)}`; - } -} - export function generateTypeName(type: Type): string { switch (type.constructor) { case StringPrimitiveType: @@ -122,3 +103,23 @@ export function generateTypeName(type: Type): string { throw new Error(`BUG: generateTypeName with ${type.constructor.name}`); } } + +export function cast(value: string, type: Type): string { + if (type instanceof OptionalType) { + return cast(value, (type as OptionalType).base); + } else if (type instanceof ArrayType) { + return `(${value} as List)?.map((e) => ${cast("e", (type as ArrayType).base)})?.toList()`; + } else if (type instanceof VoidPrimitiveType) { + return value; + } else if (type instanceof FloatPrimitiveType || type instanceof MoneyPrimitiveType) { + return `(${value} as num)?.toDouble()`; + } + + return `${value} as ${generateTypeName(type)}`; +} + +export function generateClass(type: StructType): string { + return `class ${type.name} {\n ${type.fields + .map((field: any) => `${generateTypeName(field.type)} ${field.name};`) + .join("\n ")}\n\n${generateConstructor(type)}}\n`; +} diff --git a/kotlin-generator/.eslintrc.json b/kotlin-generator/.eslintrc.json new file mode 100644 index 000000000..9c2b6a1a8 --- /dev/null +++ b/kotlin-generator/.eslintrc.json @@ -0,0 +1,423 @@ +{ + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended" + ], + "env": { + "node": true, + "es2017": true, + "jest": true + }, + "globals": { + "BigInt": true + }, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2019, + "project": "tsconfig.json", + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint/eslint-plugin", + "prettier" + ], + "rules": { + "prettier/prettier": "error", + "spaced-comment": [ + "error", + "always" + ], + "@typescript-eslint/array-type": [ + "error", + { + "default": "array-simple" + } + ], + "@typescript-eslint/brace-style": "error", + "@typescript-eslint/consistent-type-definitions": [ + "error", + "interface" + ], + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-var-requires": "off", + "@typescript-eslint/func-call-spacing": [ + "error", + "never" + ], + "@typescript-eslint/interface-name-prefix": "off", + "@typescript-eslint/member-delimiter-style": "off", + "@typescript-eslint/no-empty-interface": "off", + "@typescript-eslint/no-unnecessary-qualifier": "error", + "@typescript-eslint/no-unnecessary-type-arguments": "error", + "@typescript-eslint/no-unused-expressions": "error", + "@typescript-eslint/no-use-before-define": "error", + "@typescript-eslint/no-useless-constructor": "error", + "@typescript-eslint/prefer-for-of": "error", + "@typescript-eslint/promise-function-async": "error", + "@typescript-eslint/require-array-sort-compare": "error", + "@typescript-eslint/restrict-plus-operands": "error", + "@typescript-eslint/semi": [ + "error", + "always", + { + "omitLastInOneLineBlock": false + } + ], + "array-callback-return": "error", + "block-scoped-var": "error", + "consistent-return": "error", + "curly": "error", + "default-case": "error", + "default-param-last": "error", + "dot-notation": "error", + "eqeqeq": [ + "error", + "always" + ], + "global-require": "error", + "guard-for-in": "error", + "no-alert": "error", + "no-buffer-constructor": "error", + "no-caller": "error", + "no-div-regex": "error", + "no-else-return": "error", + "no-empty-function": [ + "error", + { + "allow": [ + "constructors" + ] + } + ], + "no-eq-null": "error", + "no-eval": "error", + "no-extend-native": "error", + "no-extra-bind": "error", + "no-floating-decimal": "error", + "no-implicit-coercion": "error", + "no-implied-eval": "error", + "no-import-assign": "error", + "no-invalid-this": "error", + "no-iterator": "error", + "no-label-var": "error", + "no-labels": "error", + "no-lone-blocks": "error", + "no-loop-func": "error", + "no-multi-spaces": "error", + "no-multi-str": "error", + "no-new-func": "error", + "no-new-wrappers": "error", + "no-new": "error", + "no-octal-escape": "error", + "no-path-concat": "error", + "no-process-exit": "error", + "no-proto": "error", + "no-prototype-builtins": "off", + "no-restricted-modules": [ + "error", + "moment" + ], + "no-return-assign": "error", + "no-return-await": "error", + "no-script-url": "error", + "no-self-compare": "error", + "no-sequences": "error", + "no-shadow": "error", + "no-sync": [ + "error", + { + "allowAtRootLevel": true + } + ], + "no-template-curly-in-string": "error", + "no-throw-literal": "error", + "no-undef-init": "error", + "no-unmodified-loop-condition": "error", + "no-useless-call": "error", + "no-useless-concat": "error", + "no-void": "error", + "prefer-named-capture-group": "warn", + "prefer-regex-literals": "error", + "radix": "error", + "require-unicode-regexp": "error", + "strict": "error", + "wrap-iife": [ + "error", + "any" + ], + "yoda": "error", + "array-bracket-spacing": [ + "error", + "never" + ], + "array-element-newline": [ + "error", + "consistent" + ], + "block-spacing": "error", + "camelcase": "warn", + "capitalized-comments": [ + "error", + "always" + ], + "comma-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "comma-style": [ + "error", + "last" + ], + "computed-property-spacing": [ + "error", + "never" + ], + "eol-last": [ + "error", + "always" + ], + "func-name-matching": [ + "error", + "always" + ], + "func-names": [ + "error", + "as-needed" + ], + "func-style": [ + "error", + "declaration" + ], + "function-call-argument-newline": [ + "error", + "consistent" + ], + "jsx-quotes": [ + "error", + "prefer-double" + ], + "key-spacing": [ + "error", + { + "beforeColon": false + } + ], + "keyword-spacing": [ + "error", + { + "before": true, + "after": true + } + ], + "linebreak-style": [ + "error", + "unix" + ], + "lines-between-class-members": [ + "error", + "always" + ], + "max-depth": [ + "error", + 5 + ], + "max-nested-callbacks": [ + "error", + 10 + ], + "max-params": [ + "error", + 10 + ], + "max-statements-per-line": [ + "error", + { + "max": 2 + } + ], + "multiline-comment-style": [ + "error", + "starred-block" + ], + "new-parens": "error", + "newline-per-chained-call": [ + "error", + { + "ignoreChainWithDepth": 3 + } + ], + "no-array-constructor": "error", + "no-lonely-if": "error", + "no-multi-assign": "error", + "no-multiple-empty-lines": "error", + "no-negated-condition": "error", + "no-new-object": "error", + "no-tabs": "error", + "no-trailing-spaces": "error", + "no-unneeded-ternary": "error", + "no-whitespace-before-property": "error", + "object-curly-spacing": [ + "error", + "always" + ], + "one-var": [ + "error", + "never" + ], + "operator-assignment": [ + "error", + "always" + ], + "padded-blocks": [ + "error", + { + "classes": "never" + } + ], + "padding-line-between-statements": [ + "error", + { + "blankLine": "always", + "prev": [ + "const", + "let" + ], + "next": "*" + }, + { + "blankLine": "any", + "prev": [ + "const", + "let" + ], + "next": [ + "const", + "let", + "var" + ] + }, + { + "blankLine": "always", + "prev": "directive", + "next": "*" + }, + { + "blankLine": "any", + "prev": "directive", + "next": "directive" + }, + { + "blankLine": "always", + "prev": "break", + "next": "*" + }, + { + "blankLine": "always", + "prev": "block-like", + "next": "*" + } + ], + "prefer-object-spread": "error", + "quote-props": [ + "error", + "as-needed" + ], + "semi-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "semi-style": [ + "error", + "last" + ], + "sort-keys": "error", + "space-before-blocks": "error", + "space-in-parens": [ + "error", + "never" + ], + "space-infix-ops": [ + "error", + { + "int32Hint": false + } + ], + "space-unary-ops": "error", + "switch-colon-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "template-tag-spacing": [ + "error", + "never" + ], + "unicode-bom": [ + "error", + "never" + ], + "arrow-parens": [ + "error", + "as-needed" + ], + "arrow-spacing": [ + "error", + { + "before": true, + "after": true + } + ], + "generator-star-spacing": [ + "error", + { + "before": true, + "after": false + } + ], + "no-confusing-arrow": "error", + "no-duplicate-imports": "error", + "no-useless-computed-key": "error", + "no-useless-rename": "error", + "no-var": "error", + "object-shorthand": [ + "error", + "properties" + ], + "prefer-arrow-callback": "error", + "prefer-const": "error", + "prefer-destructuring": "error", + "prefer-numeric-literals": "error", + "prefer-rest-params": "error", + "prefer-spread": "error", + "prefer-template": "error", + "rest-spread-spacing": [ + "error", + "never" + ], + "sort-imports": [ + "error", + { + "ignoreCase": true, + "ignoreDeclarationSort": true + } + ], + "template-curly-spacing": [ + "error", + "never" + ], + "yield-star-spacing": [ + "error", + "before" + ] + } +} diff --git a/kotlin-generator/package.json b/kotlin-generator/package.json index 904027f74..3e7ebf49e 100644 --- a/kotlin-generator/package.json +++ b/kotlin-generator/package.json @@ -5,6 +5,8 @@ "main": "src/index.ts", "scripts": { "test": "jest --passWithNoTests", + "eslint:fix": "eslint --fix '**/*.ts'", + "eslint:check": "eslint '**/*.ts'", "prettier:fix": "prettier --write '**/*.{t,j}s'", "prettier:check": "prettier --check '**/*.{t,j}s'", "build": "tsc" @@ -25,6 +27,11 @@ "devDependencies": { "@types/jest": "^26.0.14", "@types/node": "^14.11.9", + "@typescript-eslint/eslint-plugin": "^4.4.1", + "@typescript-eslint/parser": "^4.4.1", + "eslint": "^7.11.0", + "eslint-config-prettier": "^6.10.0", + "eslint-plugin-prettier": "^3.1.2", "jest": "^26.5.3", "prettier": "^2.1.2", "ts-jest": "^26.4.1", diff --git a/kotlin-generator/src/android-client.ts b/kotlin-generator/src/android-client.ts index 4bca445f8..74163aed9 100644 --- a/kotlin-generator/src/android-client.ts +++ b/kotlin-generator/src/android-client.ts @@ -1,9 +1,7 @@ import { AstRoot } from "@sdkgen/parser"; import { generateClass, generateEnum, generateErrorClass, generateJsonAddRepresentation, generateKotlinTypeName, mangle } from "./helpers"; -interface Options {} - -export function generateAndroidClientSource(ast: AstRoot, options: Options) { +export function generateAndroidClientSource(ast: AstRoot): string { let code = `@file:Suppress("UNNECESSARY_SAFE_CALL") import android.os.Parcelable @@ -31,7 +29,7 @@ class ApiClient( val applicationContext: Context, defaultTimeoutMillis: Long = 10000L ) : SdkgenHttpClient(baseUrl, applicationContext, defaultTimeoutMillis) { - + private val gson = GsonBuilder() .registerTypeAdapter(object : TypeToken() {}.type, ByteArrayDeserializer()) .create()\n\n`; @@ -51,6 +49,7 @@ class ApiClient( const errorTypeEnumEntries: string[] = []; const connectionError = "Connection"; + errorTypeEnumEntries.push(connectionError); code += ` ${generateErrorClass(connectionError)}`; @@ -77,9 +76,10 @@ class ApiClient( code += ast.operations .map(op => { let opImpl = ""; - let args = op.args + const args = op.args .map(arg => `${mangle(arg.name)}: ${generateKotlinTypeName(arg.type)}`) .concat([`timeoutMillis: Long? = null`, `callback: ((response: Response<${generateKotlinTypeName(op.returnType)}>) -> Unit)? = null`]); + opImpl += ` fun ${mangle(op.prettyName)}(\n ${args.join(",\n ")}\n ): Deferred> = sdkgenIOScope.async {\n`; @@ -93,7 +93,7 @@ class ApiClient( } opImpl += `\n`; - opImpl += ` val call = makeRequest(\"${op.prettyName}\", bodyArgs, timeoutMillis)\n`; + opImpl += ` val call = makeRequest("${op.prettyName}", bodyArgs, timeoutMillis)\n`; opImpl += ` val response: Response<${generateKotlinTypeName(op.returnType)}> = handleCallResponse(call)\n`; opImpl += ` withContext(Dispatchers.Main) { callback?.invoke(response) } \n`; opImpl += ` return@async response\n`; diff --git a/kotlin-generator/src/helpers.ts b/kotlin-generator/src/helpers.ts index 749a38ac3..5f7ecef2c 100644 --- a/kotlin-generator/src/helpers.ts +++ b/kotlin-generator/src/helpers.ts @@ -28,55 +28,6 @@ import { XmlPrimitiveType, } from "@sdkgen/parser"; -export function generateEnum(type: EnumType) { - let enumDesc = "@Parcelize \n"; - enumDesc += ` enum class ${type.name} : Parcelable { ${type.values.map(x => mangle(x.value)).join(", ")} }\n`; - return enumDesc; -} - -export function getAnnotation(type: Type, fieldName?: string): string { - let fieldAnnotation = ""; - - if (fieldName && fieldName !== mangle(fieldName)) { - fieldAnnotation += ` @SerializedName("${fieldName}")\n`; - } - - switch (type.constructor) { - case DatePrimitiveType: - fieldAnnotation += " @JsonAdapter(DateAdapter::class)\n"; - break; - case DateTimePrimitiveType: - fieldAnnotation += " @JsonAdapter(DateTimeAdapter::class)\n"; - break; - case ArrayType: - fieldAnnotation += getAnnotation((type as ArrayType).base); - break; - case OptionalType: - fieldAnnotation += getAnnotation((type as OptionalType).base); - break; - } - - return fieldAnnotation; -} - -export function generateClass(type: StructType) { - let classDesc = "@Parcelize\n"; - classDesc += ` data class ${type.name}(\n${type.fields - .map(field => { - let fieldDesc = getAnnotation(field.type, field.name); - fieldDesc += ` var ${mangle(field.name)}: ${generateKotlinTypeName(field.type)}${ - field.type.constructor === OptionalType ? " = null" : "" - }`; - return fieldDesc; - }) - .join(",\n")}\n ) : Parcelable\n`; - return classDesc; -} - -export function generateErrorClass(error: string) { - return `class ${error}(message: String) : Error(message)\n`; -} - export function generateKotlinTypeName(type: Type): string { switch (type.constructor) { case IntPrimitiveType: @@ -119,7 +70,7 @@ export function generateKotlinTypeName(type: Type): string { return "JsonElement"; case OptionalType: - return generateKotlinTypeName((type as OptionalType).base) + "?"; + return `${generateKotlinTypeName((type as OptionalType).base)}?`; case ArrayType: { return `ArrayList<${generateKotlinTypeName((type as ArrayType).base)}>`; @@ -137,45 +88,6 @@ export function generateKotlinTypeName(type: Type): string { } } -export function generateJsonAddRepresentation(type: Type, fieldName: string): string { - switch (type.constructor) { - case StringPrimitiveType: - case CpfPrimitiveType: - case CnpjPrimitiveType: - case EmailPrimitiveType: - case HtmlPrimitiveType: - case UrlPrimitiveType: - case UuidPrimitiveType: - case HexPrimitiveType: - case Base64PrimitiveType: - case XmlPrimitiveType: - case IntPrimitiveType: - case UIntPrimitiveType: - case MoneyPrimitiveType: - case FloatPrimitiveType: - case BoolPrimitiveType: - return `addProperty(\"${fieldName}\", ${mangle(fieldName)})`; - case OptionalType: - return generateJsonAddRepresentation((type as OptionalType).base, fieldName); - case DatePrimitiveType: - return `addProperty(\"${fieldName}\", ${mangle(fieldName)}?.let { DateAdapter.sdf.format(it.time)}) `; - case DateTimePrimitiveType: - return `addProperty(\"${fieldName}\", ${mangle(fieldName)}?.let { DateTimeAdapter.sdf.format(it.time)})`; - case ArrayType: - case StructType: - case EnumType: - case TypeReference: - case JsonPrimitiveType: - return `add(\"${fieldName}\", gson.toJsonTree(${mangle(fieldName)}))`; - case VoidPrimitiveType: - return ""; - case BytesPrimitiveType: - return `addProperty(\"${fieldName}\", Base64.encodeToString(${mangle(fieldName)}, Base64.DEFAULT))`; - default: - throw new Error(`BUG: No result found for generateJsonRepresentation with ${type.constructor.name}`); - } -} - export function mangle(fieldName: string): string { const mangleList = [ "in", @@ -267,3 +179,96 @@ export function mangle(fieldName: string): string { return fieldName; } + +export function generateJsonAddRepresentation(type: Type, fieldName: string): string { + switch (type.constructor) { + case StringPrimitiveType: + case CpfPrimitiveType: + case CnpjPrimitiveType: + case EmailPrimitiveType: + case HtmlPrimitiveType: + case UrlPrimitiveType: + case UuidPrimitiveType: + case HexPrimitiveType: + case Base64PrimitiveType: + case XmlPrimitiveType: + case IntPrimitiveType: + case UIntPrimitiveType: + case MoneyPrimitiveType: + case FloatPrimitiveType: + case BoolPrimitiveType: + return `addProperty("${fieldName}", ${mangle(fieldName)})`; + case OptionalType: + return generateJsonAddRepresentation((type as OptionalType).base, fieldName); + case DatePrimitiveType: + return `addProperty("${fieldName}", ${mangle(fieldName)}?.let { DateAdapter.sdf.format(it.time)}) `; + case DateTimePrimitiveType: + return `addProperty("${fieldName}", ${mangle(fieldName)}?.let { DateTimeAdapter.sdf.format(it.time)})`; + case ArrayType: + case StructType: + case EnumType: + case TypeReference: + case JsonPrimitiveType: + return `add("${fieldName}", gson.toJsonTree(${mangle(fieldName)}))`; + case VoidPrimitiveType: + return ""; + case BytesPrimitiveType: + return `addProperty("${fieldName}", Base64.encodeToString(${mangle(fieldName)}, Base64.DEFAULT))`; + default: + throw new Error(`BUG: No result found for generateJsonRepresentation with ${type.constructor.name}`); + } +} + +export function generateEnum(type: EnumType): string { + let enumDesc = "@Parcelize \n"; + + enumDesc += ` enum class ${type.name} : Parcelable { ${type.values.map(x => mangle(x.value)).join(", ")} }\n`; + return enumDesc; +} + +export function getAnnotation(type: Type, fieldName?: string): string { + let fieldAnnotation = ""; + + if (fieldName && fieldName !== mangle(fieldName)) { + fieldAnnotation += ` @SerializedName("${fieldName}")\n`; + } + + switch (type.constructor) { + case DatePrimitiveType: + fieldAnnotation += " @JsonAdapter(DateAdapter::class)\n"; + break; + case DateTimePrimitiveType: + fieldAnnotation += " @JsonAdapter(DateTimeAdapter::class)\n"; + break; + case ArrayType: + fieldAnnotation += getAnnotation((type as ArrayType).base); + break; + case OptionalType: + fieldAnnotation += getAnnotation((type as OptionalType).base); + break; + default: + break; + } + + return fieldAnnotation; +} + +export function generateClass(type: StructType): string { + let classDesc = "@Parcelize\n"; + + classDesc += ` data class ${type.name}(\n${type.fields + .map(field => { + let fieldDesc = getAnnotation(field.type, field.name); + + fieldDesc += ` var ${mangle(field.name)}: ${generateKotlinTypeName(field.type)}${ + field.type.constructor === OptionalType ? " = null" : "" + }`; + return fieldDesc; + }) + .join(",\n")}\n ) : Parcelable\n`; + return classDesc; +} + +export function generateErrorClass(error: string): string { + return `class ${error}(message: String) : Error(message)\n`; +} diff --git a/node-runtime/src/http-server.ts b/node-runtime/src/http-server.ts index 01a1e343d..28ccfba32 100644 --- a/node-runtime/src/http-server.ts +++ b/node-runtime/src/http-server.ts @@ -83,7 +83,7 @@ export class SdkgenHttpServer { try { res.setHeader("Content-Type", "application/octet-stream"); - res.write(generateFn(this.ast, {})); + res.write(generateFn(this.ast)); } catch (e) { console.error(e); res.statusCode = 500; From ad22db9d249328bf80e431a279f19882a5b434a7 Mon Sep 17 00:00:00 2001 From: Guilherme Bernal Date: Sun, 18 Oct 2020 23:36:10 +0000 Subject: [PATCH 06/10] fix: node-runtime spec --- node-runtime/spec/rest/rest.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node-runtime/spec/rest/rest.spec.ts b/node-runtime/spec/rest/rest.spec.ts index 1c90b53e9..b9b7b2d26 100644 --- a/node-runtime/spec/rest/rest.spec.ts +++ b/node-runtime/spec/rest/rest.spec.ts @@ -246,7 +246,7 @@ describe("Rest API", () => { data: `{"val":0}`, method: "POST", path: "/obj", - result: `{"message":"Value is zero ~ spec error","type":"Fatal"}`, + result: `{"message":"Error: Value is zero ~ spec error","type":"Fatal"}`, resultHeaders: { "content-type": "application/json", }, From b5b2207bcd5fa958560549d80f659ce3de3155de Mon Sep 17 00:00:00 2001 From: Guilherme Bernal Date: Mon, 19 Oct 2020 07:23:31 -0300 Subject: [PATCH 07/10] Update dart-generator/src/helpers.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Douglas Gadêlha --- dart-generator/src/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dart-generator/src/helpers.ts b/dart-generator/src/helpers.ts index be388507d..fc1a0242a 100644 --- a/dart-generator/src/helpers.ts +++ b/dart-generator/src/helpers.ts @@ -32,7 +32,7 @@ export function generateEnum(type: EnumType): string { return `enum ${type.name} {\n ${type.values.map(x => x.value).join(",\n ")}\n}\n`; } -// /Generate the class constructor with the tag [@required] for non nullable types +// Generate the class constructor with the tag [@required] for non nullable types function generateConstructor(type: StructType): string { const doubleSpace = " "; const fourSpaces = " "; From 7bd764f47397337b03f6e808e46a62591a380d9d Mon Sep 17 00:00:00 2001 From: Guilherme Bernal Date: Mon, 19 Oct 2020 07:23:50 -0300 Subject: [PATCH 08/10] Update playground/src/configuration/serviceWorker.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Douglas Gadêlha --- playground/src/configuration/serviceWorker.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/playground/src/configuration/serviceWorker.ts b/playground/src/configuration/serviceWorker.ts index 190322bd6..16f209a89 100644 --- a/playground/src/configuration/serviceWorker.ts +++ b/playground/src/configuration/serviceWorker.ts @@ -11,7 +11,6 @@ export function setupServiceWorker(configEnvs: ConfigEnvs): void { runtime.install({ onUpdateReady: () => runtime.applyUpdate(), - // Tslint:disable-next-line: deprecation onUpdated: () => window.location.reload(true), }); From 320d52a3f6faa5cb70a082e7dd1ced98847b60ee Mon Sep 17 00:00:00 2001 From: Guilherme Bernal Date: Mon, 19 Oct 2020 07:24:03 -0300 Subject: [PATCH 09/10] Update playground/src/helpers/requestModel/index.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Douglas Gadêlha --- playground/src/helpers/requestModel/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/src/helpers/requestModel/index.ts b/playground/src/helpers/requestModel/index.ts index 4a93594b8..c7ca9b656 100644 --- a/playground/src/helpers/requestModel/index.ts +++ b/playground/src/helpers/requestModel/index.ts @@ -84,7 +84,7 @@ export class requestModel { this.response = res.result; this.status = "sucess"; if (callBack) { - callBack("sucess"); + callBack("success"); } } else { this.status = "error"; From 73b11b64f5e8e50f38ed97f5bd7f9e5ecb01ebda Mon Sep 17 00:00:00 2001 From: Guilherme Bernal Date: Mon, 19 Oct 2020 12:38:41 +0000 Subject: [PATCH 10/10] fix: typo on playground --- .../src/components/requestCard/bottom/bottom.test.tsx | 2 +- playground/src/components/requestCard/bottom/index.tsx | 6 +++--- playground/src/components/requestCard/header/index.tsx | 2 +- playground/src/components/requestCard/index.tsx | 2 +- playground/src/helpers/requestModel/index.ts | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/playground/src/components/requestCard/bottom/bottom.test.tsx b/playground/src/components/requestCard/bottom/bottom.test.tsx index c5a896fc1..51ecedd4f 100644 --- a/playground/src/components/requestCard/bottom/bottom.test.tsx +++ b/playground/src/components/requestCard/bottom/bottom.test.tsx @@ -28,5 +28,5 @@ describe("", () => { testForStatus("notFetched", "Make Request", "play", "blue"); testForStatus("fetching", "Fetching", "pause", "orange"); testForStatus("error", "Error, Retry?", "redo", "red"); - testForStatus("sucess", "Success, Retry?", "redo", "green"); + testForStatus("success", "Success, Retry?", "redo", "green"); }); diff --git a/playground/src/components/requestCard/bottom/index.tsx b/playground/src/components/requestCard/bottom/index.tsx index f78c8a26c..6a89a8bc2 100644 --- a/playground/src/components/requestCard/bottom/index.tsx +++ b/playground/src/components/requestCard/bottom/index.tsx @@ -15,19 +15,19 @@ export default function Bottom(props: BottomProps): JSX.Element { error: faRedo, fetching: faPause, notFetched: faPlay, - sucess: faRedo, + success: faRedo, }; const labels: Record = { error: "Error, Retry?", fetching: "Fetching", notFetched: "Make Request", - sucess: "Success, Retry?", + success: "Success, Retry?", }; const colors: Record = { error: s.red, fetching: s.orange, notFetched: s.blue, - sucess: s.green, + success: s.green, }; const selectedIcon = icons[props.status]; const selectedLabel = labels[props.status]; diff --git a/playground/src/components/requestCard/header/index.tsx b/playground/src/components/requestCard/header/index.tsx index b550699db..37d854320 100644 --- a/playground/src/components/requestCard/header/index.tsx +++ b/playground/src/components/requestCard/header/index.tsx @@ -19,7 +19,7 @@ function Header(props: HeaderProps) { error: s.red, fetching: s.orange, notFetched: s.gray, - sucess: s.green, + success: s.green, }; const accentColorClass = colors[model.status]; diff --git a/playground/src/components/requestCard/index.tsx b/playground/src/components/requestCard/index.tsx index 53e22be0b..042dff7d9 100644 --- a/playground/src/components/requestCard/index.tsx +++ b/playground/src/components/requestCard/index.tsx @@ -34,7 +34,7 @@ function Card(props: CardProps) { status={status} onClick={() => { props.model.reset(); - props.model.call(jsonArgs, newStatus => (newStatus === "sucess" ? setActiveTab("response") : setActiveTab("error"))); + props.model.call(jsonArgs, newStatus => (newStatus === "success" ? setActiveTab("response") : setActiveTab("error"))); }} />
diff --git a/playground/src/helpers/requestModel/index.ts b/playground/src/helpers/requestModel/index.ts index c7ca9b656..12f872d49 100644 --- a/playground/src/helpers/requestModel/index.ts +++ b/playground/src/helpers/requestModel/index.ts @@ -2,7 +2,7 @@ import { persistEndpointBookmarkStatus } from "helpers/localStorage/bookmarkedEn import { observable } from "mobx"; import { AnnotationJson } from "resources/types/ast"; -export type RequestStatus = "notFetched" | "sucess" | "fetching" | "error"; +export type RequestStatus = "notFetched" | "success" | "fetching" | "error"; export interface ModelAnotations { func: AnnotationJson[]; @@ -82,7 +82,7 @@ export class requestModel { if (res.ok) { this.response = res.result; - this.status = "sucess"; + this.status = "success"; if (callBack) { callBack("success"); }