access_grant 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.codegraph/.gitignore +5 -0
- data/.rspec +3 -0
- data/.rubocop.yml +98 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +33 -0
- data/CONTRIBUTING.md +99 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +21 -0
- data/README.md +123 -0
- data/Rakefile +12 -0
- data/docs/architecture.md +1157 -0
- data/docs/proposal.md +143 -0
- data/docs/superpowers/plans/2026-09-08-access-grant-v1.md +468 -0
- data/docs/superpowers/plans/2026-09-08-gem-release.md +367 -0
- data/docs/superpowers/specs/2026-09-05-owner-role-design.md +271 -0
- data/docs/superpowers/specs/2026-09-07-proposal-review.md +71 -0
- data/docs/superpowers/specs/2026-09-07-usage-scenarios.md +301 -0
- data/docs/superpowers/specs/2026-09-08-gem-release-design.md +82 -0
- data/lib/access_grant/catalog/dsl.rb +138 -0
- data/lib/access_grant/catalog.rb +76 -0
- data/lib/access_grant/configuration.rb +55 -0
- data/lib/access_grant/controller_methods.rb +104 -0
- data/lib/access_grant/models/permission.rb +36 -0
- data/lib/access_grant/models/role.rb +152 -0
- data/lib/access_grant/models/role_permission.rb +11 -0
- data/lib/access_grant/owner.rb +144 -0
- data/lib/access_grant/permission_key.rb +29 -0
- data/lib/access_grant/railtie.rb +17 -0
- data/lib/access_grant/recovery.rb +90 -0
- data/lib/access_grant/sync.rb +68 -0
- data/lib/access_grant/tenant.rb +47 -0
- data/lib/access_grant/user.rb +102 -0
- data/lib/access_grant/version.rb +5 -0
- data/lib/access_grant.rb +125 -0
- data/lib/generators/access_grant/install/install_generator.rb +22 -0
- data/lib/generators/access_grant/install/templates/create_access_grant_tables.rb.tt +39 -0
- data/lib/generators/access_grant/setup/setup_generator.rb +188 -0
- data/lib/generators/access_grant/setup/templates/access_grant.rb.tt +80 -0
- data/lib/generators/access_grant/setup/templates/create_access_grant_user_roles.rb.tt +14 -0
- data/lib/generators/access_grant/setup/templates/permissions.rb.tt +10 -0
- data/lib/generators/access_grant/setup/templates/roles.rb.tt +27 -0
- data/lib/tasks/access_grant_tasks.rake +27 -0
- metadata +121 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 286d6345a4ae68c6b87490f90d069e25cd7ea54c74543814ddee2d408083e451
|
|
4
|
+
data.tar.gz: f00a934a2747d2a5182abca43d85cda476854e4bd68086b86076ad60ad9a0f68
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 28ebe5a51b2ac76094c5fd207d9f720f8763f3bed803752b9a09ed9da0fa4527375c52c27eed75ad63fd877ca03f0d11e0e003b9d772118e3c3628cb009e58e0
|
|
7
|
+
data.tar.gz: 5e5c81eb84665e3b9da6dde01ebbcb83f9ed6ca24a78b4ec55410f22330c54feda960245d9ada5c6cdaea195c1d9d91ebdd969db7f354824380f0aecbb1a6910
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
plugins:
|
|
2
|
+
- rubocop-rspec
|
|
3
|
+
|
|
4
|
+
AllCops:
|
|
5
|
+
TargetRubyVersion: 3.1
|
|
6
|
+
NewCops: enable
|
|
7
|
+
SuggestExtensions: false
|
|
8
|
+
Exclude:
|
|
9
|
+
- "bin/**/*"
|
|
10
|
+
- "pkg/**/*"
|
|
11
|
+
- "tmp/**/*"
|
|
12
|
+
- "vendor/**/*"
|
|
13
|
+
|
|
14
|
+
Style/StringLiterals:
|
|
15
|
+
EnforcedStyle: double_quotes
|
|
16
|
+
|
|
17
|
+
Style/Documentation:
|
|
18
|
+
Enabled: false
|
|
19
|
+
|
|
20
|
+
Metrics/BlockLength:
|
|
21
|
+
Exclude:
|
|
22
|
+
- "spec/**/*"
|
|
23
|
+
- "*.gemspec"
|
|
24
|
+
|
|
25
|
+
Metrics/MethodLength:
|
|
26
|
+
Max: 20
|
|
27
|
+
CountAsOne: ["array", "hash", "heredoc"]
|
|
28
|
+
Exclude:
|
|
29
|
+
- "lib/generators/**/*"
|
|
30
|
+
|
|
31
|
+
Metrics/AbcSize:
|
|
32
|
+
Max: 25
|
|
33
|
+
Exclude:
|
|
34
|
+
- "lib/generators/**/*"
|
|
35
|
+
|
|
36
|
+
Metrics/ClassLength:
|
|
37
|
+
Max: 200
|
|
38
|
+
Exclude:
|
|
39
|
+
- "lib/generators/**/*"
|
|
40
|
+
|
|
41
|
+
Metrics/CyclomaticComplexity:
|
|
42
|
+
Max: 10
|
|
43
|
+
|
|
44
|
+
Metrics/PerceivedComplexity:
|
|
45
|
+
Max: 10
|
|
46
|
+
|
|
47
|
+
Layout/LineLength:
|
|
48
|
+
Max: 120
|
|
49
|
+
|
|
50
|
+
Gemspec/DevelopmentDependencies:
|
|
51
|
+
EnforcedStyle: Gemfile
|
|
52
|
+
|
|
53
|
+
# Integration / generator specs intentionally use string describes and richer examples.
|
|
54
|
+
RSpec/ExampleLength:
|
|
55
|
+
Max: 50
|
|
56
|
+
|
|
57
|
+
RSpec/MultipleExpectations:
|
|
58
|
+
Max: 25
|
|
59
|
+
|
|
60
|
+
RSpec/DescribeClass:
|
|
61
|
+
Exclude:
|
|
62
|
+
- "spec/integration/**/*"
|
|
63
|
+
- "spec/generators/**/*"
|
|
64
|
+
- "spec/support/**/*"
|
|
65
|
+
- "spec/access_grant/user_spec.rb"
|
|
66
|
+
- "spec/access_grant/controller_methods_spec.rb"
|
|
67
|
+
- "spec/access_grant/railtie_spec.rb"
|
|
68
|
+
- "spec/access_grant/apply_configuration_spec.rb"
|
|
69
|
+
- "spec/access_grant/owner_spec.rb"
|
|
70
|
+
|
|
71
|
+
RSpec/SpecFilePathFormat:
|
|
72
|
+
Exclude:
|
|
73
|
+
- "spec/access_grant/models/**/*"
|
|
74
|
+
|
|
75
|
+
RSpec/InstanceVariable:
|
|
76
|
+
Exclude:
|
|
77
|
+
- "spec/generators/**/*"
|
|
78
|
+
- "spec/access_grant/controller_methods_spec.rb"
|
|
79
|
+
- "spec/access_grant/owner_spec.rb"
|
|
80
|
+
|
|
81
|
+
RSpec/ContextWording:
|
|
82
|
+
Prefixes:
|
|
83
|
+
- when
|
|
84
|
+
- with
|
|
85
|
+
- without
|
|
86
|
+
- and
|
|
87
|
+
- for
|
|
88
|
+
- if
|
|
89
|
+
|
|
90
|
+
RSpec/VerifiedDoubles:
|
|
91
|
+
Exclude:
|
|
92
|
+
- "spec/access_grant/controller_methods_spec.rb"
|
|
93
|
+
|
|
94
|
+
RSpec/MessageSpies:
|
|
95
|
+
EnforcedStyle: receive
|
|
96
|
+
|
|
97
|
+
RSpec/StubbedMock:
|
|
98
|
+
Enabled: false
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.2.6
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
5
|
+
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [1.0.0] - 2026-09-08
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Configuration** — table names, tenant/user classes, owner role mode, controller
|
|
14
|
+
hooks (`current_user_method`, `current_tenant_method`), and recovery callback.
|
|
15
|
+
- **Permission catalog DSL** — code-defined `AccessGrant.permissions` catalog with
|
|
16
|
+
`resource` / `action` blocks; synced to DB via `rake access_grant:sync_permissions`.
|
|
17
|
+
- **Sync** — idempotent upsert of catalog permissions into the database (never deletes).
|
|
18
|
+
- **`permitted?(key, tenant:)`** — runtime authorization check on user models.
|
|
19
|
+
- **Owner role** — protected/bypass/both modes, last-owner guard, `grant_owner!` /
|
|
20
|
+
`revoke_owner!`.
|
|
21
|
+
- **Recovery rake** — `access_grant:grant_role` for ops lockout recovery.
|
|
22
|
+
- **Controller authorization** — `access_grant_authorize!` before-action hook
|
|
23
|
+
(raises `AccessGrant::NotAuthorizedError`).
|
|
24
|
+
- **Generators** — `access_grant:install` (core tables) and `access_grant:setup`
|
|
25
|
+
(initializer, catalog, user_roles migration, model patches).
|
|
26
|
+
- **Railtie** — loads rake tasks and `ActiveSupport.on_load` hooks for
|
|
27
|
+
`ActionController` and `ActiveRecord`.
|
|
28
|
+
- Project scaffolding: gemspec, RSpec/RuboCop harness, CI, and design docs
|
|
29
|
+
(docs/proposal.md, docs/architecture.md).
|
|
30
|
+
|
|
31
|
+
## [0.1.0] - 2026-09-05
|
|
32
|
+
|
|
33
|
+
- Initial gem skeleton (pre-release, unpublished).
|
data/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Getting started
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
bin/setup
|
|
7
|
+
bundle exec rspec
|
|
8
|
+
bundle exec rubocop
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`bin/console` starts an IRB session with the gem loaded, for quick manual
|
|
12
|
+
experiments.
|
|
13
|
+
|
|
14
|
+
## Branching
|
|
15
|
+
|
|
16
|
+
`main` is always releasable. Work happens on short-lived branches named by
|
|
17
|
+
type:
|
|
18
|
+
|
|
19
|
+
- `feature/<slug>` — new functionality
|
|
20
|
+
- `fix/<slug>` — bug fixes
|
|
21
|
+
- `docs/<slug>` — documentation-only changes
|
|
22
|
+
- `chore/<slug>` — tooling, dependencies, CI, maintenance
|
|
23
|
+
|
|
24
|
+
## Commit messages
|
|
25
|
+
|
|
26
|
+
This project follows [Conventional Commits](https://www.conventionalcommits.org/):
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
<type>[optional scope]: <description>
|
|
30
|
+
|
|
31
|
+
[optional body]
|
|
32
|
+
|
|
33
|
+
[optional footer(s)]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Common types: `feat`, `fix`, `docs`, `chore`, `refactor`, `test`, `ci`.
|
|
37
|
+
|
|
38
|
+
Examples:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
feat: add permitted? memoization per instance
|
|
42
|
+
fix: prevent duplicate role names within a tenant
|
|
43
|
+
docs: document the migration sequencing guardrail
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
A breaking change is marked either with `!` after the type/scope
|
|
47
|
+
(`feat!: rename acts_as_permissible to acts_as_grantable`) or a
|
|
48
|
+
`BREAKING CHANGE:` footer explaining the impact.
|
|
49
|
+
|
|
50
|
+
## Pull requests
|
|
51
|
+
|
|
52
|
+
Every PR should:
|
|
53
|
+
|
|
54
|
+
- Pass `bundle exec rspec` and `bundle exec rubocop` (enforced by CI).
|
|
55
|
+
- Update `CHANGELOG.md` under `[Unreleased]` for any user-facing change.
|
|
56
|
+
- Update `README.md` / `docs/` if behavior or the public API changed.
|
|
57
|
+
|
|
58
|
+
The PR template checklist covers this — fill it in rather than deleting it.
|
|
59
|
+
|
|
60
|
+
## Versioning & releases
|
|
61
|
+
|
|
62
|
+
This project follows [Semantic Versioning](https://semver.org/).
|
|
63
|
+
|
|
64
|
+
### Cut a release
|
|
65
|
+
|
|
66
|
+
1. Open a PR on `main` that:
|
|
67
|
+
- Bumps `AccessGrant::VERSION` in `lib/access_grant/version.rb`
|
|
68
|
+
- Moves `[Unreleased]` entries in `CHANGELOG.md` under
|
|
69
|
+
`## [X.Y.Z] - YYYY-MM-DD` and leaves a fresh empty `[Unreleased]`
|
|
70
|
+
- Uses commit message `chore: release vX.Y.Z`
|
|
71
|
+
2. Merge the PR and wait for CI on `main` to pass.
|
|
72
|
+
3. In GitHub Actions, run the **Release** workflow on `main` with input
|
|
73
|
+
`version` set to `X.Y.Z` (must match `AccessGrant::VERSION`).
|
|
74
|
+
4. The workflow:
|
|
75
|
+
- Fails fast if the version or CHANGELOG heading does not match
|
|
76
|
+
- Publishes the gem to RubyGems.org via Trusted Publishing (OIDC)
|
|
77
|
+
- Creates git tag `vX.Y.Z` and a GitHub Release from the CHANGELOG section
|
|
78
|
+
|
|
79
|
+
Do **not** run `bundle exec rake release` locally for production publishes;
|
|
80
|
+
Actions owns tagging and `gem push`.
|
|
81
|
+
|
|
82
|
+
### One-time RubyGems setup
|
|
83
|
+
|
|
84
|
+
1. Create a RubyGems.org account and enable MFA.
|
|
85
|
+
2. Configure a [Trusted Publisher](https://guides.rubygems.org/trusted-publishing/)
|
|
86
|
+
for gem `access_grant`:
|
|
87
|
+
- Repository owner: `SahSantoshh`
|
|
88
|
+
- Repository name: `access_grant`
|
|
89
|
+
- Workflow filename: `release.yml`
|
|
90
|
+
- Environment: leave blank (must match the workflow — no `environment:` key)
|
|
91
|
+
3. For the first publish, use RubyGems’ pending trusted-publisher flow if the
|
|
92
|
+
gem name is not on the index yet.
|
|
93
|
+
|
|
94
|
+
## Code style
|
|
95
|
+
|
|
96
|
+
RuboCop must pass with no offenses (`bundle exec rubocop`). Match the
|
|
97
|
+
conventions already established in the codebase rather than introducing new
|
|
98
|
+
ones ad hoc; propose `.rubocop.yml` changes separately if a rule genuinely
|
|
99
|
+
doesn't fit.
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Santosh Sah
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# AccessGrant
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/access_grant)
|
|
4
|
+
[](https://github.com/SahSantoshh/access_grant/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE.txt)
|
|
6
|
+
|
|
7
|
+
Dynamic, database-backed, per-tenant role and permission management for
|
|
8
|
+
Rails — the "roles and permissions live in the database, admins edit them at
|
|
9
|
+
runtime" pattern, without a canonical Rails equivalent until now.
|
|
10
|
+
|
|
11
|
+
> **Status:** v1.0 published on [RubyGems](https://rubygems.org/gems/access_grant).
|
|
12
|
+
> Design: [`docs/architecture.md`](docs/architecture.md),
|
|
13
|
+
> [`docs/proposal.md`](docs/proposal.md).
|
|
14
|
+
|
|
15
|
+
## The problem
|
|
16
|
+
|
|
17
|
+
Every mainstream Rails authorization gem hardcodes "who can do what" in Ruby
|
|
18
|
+
code — a policy class, an `Ability` class. Changing a permission means a
|
|
19
|
+
developer opening a pull request and deploying. AccessGrant instead keeps
|
|
20
|
+
the permission *catalog* in code (so a new capability always requires a code
|
|
21
|
+
change to exist) but keeps the *role → permission mapping* in the database,
|
|
22
|
+
fully editable by tenant admins at runtime.
|
|
23
|
+
|
|
24
|
+
## How it compares
|
|
25
|
+
|
|
26
|
+
| Gem | Roles as data | Permission catalog | Runtime-editable | Per-tenant scoping |
|
|
27
|
+
|---|---|---|---|---|
|
|
28
|
+
| Pundit | No | No | No | App-defined |
|
|
29
|
+
| CanCanCan | No | No | No | App-defined |
|
|
30
|
+
| Action Policy | No | No | No | App-defined |
|
|
31
|
+
| Rolify | Yes | **No** — no permission concept at all | N/A | Partial |
|
|
32
|
+
| **AccessGrant** | Yes | **Yes** | **Yes** | **Yes** |
|
|
33
|
+
|
|
34
|
+
Built **from scratch** (not on Pundit or Rolify). See [`docs/proposal.md`](docs/proposal.md).
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
# Gemfile
|
|
40
|
+
gem "access_grant"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
bundle install
|
|
45
|
+
rails g access_grant:install
|
|
46
|
+
rails g access_grant:setup \
|
|
47
|
+
--multi-tenant \
|
|
48
|
+
--tenant=Organization \
|
|
49
|
+
--user=User \
|
|
50
|
+
--owner-role=protected \
|
|
51
|
+
--tables=auto
|
|
52
|
+
rails db:migrate
|
|
53
|
+
bundle exec rake access_grant:sync_permissions
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`setup` patches models with `access_grant :tenant` / `access_grant :user`
|
|
57
|
+
and writes `config/initializers/access_grant.rb` plus
|
|
58
|
+
`config/access_grant/{permissions,roles}.rb`.
|
|
59
|
+
|
|
60
|
+
**On every deploy:** run `bundle exec rake access_grant:sync_permissions`
|
|
61
|
+
after migrate. Catalog sync is **not** a migration.
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
class Organization < ApplicationRecord
|
|
67
|
+
access_grant :tenant
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class User < ApplicationRecord
|
|
71
|
+
access_grant :user
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# config/access_grant/permissions.rb
|
|
75
|
+
AccessGrant.permissions do
|
|
76
|
+
resource :invoices do
|
|
77
|
+
action :couple, description: "Can couple invoices together"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
org.grant_owner!(current_user)
|
|
82
|
+
current_user.permitted?("invoices.index", tenant: org) # => true / false
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
# ApplicationController
|
|
87
|
+
access_grant_authorize! # uses current_user + current_tenant
|
|
88
|
+
# host defines: def current_tenant; …; end
|
|
89
|
+
|
|
90
|
+
# Skip public auth endpoints (Devise sessions/registrations/passwords, etc.)
|
|
91
|
+
skip_access_grant_authorize! if: :devise_controller?
|
|
92
|
+
|
|
93
|
+
# Raises AccessGrant::NotAuthorizedError — rescue in the host, e.g.:
|
|
94
|
+
# rescue_from AccessGrant::NotAuthorizedError, with: -> { head :forbidden }
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Keys are strictly `resource.action`. Permission descriptions come from the
|
|
98
|
+
catalog (dev/sync only); role descriptions are admin-editable.
|
|
99
|
+
|
|
100
|
+
See [`docs/architecture.md`](docs/architecture.md) for models, Owner,
|
|
101
|
+
[configuration reference](docs/architecture.md#configuration-reference)
|
|
102
|
+
(every `config.*` option with examples), overrides, and the full decision
|
|
103
|
+
table. Acceptance scenarios:
|
|
104
|
+
[`docs/superpowers/specs/2026-09-07-usage-scenarios.md`](docs/superpowers/specs/2026-09-07-usage-scenarios.md).
|
|
105
|
+
|
|
106
|
+
## Development
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
bin/setup
|
|
110
|
+
bundle exec rspec
|
|
111
|
+
bundle exec rubocop
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Public APIs use **YARD** comments (`@param`, `@return`, `@raise`, `@example`) —
|
|
115
|
+
the usual Ruby documentation style (YARD extends RDoc). IDEs show them on
|
|
116
|
+
hover; optionally generate HTML with `yard doc` if the `yard` gem is installed.
|
|
117
|
+
|
|
118
|
+
See [`CONTRIBUTING.md`](CONTRIBUTING.md) for branching, commit, and release
|
|
119
|
+
conventions.
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
MIT — see [`LICENSE.txt`](LICENSE.txt).
|