{"id":19091812,"url":"https://github.com/onyxblade/camille","last_synced_at":"2025-04-30T11:51:44.016Z","repository":{"id":143205141,"uuid":"607060658","full_name":"onyxblade/camille","owner":"onyxblade","description":"Type-safe data exchange between front-end and Rails","archived":false,"fork":false,"pushed_at":"2024-12-16T00:56:34.000Z","size":198,"stargazers_count":13,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2024-12-26T15:51:30.475Z","etag":null,"topics":["api","rails","typescript"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/onyxblade.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-02-27T08:22:40.000Z","updated_at":"2024-12-16T00:56:25.000Z","dependencies_parsed_at":"2024-06-17T04:36:41.358Z","dependency_job_id":"6a555fc7-208a-4a30-b6c6-1adf64981715","html_url":"https://github.com/onyxblade/camille","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/onyxblade%2Fcamille","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/onyxblade%2Fcamille/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/onyxblade%2Fcamille/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/onyxblade%2Fcamille/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/onyxblade","download_url":"https://codeload.github.com/onyxblade/camille/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":232962563,"owners_count":18603378,"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":["api","rails","typescript"],"created_at":"2024-11-09T03:17:05.570Z","updated_at":"2025-04-30T11:51:44.010Z","avatar_url":"https://github.com/onyxblade.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"# Camille\n\n![Gem Version](https://img.shields.io/gem/v/camille)\n\n## Why?\n\nTraditionally, the JSON response from a Rails API server isn't typed. So even if we have TypeScript at the front-end, we still have little guarantee that our back-end would return the correct type and structure of data.\n\nIn order to eliminate type mismatch between both ends, Camille provides a syntax for you to define type schema for your Rails API, and uses these schemas to generate the TypeScript functions for calling the API.\n\nFor example, an endpoint defined in Ruby, where `data` is a controller action,\n\n```ruby\nget :data do\n  params(\n    id: Number\n  )\n  response(\n    name: String\n  )\nend\n```\n\nwill become a function in TypeScript:\n\n```typescript\ndata(params: {id: number}): Promise\u003c{name: string}\u003e\n```\n\nTherefore, if the front-end requests the API by calling `data`, we have guarantee that `id` is presented in `params`, and Camille will require the response to contain a string `name`, so the front-end can receive the correct type of data.\n\nBy using these request functions, we also don't need to know about HTTP verbs and paths. It's impossible to have unrecognized routes, since Camille will make sure that each function handled by the correct Rails action.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'camille'\n```\n\nAnd then execute:\n\n```bash\nbundle install\nbundle exec rails g camille:install\n```\n\n## Usage\n\n### Schemas\n\nA schema defines the type of `params` and `response` for a controller action. The following commands will generate schema definition files in `config/camille/schemas`.\n\n```bash\n# to generate a schema for ProductsController\nbundle exec rails g camille:schema products\n# to generate a schema for Api::ProductController\nbundle exec rails g camille:schema api/products\n```\n\nAn example of schema definition:\n\n```ruby\nusing Camille::Syntax\n\nclass Camille::Schemas::Api::Products \u003c Camille::Schema\n  include Camille::Types\n\n  get :data do\n    params(\n      id: Number\n    )\n    response(\n      name: String\n    )\n  end\nend\n```\n\nThe `Api::Products` schema defines one endpoint `data` and its params and response type. This endpoint corresponds to the `data` action on `Api::ProductsController`. Inside the action, you can assume that `params[:id]` is a number, and you will need to `render json: {name: 'some string'}` in order to pass the typecheck.\n\nWhen generating TypeScript request functions, the `data` endpoint will become a function having the following signature:\n\n```typescript\ndata(params: {id: number}): Promise\u003c{name: string}\u003e\n```\n\nTherefore, the front-end user is required to provide an `id` when they call this function. And they can expect to get a `name` from the response of this request. There are no more type mismatch between both ends.\n\nThe `params` type for an endpoint is required to be an object type, or a hash in Ruby, while `response` type can be any supported type, for example a `Boolean`.\n\nCamille will automatically add a Rails route for each endpoint. You don't need to do anything other than having the schema file in place.\n\nWhen defining an endpoint, you can also use `post` instead of `get` for non-idempotent requests. However, no other HTTP verbs are supported, because verbs in RESTful like `patch` and `delete` indicate what we do on resources, but in RPC-style design each request is merely a function call that does not concern RESTful resources.\n\n### Custom types\n\nIn addition to primitive types, you can define custom types in Camille. The following commands will generate type definition files in `config/camille/types`.\n\n```bash\n# to generate a type named Product\nrails g camille:type product\n# to generate a type named Nested::Product\nrails g camille:type nested/product\n```\n\nAn example of custom type definition:\n\n```ruby\nusing Camille::Syntax\n\nclass Camille::Types::Product \u003c Camille::Type\n  include Camille::Types\n\n  alias_of(\n    id: Number,\n    name: String\n  )\nend\n```\n\nEach custom type is considered a type alias in TypeScript. And `alias_of` defines what this type is aliasing. In this case, the `Product` type is an alias of an object type having fields `id` as `Number` and `name` as `String`. When generating TypeScript, it will be converted to the following:\n\n```typescript\ntype Product = {id: number, name: string}\n```\n\nYou can perform a type check on a value using `check`, which can be handy in testing:\n\n```ruby\n# `check` will return either a Camille::Checked or a Camille::TypeError\nresult = Camille::Types::Product.check(hash)\nif result.checked?\n  # the hash is accepted by Camille::Types::Product type\nelse\n  p result\nend\n```\n\n### Available syntax for types\n\nCamille supports most of the type syntax in TypeScript. Below is a list of types that you can use in type and schema definition.\n\n```ruby\nparams(\n  # primitive types in TypeScript\n  number: Number,\n  string: String,\n  boolean: Boolean,\n  null: Null,\n  undefined: Undefined,\n  any: Any,\n  # an array type is a type name followed by '[]'\n  array: Number[],\n  # an object type looks like hash\n  object: {\n    field: Number\n  },\n  # an array of objects also works\n  object_array: {\n    field: Number\n  }[],\n  # a union type is two types connected by '|'\n  union: Number | String,\n  # an intersection type is two types connected by '\u0026'\n  intersection: { id: Number } \u0026 { name: String },\n  # a tuple type is several types put inside '[]'\n  tuple: [Number, String, Boolean],\n  # a field followed by '?' is optional, the same as in TypeScript\n  optional?: Number,\n  # literal types\n  number_literal: 1,\n  string_literal: 'hello',\n  boolean_literal: false,\n  # a custom type we defined above\n  product: Product,\n  # Pick and Omit accept a type and an array of symbols\n  pick: Pick[{a: 1, b: 2}, [:a, :b]],\n  omit: Omit[Product, [:id]],\n  # Record accepts a key type and a value type\n  record: Record[Number, String]\n)\n```\n\n### TypeScript generation\n\nAfter you have your types and schemas in place, you can visit `/camille/endpoints.ts` in development environment to have the TypeScript request functions generated.\n\nAn example from our previously defined type and schema will be:\n\n```typescript\nimport request from './request'\n\nexport type Product = {id: number, name: string}\n\nexport default {\n  api: {\n    data(params: {id: number}): Promise\u003c{name: string}\u003e {\n      return request('get', '/api/products/data', params)\n    }\n  }\n}\n```\n\nThe first line of `import` is configurable as `config.ts_header` in `config/camille/configuration.rb`. You would need to implement a `request` function that performs the HTTP request.\n\n### Conversion between camelCase and snake_case\n\nIn TypeScript world, people usually use camelCase to name functions and variables, while in Ruby the convention is to use snake_case. Camille will automatically convert between these two when processing request.\n\nFor example,\n\n```ruby\nget :special_data do\n  params(\n    long_id: Number\n  )\n  response(\n    long_name: String\n  )\nend\n```\n\nwill have TS signature:\n\n```typescript\nspecialData(params: {longId: number}): Promise\u003c{longName: string}\u003e\n```\n\nIn the Rails action you still use `params[:long_id]` to access the parameter and return `long_name` in response.\n\n### Typechecking\n\nIf a controller action has a corresponding schema, Camille will raise an error if the returned JSON doesn't match the response type specified in the schema.\n\nFor example for\n```ruby\nresponse(\n  object: {\n    array: Number[]\n  }\n)\n```\n\nif we return such a JSON in our action\n```ruby\nrender json: {\n  object: {\n    array: [1, 2, '3']\n  }\n}\n```\n\nCamille will print the following error:\n```\nobject:\n  array:\n    array[2]: Expected number, got \"3\".\n```\n\n### Reloading\n\nEverything in `config/camille/types` and `config/camille/schemas` will automatically reload after changes in development environment, just like other files in Rails.\n\n## Versioning\n\nThis project uses [Semantic Versioning](https://semver.org/).\n\n## Development\n\nRun tests with `bundle exec rake`.\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/onyxblade/camille.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fonyxblade%2Fcamille","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fonyxblade%2Fcamille","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fonyxblade%2Fcamille/lists"}