calmlens 0.20.0 → 0.21.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.
@@ -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,56 +9,72 @@ 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,
53
65
  'policies.assign' => %w[policyId].freeze,
54
66
  'policies.create' => %w[description name presetId settings].freeze,
55
67
  'policies.delete' => %w[policyId].freeze,
56
- 'policies.list' => %w[].freeze,
68
+ 'policies.each' => %w[cursor pageSize sort].freeze,
69
+ 'policies.list' => %w[cursor pageSize sort].freeze,
70
+ 'policies.reset' => %w[policyId presetVersion revision].freeze,
57
71
  'policies.retrieve' => %w[policyId].freeze,
72
+ 'policies.retrieveAssigned' => %w[].freeze,
58
73
  'policies.setAutomaticUpdates' => %w[enabled policyId presetVersion revision].freeze,
59
74
  'policies.update' => %w[description name policyId revision settings].freeze,
75
+ 'policies.upgrade' => %w[policyId presetVersion revision].freeze,
60
76
  'text.classify' => %w[
61
- authorId context description externalId externalUrl folderId metadata name processingOptions text
77
+ authorId context description externalId externalUrl folderId metadata name processingOptions tags text
62
78
  uploadPresetId visibility
63
79
  ].freeze
64
80
  }.freeze
@@ -67,13 +83,15 @@ module CalmLens
67
83
  # is left out, as the API refuses null for it.
68
84
  NULLABLE_OPTION_KEYS = {
69
85
  'assets.classify' => %w[
70
- authorId context description externalId folderId metadata processingOptions uploadPresetId visibility
86
+ authorId context description externalId folderId metadata processingOptions tags uploadPresetId
87
+ visibility
71
88
  ].freeze,
72
89
  'assets.create' => %w[
73
- authorId context description externalId folderId metadata processingOptions uploadPresetId visibility
90
+ authorId context description externalId folderId metadata processingOptions tags uploadPresetId
91
+ visibility
74
92
  ].freeze,
75
- 'assets.each' => %w[filter folderId].freeze,
76
- 'assets.list' => %w[filter folderId].freeze,
93
+ 'assets.each' => %w[filter].freeze,
94
+ 'assets.list' => %w[filter].freeze,
77
95
  'assets.move' => %w[folderId].freeze,
78
96
  'assets.update' => %w[description externalId externalUrl metadata tags visibility].freeze,
79
97
  'folders.create' => %w[parentId].freeze,
@@ -81,8 +99,11 @@ module CalmLens
81
99
  'hashLists.addEntry' => %w[label quality].freeze,
82
100
  'hashLists.create' => %w[description].freeze,
83
101
  'hashLists.update' => %w[description].freeze,
102
+ 'lenses.create' => %w[description transform].freeze,
103
+ 'lenses.update' => %w[description transform].freeze,
84
104
  'text.classify' => %w[
85
- authorId context description externalId folderId metadata processingOptions uploadPresetId visibility
105
+ authorId context description externalId folderId metadata processingOptions tags uploadPresetId
106
+ visibility
86
107
  ].freeze
87
108
  }.freeze
88
109
  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
@@ -19,9 +19,9 @@ module CalmLens
19
19
  # beside a request's; Internal::OPTION_KEYS, generated from the API, has the rest.
20
20
  LOCAL_OPTION_KEYS = {
21
21
  'assets.waitForDecision' => %w[assetId timeout].freeze,
22
- 'cdn.getAssetUrl' => %w[assetIdentifier lens resource transform viewerClaims].freeze,
23
- 'cdn.serve' => %w[assetIdentifier init lens resource transform viewerClaims].freeze,
24
- 'uploadTokens.sign' => %w[expiresInSeconds externalId uploadPresetId].freeze
22
+ 'cdn.getAssetUrl' => %w[assetId externalId lens projectId resource transform viewerClaims].freeze,
23
+ 'cdn.serve' => %w[assetId externalId init lens projectId resource transform viewerClaims].freeze,
24
+ 'uploadTokens.sign' => %w[expiresIn externalId uploadPresetId].freeze
25
25
  }.freeze
26
26
  private_constant :LOCAL_OPTION_KEYS
27
27
 
@@ -30,9 +30,10 @@ module CalmLens
30
30
  # `sdk/invalid-argument` instead of being silently dropped.
31
31
  # An option given as nil is left out, as if omitted, unless the API takes null for it
32
32
  # (Internal::NULLABLE_OPTION_KEYS), such as `description: nil` to clear a description;
33
- # `keep_nil: true` sends every option as given, as for a lens definition.
34
- def call(options, method, keep_nil: false)
35
- operation = [self.class::OPERATIONS, Internal.camelize(method)].compact.join('.')
33
+ # `keep_nil: true` sends every option as given, as for a lens definition. `prefix` names the
34
+ # operation's resource, the resource's own unless the method belongs to the client itself.
35
+ def call(options, method, keep_nil: false, prefix: self.class::OPERATIONS)
36
+ operation = [prefix, Internal.camelize(method)].compact.join('.')
36
37
  check_option_keys(options, operation)
37
38
  api, request = Internal.options(options)
38
39
  nullable = Internal::NULLABLE_OPTION_KEYS.fetch(operation, [])
@@ -84,6 +85,23 @@ module CalmLens
84
85
  end
85
86
  end
86
87
 
88
+ # A list's response, once it is a cursor page: a Hash with an `items` Array. `what` names the
89
+ # items in the `sdk/invalid-response` error otherwise.
90
+ def cursor_page(response, what)
91
+ return response if response.is_a?(Hash) && response['items'].is_a?(Array)
92
+
93
+ raise InvalidResponseError.new(SdkErrorCodes::INVALID_RESPONSE, "Invalid #{what} page")
94
+ end
95
+
96
+ # Iterates the items of every page `list` returns for `options`, from its `cursor`, yielding each
97
+ # to `block`. `method` names the public iterator, whose options are checked like the list's.
98
+ def each_item(options, method, list, &block)
99
+ api, request = call(options, method) { |*split| split }
100
+ listing = pages(api['cursor']) { |cursor| list.call(api.merge('cursor' => cursor, 'request_options' => request)) }
101
+ listing.each { |page| page['items'].each(&block) }
102
+ nil
103
+ end
104
+
87
105
  # Records a cursor an iteration sends, raising `sdk/invalid-response` (with `details['cursor']`)
88
106
  # when a page with more to follow hands back one it already sent, which would repeat pages forever.
89
107
  def new_cursor!(sent, cursor)