{"id":13483782,"url":"https://github.com/nullobject/rein","last_synced_at":"2025-05-15T11:06:49.952Z","repository":{"id":1038506,"uuid":"867718","full_name":"nullobject/rein","owner":"nullobject","description":"Database constraints made easy for ActiveRecord.","archived":false,"fork":false,"pushed_at":"2020-10-27T00:02:46.000Z","size":307,"stargazers_count":670,"open_issues_count":10,"forks_count":31,"subscribers_count":9,"default_branch":"master","last_synced_at":"2025-04-14T18:12:46.890Z","etag":null,"topics":["activerecord","constraints","database","library","postgres","rails","ruby"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nullobject.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2010-08-28T02:07:57.000Z","updated_at":"2024-10-26T11:11:42.000Z","dependencies_parsed_at":"2022-07-17T09:30:42.120Z","dependency_job_id":null,"html_url":"https://github.com/nullobject/rein","commit_stats":null,"previous_names":[],"tags_count":15,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nullobject%2Frein","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nullobject%2Frein/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nullobject%2Frein/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nullobject%2Frein/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nullobject","download_url":"https://codeload.github.com/nullobject/rein/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248933340,"owners_count":21185460,"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":["activerecord","constraints","database","library","postgres","rails","ruby"],"created_at":"2024-07-31T17:01:15.213Z","updated_at":"2025-04-14T18:12:50.720Z","avatar_url":"https://github.com/nullobject.png","language":"Ruby","readme":"# Rein\n\n[![Build Status](https://travis-ci.com/nullobject/rein.svg?branch=master)](https://travis-ci.com/nullobject/rein)\n\n[Data integrity](http://en.wikipedia.org/wiki/Data_integrity) is a good thing.\nConstraining the values allowed by your application at the database-level,\nrather than at the application-level, is a more robust way of ensuring your\ndata stays sane.\n\nUnfortunately, ActiveRecord doesn't encourage (or even allow) you to use\ndatabase integrity without resorting to hand-crafted SQL. Rein (pronounced\n\"rain\") adds a handful of methods to your ActiveRecord migrations so that you\ncan easily tame the data in your database.\n\nAll methods in the DSL are automatically *reversible*, so you can take\nadvantage of reversible Rails migrations.\n\n## Table of Contents\n\n* [Getting Started](#getting-started)\n* [Constraint Types](#constraint-types)\n  * [Summary](#summary)\n  * [Foreign Key Constraints](#foreign-key-constraints)\n  * [Unique Constraints](#unique-constraints)\n  * [Exclusion Constraints](#exclusion-constraints)\n  * [Inclusion Constraints](#inclusion-constraints)\n  * [Length Constraints](#length-constraints)\n  * [Match Constraints](#match-constraints)\n  * [Numericality Constraints](#numericality-constraints)\n  * [Presence Constraints](#presence-constraints)\n  * [Null Constraints](#null-constraints)\n  * [Check Constraints](#check-constraints)\n  * [Validate Constraints](#validate-constraints)\n* [Data Types](#data-types)\n  * [Enumerated Types](#enumerated-types)\n* [Views](#views)\n* [Schemas](#schemas)\n* [Examples](#examples)\n* [Contribute](#contribute)\n* [License](#license)\n\n## Getting Started\n\nInstall the gem:\n\n```\n\u003e gem install rein\n```\n\nAdd a constraint to your migrations:\n\n```ruby\nclass CreateAuthorsTable \u003c ActiveRecord::Migration\n  def change\n    create_table :authors do |t|\n      t.string :name, null: false\n    end\n\n    # An author must have a name.\n    add_presence_constraint :authors, :name\n  end\nend\n```\n\n## Constraint Types\n\n### Summary\n\nThe table below summarises the constraint operations provided by Rein and whether they support [validation](#validate-constraints).\n\n| Rein name   | Rein method | SQL | Supports `NOT VALID`? |\n| ----------- | ----------- | --- | --------------------- |\n| Foreign Key | `add_foreign_key_constraint` | `FOREIGN KEY` | yes |\n| Unique | `add_unique_constraint` | `UNIQUE` | no |\n| Exclusion | `add_exclusion_constraint` | `EXCLUDE` | no |\n| Inclusion | `add_inclusion_constraint` | `CHECK` | yes |\n| Length | `add_length_constraint` | `CHECK` | yes |\n| Match | `add_match_constraint` | `CHECK` | yes |\n| Numericality | `add_numericality_constraint` | `CHECK` | yes |\n| Presence | `add_presence_constraint` | `CHECK` | yes |\n| Null | `add_null_constraint` | `CHECK` | yes |\n| Check | `add_check_constraint` | `CHECK` | yes |\n\n### Foreign Key Constraints\n\nA foreign key constraint specifies that the values in a column must match the\nvalues appearing in some row of another table.\n\nFor example, let's say that we want to constrain the `author_id` column in the\n`books` table to one of the `id` values in the `authors` table:\n\n```ruby\nadd_foreign_key_constraint :books, :authors\n```\n\nAdding a foreign key doesn't automatically create an index on the referenced\ncolumn. Having an index will generally speed up any joins you perform on the\nforeign key. To create an index you can specify the `index` option:\n\n```ruby\nadd_foreign_key_constraint :books, :authors, index: true\n```\n\nRein will automatically infer the column names for the tables, but if we need\nto be explicit we can using the `referenced` and `referencing` options:\n\n```ruby\nadd_foreign_key_constraint :books, :authors, referencing: :author_id, referenced: :id\n```\n\nWe can also specify the behaviour when one of the referenced rows is updated or\ndeleted:\n\n```ruby\nadd_foreign_key_constraint :books, :authors, on_delete: :cascade, on_update: :cascade\n```\n\nHere's all the options for specifying the delete/update behaviour:\n\n- `no_action`: if any referencing rows still exist when the constraint is\n  checked, an error is raised; this is the default behavior if you do not\n  specify anything.\n- `cascade`: when a referenced row is deleted, row(s) referencing it should be\n  automatically deleted as well.\n- `set_null`: sets the referencing columns to be nulls when the referenced row\n  is deleted.\n- `set_default`: sets the referencing columns to its default values when the\n  referenced row is deleted.\n- `restrict`: prevents deletion of a referenced row.\n\nTo remove a foreign key constraint:\n\n```ruby\nremove_foreign_key_constraint :books, :authors\n```\n\n### Unique Constraints\n\nA unique constraint specifies that certain columns in a table must be unique.\n\nFor example, all the books should have unique ISBNs:\n\n```ruby\nadd_unique_constraint :books, :isbn\n```\n\nBy default, the database checks unique constraints immediately (i.e. as soon as\na record is created or updated). If a record with a duplicate value exists,\nthen the database will raise an error.\n\nSometimes it is necessary to wait until the end of a transaction to do the\nchecking (e.g. maybe you want to swap the ISBNs for two books). To do so, you\nneed to tell the database to *defer* checking the constraint until the end of\nthe current transaction:\n\n```sql\nBEGIN;\nSET CONSTRAINTS books_isbn_unique DEFERRED;\nUPDATE books SET isbn = 'foo' WHERE id = 1;\nUPDATE books SET isbn = 'bar' WHERE id = 2;\nCOMMIT;\n```\n\nThis [blog\npost](https://hashrocket.com/blog/posts/deferring-database-constraints) offers\na good explanation of how to do this in a Rails app when using the\n`acts_as_list` plugin.\n\nIf you *always* want to defer checking a unique constraint, then you can set\nthe `deferred` option to `true`:\n\n```ruby\nadd_unique_constraint :books, :isbn, deferred: true\n```\n\nIf you really don't want the ability to optionally defer a unique constraint in\na transaction, then you can set the `deferrable` option to `false`:\n\n```ruby\nadd_unique_constraint :authors, :name, deferrable: false\n```\n\n### Exclusion Constraints\n\nAn exclusion constraint is a lot like a unique constraint, but more general.\nWhereas a unique constraint forbids two rows from having all constrained\ncolumns be *equal*, an exclusion constraint forbids two rows from having all\nconstrained columns be *some relationship*, where the relationship is up to you\n(and can be different for each column).  For instance you can prevent two\nranges from overlapping with the `\u0026\u0026` operator.  You can read more in [the\nPostgres\ndocs](https://www.postgresql.org/docs/9.0/static/ddl-constraints.html#DDL-CONSTRAINTS-EXCLUSION)\nor [a slideshow by the author, Jeff\nDavis](https://www.slideshare.net/pgconf/not-just-unique-exclusion-constraints).\n\nFor example, no two people should own copyright to a book at the same time:\n\n```ruby\nadd_exclusion_constraint :book_owners, [[:book_id, '='], [:owned_during, '\u0026\u0026']], using: :gist\n```\n\nBy default, the database checks exclusion constraints immediately (i.e. as soon\nas a record is created or updated). If a record with an excluded value exists,\nthen the database will raise an error.\n\nSometimes it is necessary to wait until the end of a transaction to do the\nchecking (e.g. maybe you want to move the date a copyright changed hands). To\ndo so, you need to tell the database to *defer* checking the constraint until\nthe end of the current transaction:\n\n```sql\nBEGIN;\nSET CONSTRAINTS book_owners_exclude DEFERRED;\nUPDATE book_owners\n  SET owned_during = tsrange(lower(owned_during), '1943-12-22')\n  WHERE book_id = 1 AND owner_id = 1;\nUPDATE book_owners\n  SET owned_during = tsrange('1943-12-22', upper(owned_during))\n  WHERE book_id = 1 AND owner_id = 2;\nCOMMIT;\n```\n\nIf you *always* want to defer checking a unique constraint, then you can set\nthe `deferred` option to `true`:\n\n```ruby\nadd_exclusion_constraint :book_owners, [[:book_id, '='], [:owned_during, '\u0026\u0026']], using: :gist, deferred: true\n```\n\nIf you really don't want the ability to optionally defer a unique constraint in\na transaction, then you can set the `deferrable` option to `false`:\n\n```ruby\nadd_exclusion_constraint :book_owners, [[:book_id, '='], [:owned_during, '\u0026\u0026']], using: :gist, deferrable: false\n```\n\nIf you want to specify something like a operator class for a attribute specific\nattribute, you can add it to the attribute specification between attribute name\nand the operator:\n\n```ruby\nadd_exclusion_constraint :book_owners, [[:book_id, :gist_int8_ops, '='], [:owned_during, '\u0026\u0026']], using: :gist\n```\n\nIf you want to set the constraint to a subset of the table, you can use a `where`\noption to set the filter condition:\n\n```ruby\nadd_exclusion_constraint :books, [[:isbn, '=']], using: :gist, where: \"state='active'\"\n```\n\n### Inclusion Constraints\n\nAn inclusion constraint specifies the possible values that a column value can\ntake.\n\nFor example, we can ensure that `state` column values can only ever be\n`available` or `on_loan`:\n\n```ruby\nadd_inclusion_constraint :books, :state, in: %w[available on_loan]\n```\n\nTo remove an inclusion constraint:\n\n```ruby\nremove_inclusion_constraint :books, :state\n```\n\nYou may also include an `if` option to enforce the constraint only under\ncertain conditions, like so:\n\n```ruby\nadd_inclusion_constraint :books, :state,\n  in: %w[available on_loan],\n  if: \"deleted_at IS NULL\"\n```\n\nYou may optionally provide a `name` option to customize the name:\n\n```ruby\nadd_inclusion_constraint :books, :state,\n  in: %w[available on_loan],\n  name: \"books_state_is_valid\"\n```\n\n### Length Constraints\n\nA length constraint specifies the range of values that the length of a string\ncolumn value can take.\n\nFor example, we can ensure that the `call_number` can only ever be a\nvalue between 1 and 255:\n\n```ruby\nadd_length_constraint :books, :call_number,\n  greater_than_or_equal_to: 1,\n  less_than_or_equal_to: 255\n```\n\nHere's all the options for constraining the values:\n\n- `equal_to`\n- `not_equal_to`\n- `less_than`\n- `less_than_or_equal_to`\n- `greater_than`\n- `greater_than_or_equal_to`\n\nYou may also include an `if` option to enforce the constraint only under\ncertain conditions, like so:\n\n```ruby\nadd_length_constraint :books, :call_number,\n  greater_than_or_equal_to: 1,\n  less_than_or_equal_to: 12,\n  if: \"status = 'published'\"\n```\n\nYou may optionally provide a `name` option to customize the name:\n\n```ruby\nadd_length_constraint :books, :call_number,\n  greater_than_or_equal_to: 1,\n  less_than_or_equal_to: 12,\n  name: \"books_call_number_is_valid\"\n```\n\nTo remove a length constraint:\n\n```ruby\nremove_length_constraint :books, :call_number\n```\n\n### Match Constraints\n\nA match constraint ensures that a string column value matches (or does not match)\na POSIX-style regular expression.\n\nFor example, we can ensure that the `title` can only contain printable ASCII\ncharacters, but not ampersands:\n\n```ruby\nadd_match_constraint :books, :title, accepts: '\\A[ -~]*\\Z', rejects: '\u0026'\n```\n\nMatch constraints are case-sensitive. You make them case-insensitive by using \n`accepts_case_insensitive` and `rejects_case_insensitive` instead of `accepts` \nor `rejects`.\n\nIf you only want to enforce the constraint under certain conditions,\nyou can pass an optional `if` option:\n\n```ruby\nadd_match_constraint :books, :title, accepts: '\\A[ -~]*\\Z', if: \"status = 'published'\"\n```\n\nYou may optionally provide a `name` option to customize the name:\n\n```ruby\nadd_match_constraint :books, :title, name: \"books_title_is_valid\"\n```\n\nTo remove a match constraint:\n\n```ruby\nremove_match_constraint :books, :title\n```\n\n### Numericality Constraints\n\nA numericality constraint specifies the range of values that a numeric column\nvalue can take.\n\nFor example, we can ensure that the `publication_month` can only ever be a\nvalue between 1 and 12:\n\n```ruby\nadd_numericality_constraint :books, :publication_month,\n  greater_than_or_equal_to: 1,\n  less_than_or_equal_to: 12\n```\n\nHere's all the options for constraining the values:\n\n- `equal_to`\n- `not_equal_to`\n- `less_than`\n- `less_than_or_equal_to`\n- `greater_than`\n- `greater_than_or_equal_to`\n\nYou may also include an `if` option to enforce the constraint only under\ncertain conditions, like so:\n\n```ruby\nadd_numericality_constraint :books, :publication_month,\n  greater_than_or_equal_to: 1,\n  less_than_or_equal_to: 12,\n  if: \"status = 'published'\"\n```\n\nYou may optionally provide a `name` option to customize the name:\n\n```ruby\nadd_numericality_constraint :books, :publication_month,\n  greater_than_or_equal_to: 1,\n  less_than_or_equal_to: 12,\n  name: \"books_publication_month_is_valid\"\n```\n\nTo remove a numericality constraint:\n\n```ruby\nremove_numericality_constraint :books, :publication_month\n```\n\n### Presence Constraints\n\nA presence constraint ensures that a string column value is non-empty.\n\nA `NOT NULL` constraint will be satisfied by an empty string, but sometimes you\nmay want to ensure that there is an actual value for a string:\n\n```ruby\nadd_presence_constraint :books, :title\n```\n\nIf you only want to enforce the constraint under certain conditions,\nyou can pass an optional `if` option:\n\n```ruby\nadd_presence_constraint :books, :isbn, if: \"status = 'published'\"\n```\n\nYou may optionally provide a `name` option to customize the name:\n\n```ruby\nadd_presence_constraint :books, :isbn, name: \"books_isbn_is_valid\"\n```\n\nTo remove a presence constraint:\n\n```ruby\nremove_presence_constraint :books, :title\n```\n\n### Null Constraints\n\nA null constraint ensures that a column does *not* contain a null value. This\nis the same as adding `NOT NULL` to a column, the difference being that it can\nbe _applied conditionally_.\n\nFor example, we can add a constraint to enforce that a book has a `due_date`,\nbut only if it's `on_loan`:\n\n```ruby\nadd_null_constraint :books, :due_date, if: \"state = 'on_loan'\"\n```\n\nTo remove a null constraint:\n\n```ruby\nremove_null_constraint :books, :due_date\n```\n\n### Check Constraints\n\nA check constraint lets you enforce any predicate about the current row.\nYou can use this if none of the other higher-level constraint types work for you.\n\nFor example, we can add a constraint to enforce that a book's title\nnever starts with an \"r\":\n\n```ruby\nadd_check_constraint :books, \"substring(title FROM 1 FOR 1) IS DISTINCT FROM 'r'\", name: 'no_r_titles'\n```\n\nNote these types must have a `name` option.\n\nTo remove a check constraint:\n\n```ruby\nremove_check_constraint :books, \"substring(title FROM 1 FOR 1) IS DISTINCT FROM 'r'\", name: 'no_r_titles'\n```\n\n### Validate Constraints\n\nAdding a constraint can be a very costly operation, especially on larger tables, as the database has to scan all rows in the table to check for violations of the new constraint. During this time, concurrent writes are blocked as an [`ACCESS EXCLUSIVE`](https://www.postgresql.org/docs/current/explicit-locking.html#LOCKING-TABLES) table lock is taken. In addition, adding a foreign key constraint obtains a `SHARE ROW EXCLUSIVE` lock on the referenced table. See the [docs](https://www.postgresql.org/docs/current/sql-altertable.html#SQL-ALTERTABLE-NOTES) for more details.\n\nIn order to allow constraints to be added concurrently on larger tables, and to allow the addition of constraints on tables containing rows with existing violations, Postgres supports adding constraints using the `NOT VALID` option (currently only for `CHECK` and foreign key constraints).\n\nThis allows the constraint to be added immediately, without validating existing rows, but enforcing the constraint for any new rows and updates. After that, a `VALIDATE CONSTRAINT` command can be issued to verify that existing rows satisfy the constraint, which is done in a way that does not lock out concurrent updates and \"with the least impact on other work\".\n\nRein supports adding `CHECK` and foreign key constraints with the `NOT VALID` option by passing `validate: false` to the options of the supported Rein DSL methods, [summarised above](#summary).\n\n```ruby\nadd_null_constraint :books, :due_date, if: \"state = 'on_loan'\", validate: false\n```\n\nWith Rails 5.2 or later, you can use [`validate_constraint`](https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/PostgreSQL/SchemaStatements.html#method-i-validate_constraint) in a subsequent migration to validate a `NOT VALID` constraint. If you are using versions of Rails below 5.2, you can use Rein's `validate_table_constraint` method:\n\n```ruby\nvalidate_table_constraint :books, \"no_r_titles\"\n```\n\nIt's safe (a no-op) to validate a constraint that is already marked as valid.\n\n### Side note on `lock_timeout`\n\nIt's advisable to set a [sensibly low `lock_timeout`](https://gocardless.com/blog/zero-downtime-postgres-migrations-the-hard-parts/) in your database migrations, otherwise existing long-running transactions can prevent your migration from acquiring the required locks, resulting in a lock queue that prevents even selects on the target table, potentially bringing your production database grinding to a halt.\n\n## Data Types\n\n### Enumerated Types\n\nAn enum is a data type that represents a static, ordered set of values.\n\n```ruby\ncreate_enum_type :book_type, %w[paperback hardcover]\n```\n\nTo drop an enum type from the database:\n\n```ruby\ndrop_enum_type :book_type\n```\n\n## Views\n\nA view is a named query that you can refer to just like an ordinary table. You\ncan even create ActiveRecord models that are backed by views in your database.\n\nFor example, we can define an `available_books` view that returns only the\nbooks which are currently available:\n\n```ruby\ncreate_view :available_books, \"SELECT * FROM books WHERE state = 'available'\"\n```\n\nTo drop a view from the database:\n\n```ruby\ndrop_view :available_books\n```\n\n## Schemas\n\nA database can contain one or more named schemas, which in turn contain tables.\nSometimes it might be helpful to split your database into multiple schemas to\nlogically group tables together.\n\n```ruby\ncreate_schema :archive\n```\n\nTo drop a schema from the database:\n\n```ruby\ndrop_schema :archive\n```\n\n## Examples\n\nLet's have a look at some example migrations to constrain database values for\nour simple library application:\n\n```ruby\nclass CreateAuthorsTable \u003c ActiveRecord::Migration\n  def change\n    # The authors table contains all the authors of the books in the library.\n    create_table :authors do |t|\n      t.string :name, null: false\n      t.timestamps, null: false\n    end\n\n    # An author must have a name.\n    add_presence_constraint :authors, :name\n  end\nend\n\nclass CreateBooksTable \u003c ActiveRecord::Migration\n  def change\n    # The books table contains all the books in the library, and their state\n    # (i.e. whether they are on loan or available).\n    create_table :books do |t|\n      t.belongs_to :author, null: false\n      t.string :title, null: false\n      t.string :state, null: false\n      t.integer :published_year, null: false\n      t.integer :published_month, null: false\n      t.date :due_date\n      t.timestamps, null: false\n    end\n\n    # A book should always belong to an author. The database should\n    # automatically delete an author's books when we delete an author.\n    add_foreign_key_constraint :books, :authors, on_delete: :cascade\n\n    # A book must have a non-empty title.\n    add_presence_constraint :books, :title\n\n    # State is always either \"available\", \"on_loan\", or \"on_hold\".\n    add_inclusion_constraint :books, :state, in: %w[available on_loan on_hold]\n\n    # Our library doesn't deal in classics.\n    add_numericality_constraint :books, :published_year,\n      greater_than_or_equal_to: 1980\n\n    # Month is always between 1 and 12.\n    add_numericality_constraint :books, :published_month,\n      greater_than_or_equal_to: 1,\n      less_than_or_equal_to: 12\n\n    # A book has a due date if it is on loan.\n    add_null_constraint :books, :due_date, if: \"state = 'on_loan'\"\n  end\nend\n\nclass CreateArchivedBooksTable \u003c ActiveRecord::Migration\n  def change\n    # The archive schema contains all of the archived data. We want to keep\n    # this separate from the public schema.\n    create_schema :archive\n\n    # The archive.books table contains all the achived books.\n    create_table \"archive.books\" do |t|\n      t.belongs_to :author, null: false\n      t.string :title, null: false\n    end\n\n    # A book should always belong to an author. The database should prevent us\n    # from deleteing an author who has books.\n    add_foreign_key_constraint \"archive.books\", :authors, on_delete: :restrict\n\n    # A book must have a non-empty title.\n    add_presence_constraint \"archive.books\", :title\n  end\nend\n```\n\n## Contribute\n\nPRs are always welcome! :heart: To work with rein, there is a\n[Makefile](https://en.wikipedia.org/wiki/Makefile) to keep things simple.\n\nBefore you do anything, you'll need to bootstrap your environment:\n\n    make config\n\nMake sure you run the tests before submitting a PR:\n\n    make test\n\n## License\n\nRein is licensed under the [MIT License](/LICENSE.md).\n","funding_links":[],"categories":["Ruby","Database Tools"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnullobject%2Frein","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnullobject%2Frein","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnullobject%2Frein/lists"}