calmlens 0.20.0 → 0.22.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.
@@ -61,6 +61,15 @@ module CalmLens
61
61
  Error.parse_retry_after(headers['retry-after'])
62
62
  end
63
63
 
64
+ # @return [Boolean] true when the SDK raised this error itself, with no HTTP response: before
65
+ # sending (an invalid argument or option, or content it checks first, such as
66
+ # `upload/unsupported-type`), or because no response arrived (a timeout or a connection
67
+ # failure). The code is the one the API would use; `status` and `request_id` are nil, so
68
+ # branch on `code`.
69
+ def local?
70
+ status.nil?
71
+ end
72
+
64
73
  # @return [Boolean] true when the resource does not exist.
65
74
  def not_found?
66
75
  status == 404 || code.end_with?('/not-found')
@@ -70,12 +79,12 @@ module CalmLens
70
79
  RETRYABLE_CODES = [SdkErrorCodes::TRANSPORT_ERROR, SdkErrorCodes::TIMEOUT,
71
80
  ErrorCodes::IDEMPOTENT_REQUEST_IN_PROGRESS].freeze
72
81
  # HTTP statuses that may succeed when the same request is repeated.
73
- RETRYABLE_STATUSES = [429, 502, 503, 504].freeze
82
+ RETRYABLE_STATUSES = [429, 500, 502, 503, 504].freeze
74
83
  private_constant :RETRYABLE_CODES, :RETRYABLE_STATUSES
75
84
 
76
85
  # @return [Boolean] true when repeating the same request may succeed: connection failures
77
86
  # (`sdk/transport-error`), timeouts (`sdk/timeout`, including {Assets#wait_for_decision}),
78
- # `idempotency/request-in-progress`, throttling (429) and 502/503/504. Everything else,
87
+ # `idempotency/request-in-progress`, throttling (429), 500, 502, 503 and 504. Everything else,
79
88
  # including other 5xx statuses, is false. Repeat a write only with the same idempotency key.
80
89
  def retryable?
81
90
  RETRYABLE_CODES.include?(code) || RETRYABLE_STATUSES.include?(status)
@@ -8,11 +8,17 @@ module CalmLens
8
8
 
9
9
  # Lists events after a cursor, oldest first. Without `cursor`, returns no events and a
10
10
  # `nextCursor` for the current time: save it, then pass each response's `nextCursor` as `cursor`.
11
+ # `types` and `asset_id` narrow the feed to some event types or one asset's events. A cursor
12
+ # belongs to the filters it was issued with, so pass the same ones with each `cursor`.
11
13
  # @option options [String] :cursor the `nextCursor` of a previous response
12
14
  # @option options [Integer] :page_size 1 to 100, default 50
15
+ # @option options [Array<String>] :types event types to list, such as `asset.blocked`
16
+ # @option options [String] :asset_id list only this asset's events
13
17
  # @return [Hash] `{'items', 'nextCursor', 'hasMore'}`; `nextCursor` is never nil
14
18
  def list(options = {})
15
- call(options, __method__) { |api, request| request('GET', project('events'), query: api, request_options: request) }
19
+ call(options, __method__) do |api, request|
20
+ request('GET', project('events'), query: feed_query(api), request_options: request)
21
+ end
16
22
  end
17
23
 
18
24
  # Iterates pages from `cursor` until `hasMore` is false. Persist each page's `nextCursor` once its
@@ -48,5 +54,48 @@ module CalmLens
48
54
 
49
55
  each_page(options) { |page| page['items'].each(&block) }
50
56
  end
57
+
58
+ private
59
+
60
+ # The API takes the types as one comma-separated value; an empty list filters nothing.
61
+ def feed_query(api)
62
+ types = api['types']
63
+ return api unless api.key?('types')
64
+ raise Internal.invalid('types must be an Array of event type Strings') unless types.is_a?(Array) && types.all?(String)
65
+
66
+ api.merge('types' => types.empty? ? nil : types.join(','))
67
+ end
68
+ end
69
+
70
+ # Reads an event's facts: an {Events#list} item, a webhook delivery body or a {WebhookEvent}.
71
+ # {CalmLens.get_event_fact} and {CalmLens.get_event_facts} call it.
72
+ module EventFacts
73
+ module_function
74
+
75
+ # The value of one of an event's facts, by key, such as `'flagged-categories'`, or nil when the
76
+ # event doesn't carry it. A key the event repeats gives its first value. The fact's `url`, when
77
+ # it has one, is on the fact in the event's `facts`.
78
+ # @param event [WebhookEvent, Hash] a parsed delivery, or an event or delivery body with string keys
79
+ # @param key [String]
80
+ # @return [Object, nil]
81
+ def value(event, key)
82
+ found = facts_of(event).find { |fact| fact['key'] == key }
83
+ found && found['value']
84
+ end
85
+
86
+ # An event's facts as a Hash of values by key, such as `{ 'flagged-categories' => ['hate'] }`.
87
+ # A key the event repeats keeps its first value, as {.value} reads it.
88
+ # @param event [WebhookEvent, Hash] a parsed delivery, or an event or delivery body with string keys
89
+ # @return [Hash{String => Object}]
90
+ def values(event)
91
+ facts_of(event).each_with_object({}) { |fact, found| found[fact['key']] = fact['value'] unless found.key?(fact['key']) }
92
+ end
93
+
94
+ # The well-formed facts of an event, in order.
95
+ def facts_of(event)
96
+ facts = event.is_a?(Hash) ? event['facts'] : event.facts
97
+ facts.is_a?(Array) ? facts.select { |fact| fact.is_a?(Hash) && fact['key'].is_a?(String) } : []
98
+ end
99
+ private_class_method :facts_of
51
100
  end
52
101
  end
@@ -6,24 +6,29 @@ module CalmLens
6
6
  # The prefix of this resource's operations in Internal::OPTION_KEYS.
7
7
  OPERATIONS = 'folders'
8
8
 
9
- # Lists one page of child folders, sorted by name unless `ordering` says otherwise.
9
+ # Lists one page of child folders, sorted by name unless `sort` says otherwise.
10
10
  # @option options [String] :parent_id list this folder's children; omit for the project root
11
11
  # @option options [String] :cursor the previous page's `nextCursor`
12
12
  # @option options [Integer] :page_size 1 to 100, default 50
13
- # @option options [Array<Hash>] :ordering one `{field: 'name' | 'createdAt', direction: 'asc' | 'desc'}`
13
+ # @option options [String] :sort `name` (the default) or `createdAt`, descending when prefixed with `-`
14
14
  # @option options [String] :search only children whose name contains it
15
- # @return [Hash] a cursor page (items, nextCursor, hasMore, pageSize, ordering) with ancestors.
15
+ # @return [Hash] a cursor page (items, nextCursor, hasMore) with ancestors.
16
16
  def list(options = {})
17
17
  call(options, __method__) do |api, request|
18
- response = request('GET', project('folders'), query: api, request_options: request)
19
- valid = response.is_a?(Hash) && response['items'].is_a?(Array) && response['ancestors'].is_a?(Array) &&
20
- [true, false].include?(response['hasMore'])
21
- raise InvalidResponseError.new(SdkErrorCodes::INVALID_RESPONSE, 'Invalid folder page') unless valid
22
-
23
- response
18
+ cursor_page(request('GET', project('folders'), query: api, request_options: request), 'folder')
24
19
  end
25
20
  end
26
21
 
22
+ # Iterates every child folder of `parent_id` (or the project root), fetching pages as needed.
23
+ # @param options [Hash] the options of {#list}; `cursor` sets the starting position.
24
+ # @yieldparam folder [Hash]
25
+ # @return [Enumerator, nil] an Enumerator when no block is given.
26
+ def each(options = {}, &block)
27
+ return enum_for(:each, options) unless block
28
+
29
+ each_item(options, __method__, method(:list), &block)
30
+ end
31
+
27
32
  # @option options [String] :name
28
33
  # @option options [String, nil] :parent_id
29
34
  # @return [Hash] the folder
@@ -8,20 +8,28 @@ module CalmLens
8
8
  # The prefix of this resource's operations in Internal::OPTION_KEYS.
9
9
  OPERATIONS = 'hashLists'
10
10
 
11
- # Lists one page of the project's hash lists, newest first.
11
+ # Lists one page of the project's hash lists, newest first unless `sort` says otherwise.
12
12
  # @option options [String] :cursor the previous page's `nextCursor`
13
13
  # @option options [Integer] :page_size 1 to 100, default 50
14
- # @return [Hash] a cursor page: items, nextCursor, hasMore, pageSize and ordering.
14
+ # @option options [String] :sort `createdAt` and `id`, comma-separated, descending when prefixed
15
+ # with `-`; `-createdAt` by default
16
+ # @return [Hash] a cursor page: items, nextCursor and hasMore.
15
17
  def list(options = {})
16
18
  call(options, __method__) do |api, request|
17
- response = request('GET', project('hash-lists'), query: api, request_options: request)
18
- valid = response.is_a?(Hash) && response['items'].is_a?(Array)
19
- raise InvalidResponseError.new(SdkErrorCodes::INVALID_RESPONSE, 'Invalid hash list page') unless valid
20
-
21
- response
19
+ cursor_page(request('GET', project('hash-lists'), query: api, request_options: request), 'hash list')
22
20
  end
23
21
  end
24
22
 
23
+ # Iterates every hash list, fetching pages as needed.
24
+ # @param options [Hash] the options of {#list}; `cursor` sets the starting position.
25
+ # @yieldparam hash_list [Hash]
26
+ # @return [Enumerator, nil] an Enumerator when no block is given.
27
+ def each(options = {}, &block)
28
+ return enum_for(:each, options) unless block
29
+
30
+ each_item(options, __method__, method(:list), &block)
31
+ end
32
+
25
33
  # @option options [String] :hash_list_id
26
34
  # @return [Hash] the hash list
27
35
  def retrieve(options)
@@ -58,17 +66,29 @@ module CalmLens
58
66
  end
59
67
  end
60
68
 
61
- # Lists one page of a list's entries, newest first.
69
+ # Lists one page of a list's entries, newest first unless `sort` says otherwise.
62
70
  # @option options [String] :hash_list_id
63
71
  # @option options [String] :cursor the previous page's `nextCursor`
64
72
  # @option options [Integer] :page_size 1 to 100, default 50
65
- # @return [Hash] a cursor page: items, nextCursor, hasMore, pageSize and ordering.
73
+ # @option options [String] :sort `createdAt` and `id`, comma-separated, descending when prefixed
74
+ # with `-`; `-createdAt` by default
75
+ # @return [Hash] a cursor page: items, nextCursor and hasMore.
66
76
  def list_entries(options)
67
77
  call(options, __method__) do |api, request|
68
78
  request('GET', "#{list_path(api)}/entries", query: without(api, 'hashListId'), request_options: request)
69
79
  end
70
80
  end
71
81
 
82
+ # Iterates every entry of a list, fetching pages as needed.
83
+ # @param options [Hash] the options of {#list_entries}; `cursor` sets the starting position.
84
+ # @yieldparam entry [Hash]
85
+ # @return [Enumerator, nil] an Enumerator when no block is given.
86
+ def each_entry(options, &block)
87
+ return enum_for(:each_entry, options) unless block
88
+
89
+ each_item(options, __method__, method(:list_entries), &block)
90
+ end
91
+
72
92
  # Adds a PDQ hash, or an image asset's hash; a hash already in the list returns its entry.
73
93
  # @option options [String] :hash_list_id
74
94
  # @option options [String] :hash 64 hexadecimal characters; send this or `asset_id`
@@ -13,6 +13,9 @@ module CalmLens
13
13
  # ASCII characters without a space at either end) and answers `idempotency/invalid-key`.
14
14
  IDEMPOTENCY_KEY = /\A[^\r\n\0]+\z/
15
15
  HEADER_VALUE = /\A[^\r\n\0]*\z/
16
+ # The CDN resources that take no lens or transform: the exact upload, the adaptive stream, the embed page,
17
+ # the public placeholder and the display info.
18
+ UNSELECTABLE_CDN_RESOURCES = %w[original hls embed placeholder info].freeze
16
19
 
17
20
  module_function
18
21
 
@@ -103,10 +106,12 @@ module CalmLens
103
106
  raise invalid('Identifier is required') if text.empty?
104
107
  raise invalid('Identifier cannot be "." or ".."') if %w[. ..].include?(text)
105
108
 
106
- # Like JavaScript's encodeURIComponent.
107
- text.encode(Encoding::UTF_8).b.gsub(/[^A-Za-z0-9\-_.!~*'()]/n) { |byte| format('%%%02X', byte.ord) }
109
+ component(text)
108
110
  end
109
111
 
112
+ # Percent-encodes text like JavaScript's encodeURIComponent.
113
+ def component(text) = text.encode(Encoding::UTF_8).b.gsub(/[^A-Za-z0-9\-_.!~*'()]/n) { |byte| format('%%%02X', byte.ord) }
114
+
110
115
  # Splits an absolute URL into its parsed origin and its raw request target. Only the origin is
111
116
  # parsed, so the request target is sent exactly as the SDK built it.
112
117
  # @return [Array(URI::Generic, String)]
@@ -133,24 +138,9 @@ module CalmLens
133
138
  end
134
139
  end
135
140
 
136
- # Encodes query values the way the API's parser expects: strings as-is, everything else as
137
- # JSON, and ordering as `ordering[i][field]` pairs.
141
+ # Encodes query values the way the API's parser expects: strings as-is, everything else as JSON.
138
142
  def query(params)
139
- encoded = {}
140
- params.each do |key, value|
141
- next if value.nil?
142
-
143
- if key == 'ordering' && value.is_a?(Array)
144
- value.each_with_index do |order, index|
145
- raise invalid('Each ordering entry must be a Hash') unless order.is_a?(Hash)
146
-
147
- order.each { |name, item| encoded["ordering[#{index}][#{name}]"] = item.to_s }
148
- end
149
- else
150
- encoded[key] = value.is_a?(String) ? value : JSON.generate(value)
151
- end
152
- end
153
- encoded
143
+ params.compact.transform_values { |value| value.is_a?(String) ? value : JSON.generate(value) }
154
144
  end
155
145
  end
156
146
  private_constant :Internal
@@ -1,20 +1,93 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CalmLens
4
- # Lens definitions, cached per client. Used through {ServerClient#ensure_lenses} and {ServerClient#ensure_lens}.
5
- # @api private
4
+ # The project's lenses: named image transforms the CDN applies at `/@<identifier>`. The CDN serves
5
+ # files made with each lens's current definition. Reach it through {ServerClient#lenses};
6
+ # {ServerClient#ensure_lenses} creates or changes lenses by identifier, for applications that
7
+ # declare theirs in code.
6
8
  class Lenses < Resource
7
9
  # The prefix of this resource's operations in Internal::OPTION_KEYS.
8
- OPERATIONS = nil
10
+ OPERATIONS = 'lenses'
11
+
12
+ # Lists one page of the project's lenses, newest first unless `sort` says otherwise.
13
+ # @option options [String] :cursor the previous page's `nextCursor`
14
+ # @option options [Integer] :page_size 1 to 100, default 50
15
+ # @option options [String] :sort `createdAt` and `id`, comma-separated, descending when prefixed
16
+ # with `-`; `-createdAt` by default
17
+ # @return [Hash] a cursor page: items, nextCursor and hasMore.
18
+ def list(options = {})
19
+ call(options, __method__) do |api, request|
20
+ cursor_page(request('GET', project('lenses'), query: api, request_options: request), 'lens')
21
+ end
22
+ end
23
+
24
+ # Iterates every lens of the project, fetching pages as needed.
25
+ # @param options [Hash] the options of {#list}; `cursor` sets the starting position.
26
+ # @yieldparam lens [Hash]
27
+ # @return [Enumerator, nil] an Enumerator when no block is given.
28
+ def each(options = {}, &block)
29
+ return enum_for(:each, options) unless block
30
+
31
+ each_item(options, __method__, method(:list), &block)
32
+ end
33
+
34
+ # Creates a lens; its identifier must be new to the project (`lens/identifier-taken` otherwise).
35
+ # @option options [String] :identifier
36
+ # @option options [String] :name
37
+ # @option options [String] :type `image`
38
+ # @option options [Boolean] :active whether the CDN serves it
39
+ # @option options [String, nil] :description
40
+ # @option options [Hash, String, nil] :transform
41
+ # @return [Hash] the lens
42
+ def create(options)
43
+ call(options, __method__) do |api, request|
44
+ lens = request('POST', project('lenses'), body: api, request_options: request)
45
+ @session.forget_lenses { |cached| cached['identifier'] == lens['identifier'] } if lens.is_a?(Hash)
46
+ lens
47
+ end
48
+ end
49
+
50
+ # @option options [String] :lens_id
51
+ # @return [Hash] the lens
52
+ def retrieve(options)
53
+ call(options, __method__) { |api, request| request('GET', lens_path(api), request_options: request) }
54
+ end
55
+
56
+ # Changes a lens's name, description, type, transform or whether the CDN serves it.
57
+ # @option options [String] :lens_id
58
+ # @return [Hash] the lens
59
+ def update(options)
60
+ call(options, __method__) do |api, request|
61
+ lens = request('PATCH', lens_path(api), body: without(api, 'lensId'), request_options: request)
62
+ @session.forget_lenses { |cached| cached['id'] == lens['id'] } if lens.is_a?(Hash)
63
+ lens
64
+ end
65
+ end
66
+
67
+ # Deletes a lens; URLs that select it stop being served.
68
+ # @option options [String] :lens_id
69
+ # @return [nil]
70
+ def delete(options)
71
+ call(options, __method__) do |api, request|
72
+ request('DELETE', lens_path(api), request_options: request)
73
+ @session.forget_lenses { |cached| cached['id'] == api['lensId'] }
74
+ nil
75
+ end
76
+ end
9
77
 
10
78
  # Lens definitions are sent as given, nil fields included.
79
+ # @api private
11
80
  def ensure_lenses(options)
12
- call(options, __method__, keep_nil: true) { |api, request| @session.ensure_lenses(api['lenses'], request) }
81
+ call(options, __method__, keep_nil: true, prefix: nil) { |api, request| @session.ensure_lenses(api['lenses'], request) }
13
82
  end
14
83
 
84
+ # @api private
15
85
  def ensure_lens(options)
16
- call(options, __method__, keep_nil: true) { |api, request| @session.ensure_lenses([api], request).first }
86
+ call(options, __method__, keep_nil: true, prefix: nil) { |api, request| @session.ensure_lenses([api], request).first }
17
87
  end
88
+
89
+ private
90
+
91
+ def lens_path(api) = "/lenses/#{Internal.segment(api['lensId'])}"
18
92
  end
19
- private_constant :Lenses
20
93
  end
@@ -9,71 +9,98 @@ module CalmLens
9
9
  'assets.batch' => %w[assetIds operation].freeze,
10
10
  'assets.classify' => %w[
11
11
  authorId content context description externalId externalUrl folderId isFinal metadata name part
12
- processingOptions sizeInBytes totalSizeInBytes uploadPresetId url visibility
12
+ processingOptions sizeInBytes tags totalSizeInBytes uploadPresetId url visibility
13
13
  ].freeze,
14
14
  'assets.create' => %w[
15
15
  authorId content context description externalId externalUrl folderId isFinal metadata name part
16
- processingOptions sizeInBytes totalSizeInBytes uploadPresetId url visibility
16
+ processingOptions sizeInBytes tags totalSizeInBytes uploadPresetId url visibility
17
17
  ].freeze,
18
- 'assets.createSignedUrl' => %w[assetId expiresInSeconds lens resource transform viewerClaims].freeze,
19
18
  'assets.decide' => %w[assetId confirmPurge decision note].freeze,
20
19
  'assets.delete' => %w[assetId].freeze,
21
- 'assets.deleteContent' => %w[assetId].freeze,
22
- 'assets.each' => %w[cursor filter folderId ordering pageSize].freeze,
23
- 'assets.eachChild' => %w[assetId cursor ordering pageSize].freeze,
20
+ 'assets.each' => %w[cursor filter pageSize sort].freeze,
21
+ 'assets.eachChild' => %w[assetId cursor pageSize sort].freeze,
22
+ 'assets.eachTimelineSegment' => %w[assetId cursor pageSize].freeze,
24
23
  'assets.explain' => %w[assetId rerun].freeze,
25
- 'assets.list' => %w[cursor filter folderId ordering pageSize].freeze,
26
- 'assets.listChildren' => %w[assetId cursor ordering pageSize].freeze,
24
+ 'assets.list' => %w[cursor filter pageSize sort].freeze,
25
+ 'assets.listChildren' => %w[assetId cursor pageSize sort].freeze,
26
+ 'assets.listTimeline' => %w[assetId cursor pageSize].freeze,
27
27
  'assets.move' => %w[assetId folderId].freeze,
28
+ 'assets.removeContent' => %w[assetId].freeze,
28
29
  'assets.retrieve' => %w[assetId].freeze,
29
30
  'assets.retry' => %w[assetId].freeze,
30
- 'assets.search' => %w[limit mode query].freeze,
31
+ 'assets.search' => %w[mode pageSize query].freeze,
31
32
  'assets.update' => %w[assetId description externalId externalUrl metadata name tags visibility].freeze,
32
- 'assets.uploadPart' => %w[assetId part queue].freeze,
33
- 'ensureLens' => %w[active description identifier name transform type version].freeze,
33
+ 'assets.uploadPart' => %w[assetId part wait].freeze,
34
+ 'cdn.signAssetUrl' => %w[
35
+ assetId expiresIn externalId lens original projectId resource transform viewerClaims
36
+ ].freeze,
37
+ 'ensureLens' => %w[active description identifier name transform type].freeze,
34
38
  'ensureLenses' => %w[lenses].freeze,
35
- 'events.each' => %w[cursor pageSize].freeze,
36
- 'events.eachPage' => %w[cursor pageSize].freeze,
37
- 'events.list' => %w[cursor pageSize].freeze,
39
+ 'events.each' => %w[assetId cursor pageSize types].freeze,
40
+ 'events.eachPage' => %w[assetId cursor pageSize types].freeze,
41
+ 'events.list' => %w[assetId cursor pageSize types].freeze,
38
42
  'folders.create' => %w[name parentId].freeze,
39
43
  'folders.delete' => %w[folderId].freeze,
40
- 'folders.list' => %w[cursor ordering pageSize parentId search].freeze,
44
+ 'folders.each' => %w[cursor pageSize parentId search sort].freeze,
45
+ 'folders.list' => %w[cursor pageSize parentId search sort].freeze,
41
46
  'folders.move' => %w[folderId parentId].freeze,
42
47
  'folders.retrieve' => %w[folderId].freeze,
43
48
  'folders.update' => %w[folderId name].freeze,
44
49
  'hashLists.addEntry' => %w[assetId hash hashListId label quality].freeze,
45
50
  'hashLists.create' => %w[action description name].freeze,
46
51
  'hashLists.delete' => %w[hashListId].freeze,
47
- 'hashLists.list' => %w[cursor ordering pageSize].freeze,
48
- 'hashLists.listEntries' => %w[cursor hashListId ordering pageSize].freeze,
52
+ 'hashLists.each' => %w[cursor pageSize sort].freeze,
53
+ 'hashLists.eachEntry' => %w[cursor hashListId pageSize sort].freeze,
54
+ 'hashLists.list' => %w[cursor pageSize sort].freeze,
55
+ 'hashLists.listEntries' => %w[cursor hashListId pageSize sort].freeze,
49
56
  'hashLists.removeEntry' => %w[entryId].freeze,
50
57
  'hashLists.retrieve' => %w[hashListId].freeze,
51
58
  'hashLists.update' => %w[action description hashListId name].freeze,
52
- 'policies.applyPreset' => %w[operation policyId presetVersion revision].freeze,
59
+ 'lenses.create' => %w[active description identifier name transform type].freeze,
60
+ 'lenses.delete' => %w[lensId].freeze,
61
+ 'lenses.each' => %w[cursor pageSize sort].freeze,
62
+ 'lenses.list' => %w[cursor pageSize sort].freeze,
63
+ 'lenses.retrieve' => %w[lensId].freeze,
64
+ 'lenses.update' => %w[active description lensId name transform type].freeze,
65
+ 'metadataFields.delete' => %w[fieldId revision].freeze,
66
+ 'metadataFields.ensure' => %w[fields revision].freeze,
67
+ 'metadataFields.list' => %w[].freeze,
53
68
  'policies.assign' => %w[policyId].freeze,
54
69
  'policies.create' => %w[description name presetId settings].freeze,
55
70
  'policies.delete' => %w[policyId].freeze,
56
- 'policies.list' => %w[].freeze,
71
+ 'policies.each' => %w[cursor pageSize sort].freeze,
72
+ 'policies.list' => %w[cursor pageSize sort].freeze,
73
+ 'policies.reset' => %w[policyId presetVersion revision].freeze,
57
74
  'policies.retrieve' => %w[policyId].freeze,
75
+ 'policies.retrieveAssigned' => %w[].freeze,
58
76
  'policies.setAutomaticUpdates' => %w[enabled policyId presetVersion revision].freeze,
59
77
  'policies.update' => %w[description name policyId revision settings].freeze,
78
+ 'policies.upgrade' => %w[policyId presetVersion revision].freeze,
79
+ 'projectSettings.get' => %w[].freeze,
80
+ 'projectSettings.update' => %w[revision searchIndexingBudgetUnits settings].freeze,
60
81
  'text.classify' => %w[
61
- authorId context description externalId externalUrl folderId metadata name processingOptions text
82
+ authorId context description externalId externalUrl folderId metadata name processingOptions tags text
62
83
  uploadPresetId visibility
63
- ].freeze
84
+ ].freeze,
85
+ 'uploadPresets.delete' => %w[uploadPresetId].freeze,
86
+ 'uploadPresets.each' => %w[cursor pageSize sort].freeze,
87
+ 'uploadPresets.ensure' => %w[uploadPresets].freeze,
88
+ 'uploadPresets.list' => %w[cursor pageSize sort].freeze
64
89
  }.freeze
65
90
 
66
91
  # The option keys each method sends as null when given nil; any other option given as nil
67
92
  # is left out, as the API refuses null for it.
68
93
  NULLABLE_OPTION_KEYS = {
69
94
  'assets.classify' => %w[
70
- authorId context description externalId folderId metadata processingOptions uploadPresetId visibility
95
+ authorId context description externalId folderId metadata processingOptions tags uploadPresetId
96
+ visibility
71
97
  ].freeze,
72
98
  'assets.create' => %w[
73
- authorId context description externalId folderId metadata processingOptions uploadPresetId visibility
99
+ authorId context description externalId folderId metadata processingOptions tags uploadPresetId
100
+ visibility
74
101
  ].freeze,
75
- 'assets.each' => %w[filter folderId].freeze,
76
- 'assets.list' => %w[filter folderId].freeze,
102
+ 'assets.each' => %w[filter].freeze,
103
+ 'assets.list' => %w[filter].freeze,
77
104
  'assets.move' => %w[folderId].freeze,
78
105
  'assets.update' => %w[description externalId externalUrl metadata tags visibility].freeze,
79
106
  'folders.create' => %w[parentId].freeze,
@@ -81,8 +108,11 @@ module CalmLens
81
108
  'hashLists.addEntry' => %w[label quality].freeze,
82
109
  'hashLists.create' => %w[description].freeze,
83
110
  'hashLists.update' => %w[description].freeze,
111
+ 'lenses.create' => %w[description transform].freeze,
112
+ 'lenses.update' => %w[description transform].freeze,
84
113
  'text.classify' => %w[
85
- authorId context description externalId folderId metadata processingOptions uploadPresetId visibility
114
+ authorId context description externalId folderId metadata processingOptions tags uploadPresetId
115
+ visibility
86
116
  ].freeze
87
117
  }.freeze
88
118
  end
@@ -1,18 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CalmLens
4
- # Moderation policies belong to the project's organisation and its projects share them. Every
4
+ # Moderation policies belong to the API key's organisation and its projects share them. Every
5
5
  # change sends the policy's `revision`, so a policy changed since is refused with
6
6
  # `policy/revision-conflict`. Through a project's key you can change only policies no other
7
- # project uses or owns. Reach it through {ServerClient#policies}.
7
+ # project uses. Reach it through {ServerClient#policies}.
8
8
  class Policies < Resource
9
9
  # The prefix of this resource's operations in Internal::OPTION_KEYS.
10
10
  OPERATIONS = 'policies'
11
11
 
12
- # Lists the organisation's policies, oldest first, and the ID of the one the project uses.
13
- # @return [Hash] `policies` and `projectPolicyId`
12
+ # Lists one page of the organisation's policies, oldest first unless `sort` says otherwise.
13
+ # @option options [String] :cursor the previous page's `nextCursor`
14
+ # @option options [Integer] :page_size 1 to 100, default 50
15
+ # @option options [String] :sort `createdAt` and `id`, comma-separated, descending when prefixed
16
+ # with `-`; `createdAt` by default
17
+ # @return [Hash] a cursor page: items, nextCursor and hasMore.
14
18
  def list(options = {})
15
- call(options, __method__) { |_api, request| request('GET', project('policies'), request_options: request) }
19
+ call(options, __method__) do |api, request|
20
+ cursor_page(request('GET', '/policies', query: api, request_options: request), 'policy')
21
+ end
22
+ end
23
+
24
+ # Iterates every policy of the organisation, fetching pages as needed.
25
+ # @param options [Hash] the options of {#list}; `cursor` sets the starting position.
26
+ # @yieldparam policy [Hash]
27
+ # @return [Enumerator, nil] an Enumerator when no block is given.
28
+ def each(options = {}, &block)
29
+ return enum_for(:each, options) unless block
30
+
31
+ each_item(options, __method__, method(:list), &block)
16
32
  end
17
33
 
18
34
  # @option options [String] :policy_id
@@ -21,21 +37,28 @@ module CalmLens
21
37
  call(options, __method__) { |api, request| request('GET', policy_path(api), request_options: request) }
22
38
  end
23
39
 
40
+ # The policy the project moderates with.
41
+ # @return [Hash] the policy
42
+ def retrieve_assigned(options = {})
43
+ call(options, __method__) { |_api, request| request('GET', project('policy'), request_options: request) }
44
+ end
45
+
24
46
  # Creates a policy from `settings` (defining every category) with a `name`, or a copy of the
25
47
  # latest release of a built-in preset with `preset_id`. It moderates nothing until it is
26
48
  # assigned to a project or named on an upload preset.
27
49
  # @option options [String] :name required with `settings`; a preset copy defaults to its name
28
50
  # @option options [String] :description
29
- # @option options [Hash] :settings
51
+ # @option options [Hash] :settings `categorySettings` with every category, each with its
52
+ # `action`, `allowances` and `restrictions`
30
53
  # @option options [String] :preset_id `general-community`, `children-platform`, `education-support`
31
54
  # or `wholesome-community`
32
55
  # @return [Hash] the policy
33
56
  def create(options)
34
- call(options, __method__) { |api, request| request('POST', project('policies'), body: api, request_options: request) }
57
+ call(options, __method__) { |api, request| request('POST', '/policies', body: api, request_options: request) }
35
58
  end
36
59
 
37
60
  # Changes the name, description or settings (complete, defining every category); later
38
- # moderation uses them. Saving a policy that follows automatic updates pins it as a copy.
61
+ # moderation uses them. Saving a `live` policy pins it.
39
62
  # @option options [String] :policy_id
40
63
  # @option options [Integer] :revision the policy's current revision
41
64
  # @option options [String] :name
@@ -48,22 +71,31 @@ module CalmLens
48
71
  end
49
72
  end
50
73
 
51
- # Resets a preset copy to its pinned preset version, or updates an unchanged copy to the latest
52
- # preset release.
74
+ # Restores the settings of the preset release a `pinned` or `copy` policy is pinned to.
75
+ # @option options [String] :policy_id
76
+ # @option options [Integer] :revision the policy's current revision
77
+ # @option options [Integer] :preset_version the pinned `preset.version` you reviewed
78
+ # @return [Hash] the policy
79
+ def reset(options)
80
+ call(options, __method__) do |api, request|
81
+ request('POST', "#{policy_path(api)}/reset", body: without(api, 'policyId'), request_options: request)
82
+ end
83
+ end
84
+
85
+ # Moves an unchanged `pinned` or `copy` policy to the latest release of its preset.
53
86
  # @option options [String] :policy_id
54
- # @option options [String] :operation `reset` or `update`
55
87
  # @option options [Integer] :revision the policy's current revision
56
- # @option options [Integer] :preset_version `preset.version` to reset, `preset.latestVersion` to update
88
+ # @option options [Integer] :preset_version the `preset.latestVersion` you reviewed
57
89
  # @return [Hash] the policy
58
- def apply_preset(options)
90
+ def upgrade(options)
59
91
  call(options, __method__) do |api, request|
60
- request('POST', "#{policy_path(api)}/preset", body: without(api, 'policyId'), request_options: request)
92
+ request('POST', "#{policy_path(api)}/upgrade", body: without(api, 'policyId'), request_options: request)
61
93
  end
62
94
  end
63
95
 
64
- # Switches automatic updates on or off for the organisation's own preset policy. Switching on
65
- # replaces the saved copy with the latest preset settings under the same ID; switching off pins
66
- # the current preset version as a copy with unchanged settings.
96
+ # Switches automatic updates on (mode `live`) or off (mode `pinned`) for the organisation's own
97
+ # preset policy. Switching on replaces the saved settings with the latest preset settings under
98
+ # the same ID; switching off pins the current preset version with unchanged settings.
67
99
  # @option options [String] :policy_id
68
100
  # @option options [Boolean] :enabled
69
101
  # @option options [Integer] :revision the policy's current revision
@@ -95,6 +127,6 @@ module CalmLens
95
127
 
96
128
  private
97
129
 
98
- def policy_path(api) = project("policies/#{Internal.segment(api['policyId'])}")
130
+ def policy_path(api) = "/policies/#{Internal.segment(api['policyId'])}"
99
131
  end
100
132
  end