{"id":13446223,"url":"https://github.com/rishiosaur/idyllic","last_synced_at":"2025-04-13T21:51:40.158Z","repository":{"id":46303454,"uuid":"336073755","full_name":"rishiosaur/idyllic","owner":"rishiosaur","description":"⚡️ An unopinionated language for building APIs ridiculously fast.","archived":false,"fork":false,"pushed_at":"2023-02-26T17:35:04.000Z","size":244,"stargazers_count":54,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-27T12:11:57.029Z","etag":null,"topics":["hacktoberfest","language","web"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rishiosaur.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2021-02-04T20:26:06.000Z","updated_at":"2024-01-16T16:26:55.000Z","dependencies_parsed_at":"2024-01-13T17:15:21.575Z","dependency_job_id":"102e1e2f-cbf8-4def-9bb7-db441ce2c33d","html_url":"https://github.com/rishiosaur/idyllic","commit_stats":{"total_commits":83,"total_committers":2,"mean_commits":41.5,"dds":0.02409638554216864,"last_synced_commit":"e49768a69e5176df554b64bb3e6c21b479f50bc3"},"previous_names":[],"tags_count":13,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rishiosaur%2Fidyllic","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rishiosaur%2Fidyllic/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rishiosaur%2Fidyllic/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rishiosaur%2Fidyllic/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rishiosaur","download_url":"https://codeload.github.com/rishiosaur/idyllic/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248788855,"owners_count":21161726,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["hacktoberfest","language","web"],"created_at":"2024-07-31T05:00:48.745Z","updated_at":"2025-04-13T21:51:40.133Z","avatar_url":"https://github.com/rishiosaur.png","language":"TypeScript","funding_links":[],"categories":["Developer Utilities"],"sub_categories":[],"readme":"![Banner](assets/Banner.svg)\n# ⚡️ Idyllic\n\n[comment]: \u003c\u003e (![lint status]\u0026#40;https://github.com/rishiosaur/idyllic/workflows/lint/badge.svg\u0026#41;)\n\n[comment]: \u003c\u003e (![format status]\u0026#40;https://github.com/rishiosaur/idyllic/workflows/format/badge.svg\u0026#41;)\n\n[comment]: \u003c\u003e (![build status]\u0026#40;https://github.com/rishiosaur/idyllic/workflows/build/badge.svg\u0026#41;)\n![GitHub](https://img.shields.io/github/license/rishiosaur/idyllic)\n![GitHub issues](https://img.shields.io/github/issues/rishiosaur/idyllic)\n![GitHub contributors](https://img.shields.io/github/contributors/rishiosaur/idyllic)\n![GitHub last commit](https://img.shields.io/github/last-commit/rishiosaur/idyllic)\n![npm (scoped)](https://img.shields.io/npm/v/@idyllic/compiler?label=%40idyllic%2Fcompiler\u0026style=flat-square)\n![npm (scoped)](https://img.shields.io/npm/v/@idyllic/server?label=%40idyllic%2Fserver\u0026style=flat-square)\n\nThe specification language for building APIs ridiculously fast.\n\n\n# Table of Contents\n- [⚡️ Idyllic](#️-idyllic)\n- [Table of Contents](#table-of-contents)\n- [Getting Started](#getting-started)\n- [Manifesto.](#manifesto)\n- [Language](#language)\n  - [Definitions](#definitions)\n    - [Handlers](#handlers)\n    - [Middleware](#middleware)\n    - [Guards](#guards)\n  - [Sequences](#sequences)\n    - [Global Sequence](#global-sequence)\n    - [Fragments](#fragments)\n  - [Routes](#routes)\n    - [Defining Handlers](#defining-handlers)\n    - [Query parameters](#query-parameters)\n  - [Conclusion](#conclusion)\n- [Compiler](#compiler)\n  - [Compilation stages](#compilation-stages)\n    - [Lexing](#lexing)\n    - [Parsing](#parsing)\n    - [Desugaring](#desugaring)\n    - [Validation](#validation)\n    - [Objectification](#objectification)\n  - [API](#api)\n- [Server](#server)\n- [Contributing](#contributing)\n\n# Getting Started\n\nYou can find a tiny example of what Idyllic looks like at [the TODO example](https://github.com/rishiosaur/idyllic-todo)\n\n# Manifesto.\n\nBoilerplate is the single most disingenuous part of the Node ecosystem today. Frameworks like Express, Koa, and so many more focus on having rigid rules for how to write code instead of, well, writing it.\n\nIdyllic aims to reverse that paradigm. Instead of writing your code to follow the arbitrary conventions of a given \nframework, the Idyllic pattern allows you to write code *however you want*.\n\nIdyllic wraps Typescript functions in a web server that you define using the Idyllic Language. There aren't any \nweird functions, conventions, objects, or patterns to recognize; making an \nIdyllic web service is indistinguishable from writing normal TS.\n\n# Language\n\nIdyllic language files are denoted by the extension `.idl`, and are used to wrap Typescript functions that are \nimported from local files like any other JS module. A program written using the Idyllic Language is known as a \nsingular Idyll, and holds definitions about how data moves through the service \u0026 API routes.\n\n```\ndefine middleware { test, logger } from \"./api\"\ndefine guards { authed } from \"./api\"\ndefine handlers { getAllTodos, postTodos } from \"./api\"\n\nglobal\n  | middleware logger\n\nfragment getTodosFragment(level)\n  | guard authed(level)\n  | middleware test\n\nroute \"/todos\" {\n  | middleware test\n\n  get {\n     | expand getTodosFragment(\"user\")\n     getAll\n  }\n  \n  post {\n     | expand getTodosFragment(\"admin\")\n     postTodos\n  }\n}\n```\n\u003ccenter\u003e\u003csmall\u003eA basic example of a todo API implemented using the Idyllic Language\u003c/small\u003e\u003c/center\u003e\n\n## Definitions\n\nWe begin by importing values from a Typescript or Javascript file. In Idylls, there are three main types of \n**functions** that can be imported: handlers, middleware, and guards.\n\n### Handlers\n\nHandlers are the simplest of the three types of functions that Idyllic supports, and are quite self-explanatory—they \nreturn values based off of a given request.\n\n```typescript\nexport const getAllTodos = () =\u003e {\n    return [{\n        name: \"todo 1\",\n        completed: false\n    }]\n}\n```\n\u003ccenter\u003e\u003csmall\u003eA handler takes in a request and returns a value—this is as simple as it gets!\u003c/small\u003e\u003c/center\u003e\n\nIt's also worth noting that Idyllic servers handle serialization for you—you don't need to set any weird headers to \nmake sure that JSON is returned.\n\nYou can import handler functions into an Idyll by using `define handlers`:\n\n```\ndefine handlers { ... } from '\u003cpath\u003e'\n```\n\n### Middleware\n\nIf you come from nearly any other Node web framework, you'll also be familiar with the concept of middleware. These \nare used as 'pipes' of data—they intercept an HTTP request and change the data within it somehow, passing it down \nthe chain of middleware and guards (we'll get to this in the next section) until the request hits a handler.\n\n```typescript\nexport const loggerMiddleware = (req) =\u003e {\n    log(req)\n    return {\n        ...req,\n        logged: true\n    }\n}\n```\n\u003ccenter\u003e\u003csmall\u003eMiddleware intercepts a request and returns some augmented data for the next piece in \nthe chain.\u003c/small\u003e\u003c/center\u003e\n\nOne important thing to note in Idyllic's implementation of middleware is that there is no 'next' function—you just \nreturn the arguments that you'd like to pass to the next function in the chain.\n\nYou can import middleware functions into an Idyll by using `define middleware`:\n\n```\ndefine middleware { ... } from '\u003cpath\u003e'\n```\n\n### Guards\n\nGuards are also fairly self-explanatory—they guard requests from being executed. You can think of them as 'filters' \nfor a given request. At a low level, they are functions that take in a request and return a boolean value—if the \nguard returns `true`, then the request continues execution, and vice versa.\n\n```typescript\nexport const authGuard = async (req) =\u003e {\n    const { headers: { authorization } } = req\n    \n    return validateToken(authorization)\n}\n```\n\u003ccenter\u003e\u003csmall\u003eGuards take in a given request and return a boolean value, stopping execution if the result is false.\n\u003c/small\u003e\u003c/center\u003e\n\nUnlike middleware, guards are not meant to change the request itself. As such, Idyllic handles context for guards; \nif the guard allows further execution, the request object that was passed into the guard function will be the same \none that is passed into the next function in the chain.\n\nYou can import guard functions into an Idyll by using `define guards`:\n\n```\ndefine guards { ... } from '\u003cpath\u003e'\n```\n\n## Sequences\n\nNow that we have functions available to us in the Idyll program through the use of `define`, how do we go about \nusing them? Well, Idylls are made up of pipelines of data known as Sequences. A Sequence is just a collection of \nguard and middleware functions (and fragments, but we'll get into those later).\n\nLet's annotate the Idyll we saw from before to see where all the Sequences are:\n\n```\ndefine middleware { test, logger } from \"./api\"\ndefine guards { authed } from \"./api\"\ndefine handlers { getAllTodos, postTodos } from \"./api\"\n\nglobal\n  | middleware logger                   - [ SEQUENCE ]\n\nfragment getTodosFragment(level)\n  | guard authed(level)                 ┓\n  | middleware test                     ┻ [ SEQUENCE ]\n\nroute \"/todos\" {\n  | middleware test                     - [ SEQUENCE ]\n\n  get {\n     | expand getTodosFragment(\"user\")  - [ SEQUENCE ]\n     getAll\n  }\n  \n  post {\n     | expand getTodosFragment(\"admin\") - [ SEQUENCE ]\n     postTodos\n  }\n}\n```\n\nEach sequence defines how a request moves through the Idyll to reach a handler function (note that handlers are not \npart of Sequences). You can find them pretty much everywhere in Idylls—they are the bulk of the language.\n\nSequences can be defined in Fragments, Routes, Request Handlers (separate from handler functions), and the Global \nSequence.\n\nThe form of a singular Sequence node is `| \u003ctype\u003e \u003cidentifier\u003e (\u003carguments\u003e)`. \n\nIf we want a given piece of data to flow through a guard called `authed`, we can write something like:\n```\n| guard authed\n```\nWe may also pass in arguments (arguments can be numbers or strings) to this guard, in case \nwe'd like to \ncustomize behaviour (these arguments will get \npassed into the corresponding function:\n```\n| guard authed(\"user\")\n```\n\nThe rules for middleware apply here as well, but instead of using the `guard` keyword, you should use the \n`middleware` keyword.\n\nWhere and how can we actually define these sequences, though?\n\n### Global Sequence\n\nOftentimes, you'll want to put some middleware in front of every request that the server handles (like a logger or \nan authentication guard). This is where the `global` keyword comes in.\n\nTo define a sequence that gets executed for every single request, you can write one directly under the `global` name \n(note: this is a reserved keyword):\n\n```\ndefine middleware { test, logger } from \"./api\"\n[...]\n\nglobal\n  | middleware logger\n  \n[...]      \n```\n\nWith a program like this, `logger` will be executed on each request.\n\n### Fragments\n\nWriting sequences all over the place can get tiring. That's why the Idyllic Language has first-class macro support!\n\nIf you're coming from GraphQL, this bit should come naturally—macros act the exact same way as they do over there. \nHowever, Idyllic macros are even more powerful because of their parameter support.\n\nDefining a Fragment is as easy as using the `fragment` keyword and adding an identifier and your sequence:\n\nTo use a Fragment, just include it in any non-Fragment sequence using the `expand` keyword the same way you would a \nGuard or Middleware function:\n\n```\n[...]\n\nfragment GetTodosAuthedAdmin\n  | guard authed(\"admin\")\n  | middleware test\n  \nfragment GetTodosAuthedUser\n  | guard authed(\"user\")\n  | middleware test\n  \nroute \"/todos\" {\n  get {\n     | expand GetTodosAuthedAdmin\n     getAll\n  }\n}\n\nroute \"/todos/{id}\" {\n  get {\n     | expand GetTodosAuthedUser\n     getAll\n  }\n}\n```\n\nWhile this example follows terrible UX, it shows the function of Fragments quite well. In the compilation stage of \nan Idyll, this program would get expanded to:\n\n```\n[...]\n  \nroute \"/todos\" {\n  get {\n      | guard authed(\"admin\")\n      | middleware test\n     getAll\n  }\n}\n\nroute \"/todos/{id}\" {\n  get {\n     | guard authed(\"user\")\n     | middleware test\n     getAll\n  }\n}\n```\n\nFragments are a purely Idyllic construct—they are not preserved in the compiled object, and there is a separate \ncompiler pass stage exclusively for rendering them out.\n\nAnyways, there is one (albeit of many) glaring problem with the fragments that we wrote in the last example: it's \nrepetitive. Instead of defining two different fragments for a specific auth level change, let's use parameters to \ngeneralize a fragment:\n\n```\n[...]\n\nfragment GetTodosAuthed(level)\n  | guard authed(level)\n  | middleware test\n  \nroute \"/todos\" {\n  get {\n     | expand GetTodosAuthed(\"admin\")\n     getAll\n  }\n}\n\nroute \"/todos/{id}\" {\n  get {\n     | expand GetTodosAuthed(\"user\")\n     getAll\n  }\n}\n```\n\nThere we go. That's much more concise! This will get expanded out to the exact same program that we saw earlier.\n\n## Routes\n\nNo web framework would be complete without some form of router support, and Idyllic is no different; they're a core \npart of the language.\n\nDefining a route requires the `route` keyword, a string literal with the route itself, the route's sequence, and the \nvarious handlers associated with it:\n```\n[...]\n\nroute \"/todos\" {\n  | middleware test \n\n  get {\n     | expand getTodosFragment(\"user\")\n     getAll\n  }\n  \n  post {\n     | expand getTodosFragment(\"admin\")\n     postTodos\n  }\n}\n\nroute \"/todos/{id}\" {\n  get {\n     | guard authed(\"user\")\n     | middleware test\n     getAll\n  }\n}\n```\n\nThis program would create an API with two routes: `/todos` and `/todos/{id}`, the latter of which runs the \nmiddleware called `test` before all requests to it (remember, Sequences can be as long as you'd like; sequences for \nRoutes run before all requests). Let's dissect this a little bit further!\n\n### Defining Handlers \n\nIn each of the two routes' brace pairs (`\"{\"` and `\"}\"`), we can see their corresponding sequences \u0026 some new code \nblocks:\n\n```\n[...]\n  get {\n     | expand getTodosFragment(\"user\")\n     getAll\n  }\n  \n  post {\n     | expand getTodosFragment(\"admin\")\n     postTodos\n  }\n  \n  [...]\n```\n\u003ccenter\u003e\u003csmall\u003eWhat are these code blocks?\n\u003c/small\u003e\u003c/center\u003e\n\nEvery route can be queried in a number of different ways—in REST, there are exactly 5 different request types: GET, \nPOST, PATCH, PUT, and DELETE. We can define behaviour for each type of request a given route supports by defining \nHandlers. This is also where the function handlers we defined at the beginning of the program go (remember, you \ncan't add handler functions in fragments)!\n\nA Handler must have a request type (this is the first thing in its definition) and a handler function. We can also \ndefine Sequences to be run before a request of a given type simply by adding nodes right after our opening brace.\n\nHandlers can also have arguments passed to them—these are defined in the exact same way as you might pass arguments \nto a middleware or guard function.\n\n### Query parameters\n\nIt's also useful to have some form of parameter support for the routes themselves—if we'd like to query just one \ntodo by ID, we might send a GET request to `/todos/3`.\n\nWe can define query parameters by using brace pairs in the route's associated string:\n\n```\nroute \"/todos/{id}\" {\n  get {\n     | guard authed(\"user\")\n     | middleware test\n     getAll\n  }\n}\n```\n\n## Conclusion\n\nThat's pretty much it as far as the Idyllic Language specification goes! Let's recap all the major features of the \nlanguage:\n\n- Static typing with Typescript \u0026 definition types\n- Parameterized, first-class macro support with Fragments\n- Data pipelines with Sequences\n- First-class support for Middleware and Guards\n- Query parameter capturing\n- Request type definitions\n\n# Compiler\n\nOf course, understanding the language would be useless without a compiler to actually use that knowledge with. \nLuckily, this repository includes a high-performance implementation of the language to get started!\n\n## Compilation stages\n\nThe compilation consists of 5 main stages: lexing/tokenization, parsing, desugaring, validation, and objectification.\n\n### Lexing\n\nLexing merely translates a source string to an array of `Token`s: a `Token` is merely a class that consists of a \nposition, literal and type.\n\n### Parsing\n\nThis is where things start getting interesting. In this stage, the compiler turns the static array of `Tokens` into \na large tree of `AstNode`-implementing classes. An \"abstract syntax tree\" is constructed—this is where the arbitrary \ntokens start to take on meaning.\n\n### Desugaring\n\nThis is the stage of compilation where macros (Fragments) are expanded into various Sequences around the programs. \nNothing super interesting; the compiler just walks itself through the tree constructed in the Parsing stage. After \nall Fragments have been expanded out, they then get deleted from the tree (they serve no purpose after expansion, \nand removal keeps runtime memory usage to a minimum)\n\n### Validation\n\nAfter desugaring, the compiler walks through the `definition`s of middleware, handler, and guard functions and \ndynamically imports them from the associated Typescript function. It then goes through and validates identifiers, \ntypes, and Sequences used in the program.\n\n### Objectification\n\nThe final stage of compilation turns the validated abstract syntax tree and constructs a \"concrete syntax tree,\" and \nthen simplifies everything down to one singular object of the form:\n\n```typescript\n\nexport interface ConcreteSequenceNode {\n\tinterop: Function\n\tname: string\n\ttype: TokenType\n\targuments: (string | number)[]\n}\n\ninterface CompiledTree {\n\tglobal: ConcreteSequenceNode[]\n\troutes: {\n\t\t[id: string]: {\n\t\t\trequests: {\n\t\t\t\t[requestType: string]: {\n\t\t\t\t\tsequence: ConcreteSequenceNode[]\n\t\t\t\t\thandler: {\n\t\t\t\t\t\tinterop: Function\n\t\t\t\t\t\tname: string\n\t\t\t\t\t\targuments: (string | number)[]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tsequence: ConcreteSequence\n\t\t}\n\t}\n}\n```\n\nThis final compilation stage turns the syntax tree into an easily traversible API-like tree, with intuitive bindings \nfor all API-specific functionality. It holds references to all Typescript functions, so we can call any handler, \nmiddleware or guard functions right from the object itself.\n\n## API\n\nAll of this functionality is held within the [`@idyllic/compiler`](/packages/compiler) package, which exposes a \nsimple `IdyllicCompiler` class. Other types are also available by hooking into `@idyllic/compiler/dist/...`.\n\nTo compile an Idyllic program directly from a file path, we may write a simple asynchronous Node snippet:\n\n```typescript\nimport { IdyllicCompiler } from \"@idyllic/compiler\";\n\n(async () =\u003e {\n    \n    // The fromFile static method reads the file into a string for us\n    const compiler = await IdyllicCompiler.fromFile(\"ast.idl\") \n    \n    // The compile method executes all 5 stages of compilation automatically.\n    const compiled = await compiler.compile()\n    \n})()\n```\n\nOf course, the `constructor` for `IdyllicCompiler` takes in a `string` as its only argument if you'd like to send in \nan Idyll constructed differently.\n\n# Server\n\nThis repository also includes an extremely fast web server that complements the compilation stage. While it isn't \nprofessionally benchmarked (yet), you can expect response times similar to (and in many cases, beating) Express.\n\nTo use this server, you can import the `IdyllicServer` class from the [`@idyllic/server`](/packages/server) package.\n\nUsing this server is as easy as adding a few lines to the reference program we wrote earlier:\n\n```typescript\nimport { IdyllicCompiler } from \"@idyllic/compiler\";\nimport { IdyllicServer } from \"@idyllic/server\";\n\n(async () =\u003e {\n    \n    // The fromFile static method reads the file into a string for us\n    const compiler = await IdyllicCompiler.fromFile(\"ast.idl\")\n\n    // The compile method executes all 5 stages of compilation automatically.\n    const compiled = await compiler.compile()\n    \n    // The server constructor takes in a compiled Idyllic object.\n    const server = new IdyllicServer(compiled)\n    \n    // The start function takes in a port number (defaults to 3000) and a function to be executed on start.\n    server.start(3000, () =\u003e {\n        console.log(\"Idyllic server has started!\")\n    })\n\n})()\n```\n\nThis server implementation uses Node's native `http` package, but because the Idyllic compiler always returns the \nsame implementation, you can write your own bindings for any Node HTTP server library out there!\n\n# Contributing\n\nContributions are always welcome—Idyllic is a massive project, and it's always nice to have some help from awesome \npeople like you :)\n\nTo get started, make sure to follow the instructions in the [Contributing file](CONTRIBUTING.md), and always \nremember to act according to the [Code of Conduct](CODE_OF_CONDUCT.md).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frishiosaur%2Fidyllic","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frishiosaur%2Fidyllic","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frishiosaur%2Fidyllic/lists"}