graphiti 1.11.1 → 1.12.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 86630b0954711272a5cb2d37e3ba983eea16b580626fe6ef37479b5cbe1ef26b
4
- data.tar.gz: 331e7c4a78b57a7362986bd0f49348cee046df52683e0f27e82564d8ff4fa3e9
3
+ metadata.gz: 0ffd673e5dc4ec50d5b5aeb01abcb192894c6451cd9bfd54eca081f76bdf1d49
4
+ data.tar.gz: 45ed5cf75d703bd9b573015cd7f798cdc8fe1d5606d198bbdbda09d8e9dd7383
5
5
  SHA512:
6
- metadata.gz: b6ee44eadb2f7e6a7c37801326557a1e88bd6921dc5aca76cbd94fe4159bd8d54bfc617c92086b9dc7d9bbdcc3d5925d846213f334dbe5f25726394842acb50a
7
- data.tar.gz: c1c0e49eb82a6afde05dac7628175e582590fc17fb04bf2aabe64050a31a5870e3f1dfcd1e07d21186635ae923cad1b6b51e4d811d9e86f1437576389da13d99
6
+ metadata.gz: 1472ab72a4ba8982a06a99d4959460ddae1e350ac6f1ce579dbe694c9c2cac6901e46d74b61532895f248e29848361aa311f39320201cb94f002874d64b34386
7
+ data.tar.gz: 44d3d9fbb1f78c6ba6a1fe005dc19a9f64a7b15236075b224571d585754928352d032a5acccd7f6165695990ff5a66750868a13ebd87fec9d33e9446e599fb97
@@ -4,6 +4,7 @@ on:
4
4
  push:
5
5
  branches: [main]
6
6
  pull_request: {}
7
+ workflow_dispatch: {}
7
8
 
8
9
  concurrency:
9
10
  group: ci-${{ github.head_ref || github.ref }}
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  graphiti changelog
2
2
 
3
+ # [1.12.0](https://github.com/graphiti-api/graphiti/compare/v1.11.1...v1.12.0) (2026-07-29)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * GQL name chaining ([#415](https://github.com/graphiti-api/graphiti/issues/415)) [skip ci] ([930928a](https://github.com/graphiti-api/graphiti/commit/930928a8779037efbcc1989eef5cf579c608e431))
9
+
10
+
11
+ ### Features
12
+
13
+ * conditional relationships evaluate readable/writable guards per-request ([#450](https://github.com/graphiti-api/graphiti/issues/450)) ([3ed3187](https://github.com/graphiti-api/graphiti/commit/3ed3187068d268cd85ee1c36dc8135aa92abd05c))
14
+ * pass the model and attribute name to writable guards ([#511](https://github.com/graphiti-api/graphiti/issues/511)) [skip ci] ([24a8a94](https://github.com/graphiti-api/graphiti/commit/24a8a94f801867f57cc7c1ae9c6a9bdbfec05c24))
15
+
3
16
  ## [1.11.1](https://github.com/graphiti-api/graphiti/compare/v1.11.0...v1.11.1) (2026-07-28)
4
17
 
5
18
  # [1.11.0](https://github.com/graphiti-api/graphiti/compare/v1.10.3...v1.11.0) (2026-07-28)
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,77 @@
1
+ # Contributing
2
+
3
+ ## Commit messages
4
+
5
+ Releases are cut automatically by [semantic-release](https://github.com/semantic-release/semantic-release)
6
+ from commit messages, so the prefix you use decides whether a release happens
7
+ and how the version changes.
8
+
9
+ Format: `type: subject`, or `type(scope): subject`.
10
+
11
+ ### Types that release
12
+
13
+ | Prefix | Version bump | Use for |
14
+ | ------ | ------------ | ------- |
15
+ | `fix:` | patch (`1.2.3` → `1.2.4`) | A bug fix users would notice |
16
+ | `perf:` | patch | A performance improvement |
17
+ | `feat:` | minor (`1.2.3` → `1.3.0`) | New functionality |
18
+
19
+ ### Types that do not release
20
+
21
+ `build:` `chore:` `ci:` `docs:` `refactor:` `style:` `test:`
22
+
23
+ These land on `main` without publishing a gem. Use them for anything with no
24
+ user-facing change — test fixes, CI tweaks, internal refactors. They are still
25
+ included in the next release's notes.
26
+
27
+ ### Breaking changes
28
+
29
+ Either add `!` after the type, or a `BREAKING CHANGE:` footer. Both produce a
30
+ major bump (`1.2.3` → `2.0.0`):
31
+
32
+ ```
33
+ feat!: remove deprecated sideload API
34
+ ```
35
+
36
+ ```
37
+ feat: replace sideload API
38
+
39
+ BREAKING CHANGE: `allow_sideload` no longer accepts a :scope option.
40
+ ```
41
+
42
+ The `!` works with a scope too: `feat(adapters)!: ...`
43
+
44
+ Describe what breaks and what to do instead — the footer text is published in
45
+ the release notes verbatim.
46
+
47
+ ### Notes
48
+
49
+ - Only the **commit** prefix matters, not the PR title, unless the PR is
50
+ squash-merged with the title as the commit subject.
51
+ - The prefix must be lowercase and followed by `: ` — `Fix: thing` and
52
+ `fix:thing` are both unparsed, and an unparsed commit never triggers a
53
+ release.
54
+ - If nothing since the last tag is `fix:`, `perf:`, `feat:`, or breaking, no
55
+ release is cut. That is expected.
56
+
57
+ ## Running the tests
58
+
59
+ ```sh
60
+ bundle exec rspec
61
+ ```
62
+
63
+ The Rails integration specs are gated behind an environment variable and are
64
+ **silently skipped** without it — a plain `rspec` run reports success while
65
+ covering none of the ActiveRecord adapter:
66
+
67
+ ```sh
68
+ BUNDLE_GEMFILE=gemfiles/rails_8_0_graphiti_rails.gemfile \
69
+ APPRAISAL_INITIALIZED=1 \
70
+ bundle exec rspec spec/integration/rails/
71
+ ```
72
+
73
+ Lint with:
74
+
75
+ ```sh
76
+ bundle exec standardrb
77
+ ```
data/graphiti.gemspec CHANGED
@@ -18,6 +18,24 @@ Gem::Specification.new do |spec|
18
18
  spec.require_paths = ["lib"]
19
19
  spec.required_ruby_version = ">= 2.7"
20
20
 
21
+ # TODO: remove once the 1.12 upgrade window has passed
22
+ spec.post_install_message = <<~MSG
23
+ Graphiti: relationship readable:/writable: guards are now enforced.
24
+
25
+ Symbols, strings, and procs passed to a relationship's readable/writable
26
+ guards were always interpreted as true and never actually called before this
27
+ conditional relationship change. They are now evaluated per-request, where
28
+ unreadable relationships are omitted from responses and includes, and unwritable
29
+ relationships reject writes.
30
+
31
+ To list every affected relationship in your app:
32
+
33
+ bin/rails runner 'puts Graphiti.guarded_relationships'
34
+
35
+ Audit these before deploying. Apps using schema.json will also see affected
36
+ relationships flagged as "became guarded" by the schema check.
37
+ MSG
38
+
21
39
  spec.add_dependency "jsonapi-serializable", "~> 0.3.0"
22
40
  spec.add_dependency "jsonapi-renderer", "~> 0.2", ">= 0.2.2"
23
41
  spec.add_dependency "dry-types", ">= 0.15.0", "< 2.0"
@@ -70,13 +70,13 @@ module Graphiti
70
70
 
71
71
  serializers = v.send(:resources)
72
72
  name = graphql ? k.to_s.camelize(:lower) : k
73
- name_chain = name_chain.dup
74
- name_chain << k unless name_chain.last == k
73
+ subchain = name_chain.dup
74
+ subchain << k unless subchain.last == k
75
75
 
76
76
  unless remote_resource? && serializers.nil?
77
77
  payload = if serializers.is_a?(Array)
78
78
  data = serializers.map { |rr|
79
- rr.to_hash(fields: fields, include: nested_include, graphql: graphql, name_chain: name_chain)
79
+ rr.to_hash(fields: fields, include: nested_include, graphql: graphql, name_chain: subchain)
80
80
  }
81
81
  graphql ? {nodes: data} : data
82
82
  elsif serializers.nil?
@@ -86,7 +86,7 @@ module Graphiti
86
86
  end
87
87
  end
88
88
  else
89
- serializers.to_hash(fields: fields, include: nested_include, graphql: graphql, name_chain: name_chain)
89
+ serializers.to_hash(fields: fields, include: nested_include, graphql: graphql, name_chain: subchain)
90
90
  end
91
91
 
92
92
  attrs[name.to_sym] = payload
@@ -211,10 +211,13 @@ module Graphiti
211
211
  allowlist = allowlist[@resource.context_namespace] if allowlist
212
212
  end
213
213
 
214
- allowlist ? Util::IncludeParams.scrub(requested, allowlist) : requested
215
- end
214
+ scrubbed = allowlist ? Util::IncludeParams.scrub(requested, allowlist) : requested
216
215
 
217
- @include_hash
216
+ scrubbed.filter do |key, value|
217
+ sideload = @resource.class.sideload(key)
218
+ sideload.nil? ? true : sideload.readable?
219
+ end
220
+ end
218
221
  end
219
222
 
220
223
  def stats
@@ -83,20 +83,22 @@ module Graphiti
83
83
  end
84
84
 
85
85
  def typecast_attributes(resource, attributes, action, payload_path)
86
- attributes.each_pair do |key, value|
87
- # Only validate id if create action, otherwise it's only used for lookup
88
- next if action != :create &&
89
- key == :id &&
90
- resource.class.config[:attributes][:id][:writable] == false
91
-
92
- begin
93
- attributes[key] = resource.typecast(key, value, :writable)
94
- rescue Graphiti::Errors::UnknownAttribute
95
- @errors.add(fully_qualified_key(key, payload_path), :unknown_attribute, message: "is an unknown attribute")
96
- rescue Graphiti::Errors::InvalidAttributeAccess
97
- @errors.add(fully_qualified_key(key, payload_path), :unwritable_attribute, message: "cannot be written")
98
- rescue Graphiti::Errors::TypecastFailed => e
99
- @errors.add(fully_qualified_key(key, payload_path), :type_error, message: "should be type #{e.type_name}")
86
+ resource.with_guarded_write(action, attributes[:id]) do
87
+ attributes.each_pair do |key, value|
88
+ # Only validate id if create action, otherwise it's only used for lookup
89
+ next if action != :create &&
90
+ key == :id &&
91
+ resource.class.config[:attributes][:id][:writable] == false
92
+
93
+ begin
94
+ attributes[key] = resource.typecast(key, value, :writable)
95
+ rescue Graphiti::Errors::UnknownAttribute
96
+ @errors.add(fully_qualified_key(key, payload_path), :unknown_attribute, message: "is an unknown attribute")
97
+ rescue Graphiti::Errors::InvalidAttributeAccess
98
+ @errors.add(fully_qualified_key(key, payload_path), :unwritable_attribute, message: "cannot be written")
99
+ rescue Graphiti::Errors::TypecastFailed => e
100
+ @errors.add(fully_qualified_key(key, payload_path), :type_error, message: "should be type #{e.type_name}")
101
+ end
100
102
  end
101
103
  end
102
104
  end
@@ -67,6 +67,32 @@ module Graphiti
67
67
  adapter.base_scope(model)
68
68
  end
69
69
 
70
+ # Exposes write context to attribute guards for the duration of the block.
71
+ # @api private
72
+ def with_guarded_write(action, id)
73
+ @guarded_write = {action: action, id: id}
74
+ yield
75
+ ensure
76
+ remove_instance_variable(:@guarded_write) if defined?(@guarded_write)
77
+ remove_instance_variable(:@guard_model) if defined?(@guard_model)
78
+ end
79
+
80
+ # The model a writable guard is being asked about. Resolved lazily and
81
+ # memoized, so it costs nothing unless a guard asks for it, and is only
82
+ # resolved once per payload. On create, this is a new unsaved instance.
83
+ # @api private
84
+ def guard_model
85
+ return @guard_model if defined?(@guard_model)
86
+
87
+ @guard_model = if !defined?(@guarded_write)
88
+ nil
89
+ elsif @guarded_write[:action] == :create || @guarded_write[:id].nil?
90
+ build(model)
91
+ else
92
+ self.class._find(id: @guarded_write[:id]).data
93
+ end
94
+ end
95
+
70
96
  def typecast(name, value, flag)
71
97
  att = get_attr!(name, flag, request: true)
72
98
  type_name = att[:type]
@@ -247,6 +247,10 @@ module Graphiti
247
247
  schema[:single] = true
248
248
  end
249
249
 
250
+ if config.guarded?
251
+ schema[:guard] = true
252
+ end
253
+
250
254
  r[name] = schema
251
255
  end
252
256
  end
@@ -102,6 +102,10 @@ module Graphiti
102
102
  next
103
103
  end
104
104
 
105
+ if new_rel[:guard] && !old_rel[:guard]
106
+ @errors << "#{old_resource[:name]}: relationship #{name.inspect} became guarded."
107
+ end
108
+
105
109
  if new_rel[:resource] != old_rel[:resource]
106
110
  @errors << "#{old_resource[:name]}: relationship #{name.inspect} changed resource from #{old_rel[:resource]} to #{new_rel[:resource]}."
107
111
  end
@@ -28,6 +28,9 @@ module Graphiti
28
28
  prev = Util::Hash.deep_dup(field_condition_blocks)
29
29
  super
30
30
  self.field_condition_blocks = prev
31
+ # _register_condition mutates the hash, which ancestors share by
32
+ # reference until reassigned - copy first so the guard doesn't leak.
33
+ self.relationship_condition_blocks = Util::Hash.deep_dup(relationship_condition_blocks)
31
34
  _register_condition(relationship_condition_blocks, name, options)
32
35
  end
33
36
 
@@ -99,11 +99,15 @@ module Graphiti
99
99
  end
100
100
 
101
101
  def readable?
102
- !!@readable
102
+ evaluate_flag @readable
103
103
  end
104
104
 
105
105
  def writable?
106
- !!@writable
106
+ evaluate_flag @writable
107
+ end
108
+
109
+ def guarded?
110
+ dynamic_flag?(@readable) || dynamic_flag?(@writable)
107
111
  end
108
112
 
109
113
  def single?
@@ -454,20 +458,50 @@ module Graphiti
454
458
  Util::Class.namespace_for(klass)
455
459
  end
456
460
 
457
- # TODO: call this at runtime to support procs
461
+ def dynamic_flag?(flag)
462
+ flag.is_a?(Symbol) || flag.is_a?(String) || flag.is_a?(Proc)
463
+ end
464
+
465
+ # The guard method may be defined on either resource. The declaring side
466
+ # wins (`has_many :positions, readable: :admin?` calls EmployeeResource#admin?,
467
+ # matching how attribute guards resolve). If the related resource is the only place
468
+ # that defines it, the guard runs there. For instance, PositionResource#admin? can guard
469
+ # every relationship that points at positions.
470
+
458
471
  def evaluate_flag(flag)
459
472
  return false if flag.blank?
460
473
 
461
- case flag.class.name
462
- when "Symbol", "String"
463
- resource.send(flag)
464
- when "Proc"
465
- resource.instance_exec(&flag)
474
+ case flag
475
+ when Symbol, String
476
+ guard_resource(flag).send(flag)
477
+ when Proc
478
+ evaluate_guard_proc(flag)
466
479
  else
467
480
  !!flag
468
481
  end
469
482
  end
470
483
 
484
+ # Private methods are eligible - guards are often not part of a
485
+ # resource's public API.
486
+ def guard_resource(method_name)
487
+ if !parent_resource.respond_to?(method_name, true) &&
488
+ resource.respond_to?(method_name, true)
489
+ resource
490
+ else
491
+ parent_resource
492
+ end
493
+ end
494
+
495
+ # A proc can't be introspected the way a method name can, so run it
496
+ # against the declaring resource and only reach for the related one if
497
+ # that resource turns out not to define what the proc references.
498
+ def evaluate_guard_proc(flag)
499
+ parent_resource.instance_exec(&flag)
500
+ rescue NameError => error
501
+ raise error unless resource.respond_to?(error.name, true)
502
+ resource.instance_exec(&flag)
503
+ end
504
+
471
505
  def context
472
506
  Graphiti.context[:object]
473
507
  end
@@ -50,14 +50,41 @@ module Graphiti
50
50
  end
51
51
  end
52
52
 
53
+ # Guards may optionally accept the model being written and the attribute
54
+ # name, mirroring readable guards.
53
55
  def guard_passes?
54
- !!resource.send(attribute[flag])
56
+ result = case guard_arity
57
+ when 0 then call_guard
58
+ when 1 then call_guard(resource.guard_model)
59
+ else call_guard(resource.guard_model, name.to_s)
60
+ end
61
+ !!result
62
+ end
63
+
64
+ def call_guard(*args)
65
+ if guard.is_a?(Proc)
66
+ resource.instance_exec(*args, &guard)
67
+ else
68
+ resource.send(guard, *args)
69
+ end
70
+ end
71
+
72
+ def guard_arity
73
+ method = guard.is_a?(Proc) ? guard : resource.method(guard)
74
+ # Negative arity means optional or splatted params - hand over
75
+ # everything and let the guard take what it wants.
76
+ method.arity.negative? ? 2 : method.arity
77
+ end
78
+
79
+ def guard
80
+ attribute[flag]
55
81
  end
56
82
 
57
83
  def guarded?
58
- request? &&
59
- attribute[flag].is_a?(Symbol) &&
60
- attribute[flag] != :required
84
+ return false unless request?
85
+ return false if guard == :required
86
+
87
+ guard.is_a?(Symbol) || guard.is_a?(String) || guard.is_a?(Proc)
61
88
  end
62
89
 
63
90
  def supported?
@@ -19,8 +19,7 @@ module Graphiti
19
19
  private
20
20
 
21
21
  def apply?(sideload)
22
- @serializer.relationship_blocks[sideload.name].nil? &&
23
- sideload.readable?
22
+ @serializer.relationship_blocks[sideload.name].nil?
24
23
  end
25
24
  end
26
25
 
@@ -32,7 +31,8 @@ module Graphiti
32
31
  end
33
32
 
34
33
  def apply
35
- @serializer.relationship(@sideload.name, &block)
34
+ sideload = @sideload
35
+ @serializer.relationship(@sideload.name, if: -> { sideload.readable? }, &block)
36
36
  end
37
37
 
38
38
  # If we can't eagerly validate links on app boot, we do it at runtime
@@ -1,3 +1,3 @@
1
1
  module Graphiti
2
- VERSION = "1.11.1"
2
+ VERSION = "1.12.0"
3
3
  end
data/lib/graphiti.rb CHANGED
@@ -62,6 +62,18 @@ module Graphiti
62
62
  @resources ||= []
63
63
  end
64
64
 
65
+ # Every guarded relationship, e.g. ["EmployeeResource.positions"]
66
+ def self.guarded_relationships
67
+ ::Rails.application.eager_load! if defined?(::Rails) && ::Rails.respond_to?(:application) && ::Rails.application
68
+
69
+ resources.flat_map { |resource_class|
70
+ resource_class.config[:sideloads]
71
+ .select { |_name, sideload| sideload.guarded? }
72
+ .keys
73
+ .map { |relationship_name| "#{resource_class.name || "(anonymous resource)"}.#{relationship_name}" }
74
+ }.uniq.sort
75
+ end
76
+
65
77
  def self.broadcast(name, payload)
66
78
  # AS::N prefers domain naming format with more specific towards end
67
79
  name = "#{name}.graphiti"
data/package.json CHANGED
@@ -37,43 +37,66 @@
37
37
  {
38
38
  "releaseRules": [
39
39
  {
40
- "type": "*!",
40
+ "breakingMarker": "!",
41
41
  "release": "major"
42
42
  },
43
+ {
44
+ "breaking": true,
45
+ "release": "major"
46
+ },
47
+ {
48
+ "revert": true,
49
+ "release": "patch"
50
+ },
43
51
  {
44
52
  "type": "feat",
45
53
  "release": "minor"
46
54
  },
47
55
  {
48
- "type": "build",
56
+ "type": "fix",
49
57
  "release": "patch"
50
58
  },
51
59
  {
52
- "type": "ci",
60
+ "type": "perf",
53
61
  "release": "patch"
54
62
  },
63
+ {
64
+ "type": "build",
65
+ "release": false
66
+ },
67
+ {
68
+ "type": "ci",
69
+ "release": false
70
+ },
55
71
  {
56
72
  "type": "chore",
57
- "release": "patch"
73
+ "release": false
58
74
  },
59
75
  {
60
76
  "type": "docs",
61
- "release": "patch"
77
+ "release": false
62
78
  },
63
79
  {
64
80
  "type": "refactor",
65
- "release": "patch"
81
+ "release": false
66
82
  },
67
83
  {
68
84
  "type": "style",
69
- "release": "patch"
85
+ "release": false
70
86
  },
71
87
  {
72
88
  "type": "test",
73
- "release": "patch"
89
+ "release": false
74
90
  }
75
91
  ],
76
92
  "parserOpts": {
93
+ "headerPattern": "^(\\w*)(?:\\((.*)\\))?(!)?: (.*)$",
94
+ "headerCorrespondence": [
95
+ "type",
96
+ "scope",
97
+ "breakingMarker",
98
+ "subject"
99
+ ],
77
100
  "noteKeywords": [
78
101
  "BREAKING CHANGE",
79
102
  "BREAKING CHANGES",
@@ -85,7 +108,20 @@
85
108
  }
86
109
  }
87
110
  ],
88
- "@semantic-release/release-notes-generator",
111
+ [
112
+ "@semantic-release/release-notes-generator",
113
+ {
114
+ "parserOpts": {
115
+ "headerPattern": "^(\\w*)(?:\\((.*)\\))?(!)?: (.*)$",
116
+ "headerCorrespondence": [
117
+ "type",
118
+ "scope",
119
+ "breakingMarker",
120
+ "subject"
121
+ ]
122
+ }
123
+ }
124
+ ],
89
125
  [
90
126
  "@semantic-release/changelog",
91
127
  {
@@ -99,7 +135,8 @@
99
135
  "@semantic-release/git",
100
136
  {
101
137
  "assets": [
102
- "CHANGELOG.md"
138
+ "CHANGELOG.md",
139
+ "lib/graphiti/version.rb"
103
140
  ],
104
141
  "message": "${nextRelease.version} CHANGELOG [skip ci]\n\n${nextRelease.notes}"
105
142
  }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphiti
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.11.1
4
+ version: 1.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lee Richmond
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-07-28 00:00:00.000000000 Z
11
+ date: 2026-07-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jsonapi-serializable
@@ -242,6 +242,7 @@ files:
242
242
  - Appraisals
243
243
  - CHANGELOG.md
244
244
  - CODE_OF_CONDUCT.md
245
+ - CONTRIBUTING.md
245
246
  - Gemfile
246
247
  - Guardfile
247
248
  - LICENSE.txt
@@ -370,7 +371,14 @@ homepage: https://github.com/graphiti-api/graphiti
370
371
  licenses:
371
372
  - MIT
372
373
  metadata: {}
373
- post_install_message:
374
+ post_install_message: "Graphiti: relationship readable:/writable: guards are now enforced.\n\nSymbols,
375
+ strings, and procs passed to a relationship's readable/writable\nguards were always
376
+ interpreted as true and never actually called before this\nconditional relationship
377
+ change. They are now evaluated per-request, where\nunreadable relationships are
378
+ omitted from responses and includes, and unwritable\nrelationships reject writes.\n\nTo
379
+ list every affected relationship in your app:\n\n bin/rails runner 'puts Graphiti.guarded_relationships'\n\nAudit
380
+ these before deploying. Apps using schema.json will also see affected \nrelationships
381
+ flagged as \"became guarded\" by the schema check.\n"
374
382
  rdoc_options: []
375
383
  require_paths:
376
384
  - lib