calmlens 0.21.0 → 0.23.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: b99a6fc15b38b4dd664a1df33e33eb8943cbd57483e16d7b7224f93cbc23d2a1
4
- data.tar.gz: 668bf0e79e7fad1838be8d4e3c0bc536a379ea0de4cf3943a97057ed5025a72a
3
+ metadata.gz: f31d8d0e9739bebd69a0bab938d60914fb7173c8543b5521bcd5ac97e441311a
4
+ data.tar.gz: c85ad7397dd68637309b17204ff12dc7d52ec1e6415d66c42bacb7a000b9e9cf
5
5
  SHA512:
6
- metadata.gz: 00023a2b77c5dd78ebd7f0563ab9d152bede63882abc80c7a3e47d667f8e2d3ec54bade2c18fa4bd901167da1e8e3b7be2181491d1b3d5d1700bca568403d6ba
7
- data.tar.gz: 7e8a4bcab13c13c6278169e15a54bec41a9fc5849c366dbff1ef0208b6fceeeece777c157c52ea2286bfcca2135c64e41c8b878c6456e534fce51fd368dd61fe
6
+ metadata.gz: 7fb2274df7c0dfa834c97e786ab3b37816ec447c5d7694a293866a75882f11e4a052e684c3c1559a477a7e7419a02144a2af232291bf21f868bffa9fada5eed8
7
+ data.tar.gz: '090730bd39a5d48f41a7d7cfdbda56538166304f1af6cf6b22c9046997135f15a66384054a9e02751d168696b46c5a811b01abf825fc77f8a1f04b808da1c8d1'
data/README.md CHANGED
@@ -69,6 +69,7 @@ New statuses and fields may appear in later API versions. The SDK returns them u
69
69
  - [Serve content from the CDN](https://calmlens.com/docs/sdk/ruby#cdn-methods), with [lenses, transforms and signed URLs](https://calmlens.com/docs/sdk/ruby/reference#cdn-urls) and [age-restricted delivery](https://calmlens.com/docs/guides/delivering-assets#age-restricted-assets).
70
70
  - [Organise assets in folders](https://calmlens.com/docs/sdk/ruby/reference#folders), [search them](https://calmlens.com/docs/guides/search) and [page through lists](https://calmlens.com/docs/sdk/ruby#pagination).
71
71
  - [Match images against hash lists](https://calmlens.com/docs/sdk/ruby/reference#hash-lists).
72
+ - Keep [upload presets](https://calmlens.com/docs/guides/upload-presets), project settings and metadata fields in your code: `upload_presets` (`list`, `each`, `ensure`, `delete`), `project_settings` (`get`, `update`) and `metadata_fields` (`list`, `ensure`, `delete`). `ensure` and `update` create or replace by key or ID, and the `revision` you read refuses a change made since.
72
73
  - [Read the events feed](https://calmlens.com/docs/sdk/ruby#pagination) and [verify webhooks in Rails or Rack](https://calmlens.com/docs/sdk/ruby#webhooks).
73
74
  - [Handle errors](https://calmlens.com/docs/sdk/ruby/reference#errors) with automatic [retries, timeouts and idempotency keys](https://calmlens.com/docs/sdk/ruby/reference#retries-and-timeouts).
74
75
  - [Type-check your code with RBS signatures](https://calmlens.com/docs/sdk/ruby/reference#types).
@@ -79,7 +80,7 @@ Every method is listed in the [Ruby SDK documentation](https://calmlens.com/docs
79
80
 
80
81
  `CalmLens::MockServerClient` from `calmlens/testing` runs the real client against an in-memory stand-in for the API and CDN, so you can test your integration offline. See [Ruby testing](https://calmlens.com/docs/sdk/ruby/testing).
81
82
 
82
- `CalmLens::TestInputs` signs test inputs with your project's test input key from the portal: `generate_text_code` turns a scenario, preset or signal into a text code, `paint_image` paints it as a test image, and `generate_media_code` gives a phrase for audio and video, so the real API returns the scenario you declared in projects with "Recognise test inputs" turned on. Submit a text code as the whole text and a test image as the whole upload.
83
+ `CalmLens::TestInputs` signs test inputs with your project's test input key from the portal: `generate_text_code` turns a scenario, preset or signal into a text code, `paint_image` paints it as a test image, and `generate_media_code` gives a phrase for audio and video, so the real API returns the scenario you declared. Submit a text code as the whole text and a test image as the whole upload.
83
84
 
84
85
  ## Links
85
86
 
@@ -15,8 +15,11 @@ module CalmLens
15
15
  # The prefix of this resource's operations in Internal::OPTION_KEYS.
16
16
  OPERATIONS = 'assets'
17
17
 
18
- SUBMIT_QUERY_KEYS = %w[name description processingOptions visibility metadata tags authorId context externalId
19
- externalUrl folderId uploadPresetId].freeze
18
+ # The asset fields an upload carries in its body: the JSON body for text, the `options` part of
19
+ # an upload form for files. They never go in the URL, which proxies and servers log, and the
20
+ # API refuses them there.
21
+ UPLOAD_OPTION_KEYS = %w[name description processingOptions visibility metadata tags authorId context externalId
22
+ externalUrl folderId uploadPresetId].freeze
20
23
  MAX_MULTIPART_IMAGE_BYTES = 20 * 1024 * 1024
21
24
  # The API's limits for one-request uploads: images up to 10 MiB (larger ones are uploaded in
22
25
  # parts), audio and video up to 50 MiB and other files up to 5,000 MiB.
@@ -276,6 +279,11 @@ module CalmLens
276
279
  # `{type:, data:}` where `data` is a binary String or an IO. An IO streams without being read into
277
280
  # memory; pass `size_in_bytes:` when its length is unknown (a pipe or socket).
278
281
  #
282
+ # Text is sent as a JSON body. Other content is sent as `multipart/form-data`: the asset's fields
283
+ # (name, description, metadata, author ID, context and so on) in its `options` part, then the
284
+ # file. The URL carries only transport values (`wait`, the content type and sizes), never the
285
+ # asset's fields.
286
+ #
279
287
  # The request carries an `Idempotency-Key` (generated unless you pass
280
288
  # `request_options: {idempotency_key:}`), so a retried upload never creates a second asset.
281
289
  #
@@ -302,7 +310,8 @@ module CalmLens
302
310
  end
303
311
 
304
312
  # Uploads content and waits for the moderation decision where the content type supports it.
305
- # Audio and video are always processed asynchronously. Takes the options of {#create}.
313
+ # Documents, websites, audio, video and files over 20 MiB sent in parts always finish in the
314
+ # background and return `pending`. Takes the options of {#create}.
306
315
  # @return [Hash] the asset; show it to everyone only when `classification.moderation.status` is
307
316
  # `allowed` or `limited` (reduce the reach of `limited` assets). Serve `age-restricted` assets
308
317
  # only behind your own age check, through signed URLs.
@@ -395,13 +404,37 @@ module CalmLens
395
404
  return upload_in_parts(options, request_options, wait, mime, body) if !multipart && splits?(content, body)
396
405
 
397
406
  check_upload_size(mime, body.size) unless multipart || @session.defer_upload_checks
398
- query = options.slice(*SUBMIT_QUERY_KEYS).merge('wait' => wait, 'contentType' => content['type'],
399
- 'sizeInBytes' => body.size)
400
- query.merge!(first_part_query(options, content, mime, body)) if multipart
407
+ part_query = first_part_query(options, content, mime, body) if multipart
408
+ if content.key?('plainText')
409
+ return expect_asset(request('POST', project('assets'), query: { 'wait' => wait },
410
+ body: text_body(options, content, multipart),
411
+ idempotent: true, request_options: request_options))
412
+ end
413
+
414
+ query = { 'wait' => wait, 'contentType' => content['type'], 'sizeInBytes' => body.size }
415
+ send_form(query.merge(part_query || {}), upload_fields(options), content['type'], body, multipart, request_options)
416
+ end
417
+
418
+ # The asset fields an upload sends, without unset values.
419
+ def upload_fields(options) = options.slice(*UPLOAD_OPTION_KEYS).compact
420
+
421
+ # The JSON body of a text upload: its fields and `content`, or a first `part` with the sizes.
422
+ def text_body(options, content, multipart)
423
+ text = content.except('sizeInBytes')
424
+ return upload_fields(options).merge('content' => text) unless multipart
425
+
426
+ sizes = { 'sizeInBytes' => options['sizeInBytes'] || content['sizeInBytes'],
427
+ 'totalSizeInBytes' => options['totalSizeInBytes'] }.compact
428
+ upload_fields(options).merge('part' => text, **sizes)
429
+ end
430
+
431
+ # Sends file content, or a first part, as an upload form: the fields in its `options` part and
432
+ # the bytes in its `file` part. The query holds only transport values.
433
+ def send_form(query, fields, type, body, multipart, request_options)
434
+ form, form_type = body.to_form(JSON.generate(fields), type)
401
435
  expect_asset(request('POST', project('assets'),
402
- query: query, raw: body, idempotent: true, request_options: request_options,
403
- upload: !content.key?('plainText'),
404
- headers: { 'Content-Type' => content['type'], 'X-CalmLens-Upload' => multipart ? 'part' : 'content' }))
436
+ query: query, raw: form, idempotent: true, request_options: request_options, upload: true,
437
+ headers: { 'Content-Type' => form_type, 'X-CalmLens-Upload' => multipart ? 'part' : 'content' }))
405
438
  end
406
439
 
407
440
  # Whether content goes in parts: not text, of known size, readable again and larger than a part.
@@ -428,12 +461,9 @@ module CalmLens
428
461
  piece = ->(number) { body.slice((number - 1) * part_size, [part_size, total - ((number - 1) * part_size)].min) }
429
462
  first = piece.call(1)
430
463
  type = options.dig('content', 'type')
431
- query = options.slice(*SUBMIT_QUERY_KEYS).merge('wait' => wait, 'contentType' => type, 'sizeInBytes' => first.size,
432
- 'partNumber' => 1, 'isFinal' => false, 'totalSizeInBytes' => total)
433
- asset = expect_asset(request('POST', project('assets'),
434
- query: query, raw: first, idempotent: true, upload: true,
435
- request_options: request_options.merge(idempotency_key: key),
436
- headers: { 'Content-Type' => type, 'X-CalmLens-Upload' => 'part' }))
464
+ query = { 'wait' => wait, 'contentType' => type, 'sizeInBytes' => first.size, 'partNumber' => 1, 'isFinal' => false,
465
+ 'totalSizeInBytes' => total }
466
+ asset = send_form(query, upload_fields(options), type, first, true, request_options.merge(idempotency_key: key))
437
467
  send_remaining_parts(asset, count, piece, key, request_options)
438
468
  end
439
469
 
@@ -473,7 +503,7 @@ module CalmLens
473
503
  headers: headers))
474
504
  end
475
505
 
476
- # Keeps only the earlier posts CalmLens uses, which keeps a raw upload's query short.
506
+ # Keeps only the earlier posts CalmLens uses, which keeps an upload's body short.
477
507
  def with_used_context(options)
478
508
  return options unless options.key?('context')
479
509
 
@@ -483,7 +513,8 @@ module CalmLens
483
513
  options.merge('context' => { 'items' => ModerationContext.select_items(context['items']) })
484
514
  end
485
515
 
486
- # The query of a first multipart part: its number, whether it is the last and the declared total.
516
+ # The transport values of a first multipart part: its number, whether it is the last and the
517
+ # declared total.
487
518
  def first_part_query(options, content, mime, body)
488
519
  total = options['totalSizeInBytes']
489
520
  unless total.nil? || (total.is_a?(Integer) && total.positive?)
data/lib/calmlens/cdn.rb CHANGED
@@ -53,7 +53,7 @@ module CalmLens
53
53
  # @option options [String] :resource `rendered` (the displayable file: an image in the best format the
54
54
  # client accepts, a PDF, or browser-safe video or audio), `original` (the exact upload), `preview` (a
55
55
  # still image for the asset), `hls` (the HLS master playlist of audio or video, at `hls/index.m3u8`,
56
- # once `content.hls` is set), `embed` (an HTML page that shows the asset, a viewer for its type, for an
56
+ # once `content.hls.status` is `ready`), `embed` (an HTML page that shows the asset, a viewer for its type, for an
57
57
  # iframe), `placeholder` (a small blurred PNG that the CDN serves publicly for every asset whatever its
58
58
  # visibility or moderation status, without age confirmation) or `info` (the asset's display info, as
59
59
  # {#get_asset_info} reads it). Without it, the URL is the asset URL itself. Lenses and transforms
@@ -69,7 +69,7 @@ module CalmLens
69
69
  # alpha-2 country. The CDN serves an asset whose moderation status is `age-restricted` only with
70
70
  # `age_confirmed: true`, even with a signed URL or the API key. Set it only where your app has
71
71
  # already decided the viewer may see age-restricted content. Your platform confirms the viewer's
72
- # age; CalmLens does not verify this claim, and anyone can add it to a URL.
72
+ # age; viewer claims are assertions from your app, and CalmLens doesn't verify them.
73
73
  # @return [String]
74
74
  def get_asset_url(options)
75
75
  call(options, __method__) { |api, _request| asset_url(api) }
@@ -40,12 +40,14 @@ module CalmLens
40
40
  PERMISSION_DENIED = 'auth/permission-denied'
41
41
  UPLOAD_TOO_LARGE = 'upload/too-large'
42
42
  DOCUMENT_TOO_MANY_ITEMS = 'document/too-many-items'
43
+ MEDIA_DURATION_LIMIT = 'media/duration-limit'
43
44
  INVALID_IMAGE = 'image/invalid'
44
45
  IMAGE_TOO_LARGE = 'image/too-large'
45
46
  FREE_UPLOADS_EXHAUSTED = 'usage/free-uploads-exhausted'
46
47
  UPLOADS_PAUSED = 'usage/uploads-paused'
47
48
  UNSUPPORTED_UPLOAD_TYPE = 'upload/unsupported-type'
48
49
  INVALID_UPLOAD_PAYLOAD = 'upload-payload/invalid'
50
+ UPLOAD_OPTIONS_IN_QUERY = 'upload/options-in-query'
49
51
  USE_UPLOAD_PART = 'upload/use-upload-part'
50
52
  UPLOAD_NOT_OPEN = 'upload/not-open'
51
53
  INVALID_UPLOAD_PART_NUMBER = 'upload-part/invalid-number'
@@ -102,6 +104,14 @@ module CalmLens
102
104
  UPLOAD_PRESET_DAILY_LIMIT_REACHED = 'upload-preset/daily-limit-reached'
103
105
  UPLOAD_PRESET_ORIGIN_NOT_ALLOWED = 'upload-preset/origin-not-allowed'
104
106
  UPLOAD_PRESET_FIELD_NOT_ALLOWED = 'upload-preset/field-not-allowed'
107
+ UPLOAD_PRESET_REVISION_CONFLICT = 'upload-preset/revision-conflict'
108
+ UPLOAD_PRESET_KEY_TAKEN = 'upload-preset/key-taken'
109
+ UPLOAD_PRESET_DAILY_LIMIT_REQUIRED = 'upload-preset/daily-limit-required'
110
+ UPLOAD_PRESET_INVALID_FOLDER = 'upload-preset/invalid-folder'
111
+ UPLOAD_PRESET_INVALID_POLICY = 'upload-preset/invalid-policy'
112
+ PROJECT_SETTINGS_REVISION_CONFLICT = 'project-settings/revision-conflict'
113
+ SEARCH_ESTIMATE_CHANGED = 'search/estimate-changed'
114
+ METADATA_FIELD_NOT_FOUND = 'metadata-field/not-found'
105
115
  UPLOAD_TOKEN_INVALID = 'upload-token/invalid'
106
116
  UPLOAD_TOKEN_EXPIRED = 'upload-token/expired'
107
117
  UPLOAD_TOKEN_USED = 'upload-token/used'
@@ -153,12 +163,14 @@ module CalmLens
153
163
  'auth/permission-denied' => 403,
154
164
  'upload/too-large' => 413,
155
165
  'document/too-many-items' => 422,
166
+ 'media/duration-limit' => 422,
156
167
  'image/invalid' => 422,
157
168
  'image/too-large' => 422,
158
169
  'usage/free-uploads-exhausted' => 403,
159
170
  'usage/uploads-paused' => 402,
160
171
  'upload/unsupported-type' => 415,
161
172
  'upload-payload/invalid' => 400,
173
+ 'upload/options-in-query' => 400,
162
174
  'upload/use-upload-part' => 409,
163
175
  'upload/not-open' => 409,
164
176
  'upload-part/invalid-number' => 409,
@@ -215,6 +227,14 @@ module CalmLens
215
227
  'upload-preset/daily-limit-reached' => 429,
216
228
  'upload-preset/origin-not-allowed' => 403,
217
229
  'upload-preset/field-not-allowed' => 400,
230
+ 'upload-preset/revision-conflict' => 409,
231
+ 'upload-preset/key-taken' => 409,
232
+ 'upload-preset/daily-limit-required' => 400,
233
+ 'upload-preset/invalid-folder' => 400,
234
+ 'upload-preset/invalid-policy' => 400,
235
+ 'project-settings/revision-conflict' => 409,
236
+ 'search/estimate-changed' => 409,
237
+ 'metadata-field/not-found' => 404,
218
238
  'upload-token/invalid' => 401,
219
239
  'upload-token/expired' => 401,
220
240
  'upload-token/used' => 409,
@@ -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')
@@ -35,7 +35,7 @@ module CalmLens
35
35
  # @option options [String] :identifier
36
36
  # @option options [String] :name
37
37
  # @option options [String] :type `image`
38
- # @option options [Boolean] :active whether the CDN serves it
38
+ # @option options [Boolean] :active whether the CDN serves it; true by default
39
39
  # @option options [String, nil] :description
40
40
  # @option options [Hash, String, nil] :transform
41
41
  # @return [Hash] the lens
@@ -62,6 +62,9 @@ module CalmLens
62
62
  'lenses.list' => %w[cursor pageSize sort].freeze,
63
63
  'lenses.retrieve' => %w[lensId].freeze,
64
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,
65
68
  'policies.assign' => %w[policyId].freeze,
66
69
  'policies.create' => %w[description name presetId settings].freeze,
67
70
  'policies.delete' => %w[policyId].freeze,
@@ -73,10 +76,16 @@ module CalmLens
73
76
  'policies.setAutomaticUpdates' => %w[enabled policyId presetVersion revision].freeze,
74
77
  'policies.update' => %w[description name policyId revision settings].freeze,
75
78
  'policies.upgrade' => %w[policyId presetVersion revision].freeze,
79
+ 'projectSettings.get' => %w[].freeze,
80
+ 'projectSettings.update' => %w[revision searchIndexingBudgetUnits settings].freeze,
76
81
  'text.classify' => %w[
77
82
  authorId context description externalId externalUrl folderId metadata name processingOptions tags text
78
83
  uploadPresetId visibility
79
- ].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
80
89
  }.freeze
81
90
 
82
91
  # The option keys each method sends as null when given nil; any other option given as nil
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CalmLens
4
+ # The project's upload presets: the rules for uploads made from browsers and apps. {#ensure}
5
+ # creates or replaces presets by `key`, for applications that declare theirs in code or
6
+ # configuration. Reach it through {ServerClient#upload_presets}.
7
+ class UploadPresets < Resource
8
+ # The prefix of this resource's operations in Internal::OPTION_KEYS.
9
+ OPERATIONS = 'uploadPresets'
10
+
11
+ # Lists one page of the project's upload presets, oldest first unless `sort` says otherwise.
12
+ # @option options [String] :cursor the previous page's `nextCursor`
13
+ # @option options [Integer] :page_size 1 to 100, default 50
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.
17
+ def list(options = {})
18
+ call(options, __method__) do |api, request|
19
+ cursor_page(request('GET', project('upload-presets'), query: api, request_options: request), 'upload preset')
20
+ end
21
+ end
22
+
23
+ # Iterates every upload preset of the project, fetching pages as needed.
24
+ # @param options [Hash] the options of {#list}; `cursor` sets the starting position.
25
+ # @yieldparam upload_preset [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
+
33
+ # Creates each preset, or replaces it with the definition sent, by `key`. Fields left out take
34
+ # their defaults; an unchanged preset keeps its revision. Every preset is checked before any is
35
+ # saved. Send each preset's `revision` (0 for one that must not exist yet) to refuse a change
36
+ # made since with `upload-preset/revision-conflict`.
37
+ # @option options [Array<Hash>] :upload_presets `[{key:, name:, revision:, access:, enabled:, ...}]`,
38
+ # at most 50
39
+ # @return [Hash] `uploadPresets`: the stored presets, in the order sent.
40
+ def ensure(options)
41
+ call(options, __method__) do |api, request|
42
+ request('PUT', project('upload-presets'), body: api, request_options: request)
43
+ end
44
+ end
45
+
46
+ # Deletes an upload preset. Browsers can no longer upload through it; assets already uploaded stay.
47
+ # @option options [String] :upload_preset_id
48
+ # @return [nil]
49
+ def delete(options)
50
+ call(options, __method__) do |api, request|
51
+ request('DELETE', "/upload-presets/#{Internal.segment(api['uploadPresetId'])}", request_options: request)
52
+ nil
53
+ end
54
+ end
55
+ end
56
+
57
+ # The project's settings, with defaults filled in. {#update} replaces them: settings left out
58
+ # return to their defaults. Send the `revision` you read to refuse a change made since with
59
+ # `project-settings/revision-conflict`. Reach it through {ServerClient#project_settings}.
60
+ class ProjectSettings < Resource
61
+ # The prefix of this resource's operations in Internal::OPTION_KEYS.
62
+ OPERATIONS = 'projectSettings'
63
+
64
+ # @return [Hash] `projectId`, `revision` and `settings`.
65
+ def get(options = {})
66
+ call(options, __method__) { |_api, request| request('GET', project('settings'), request_options: request) }
67
+ end
68
+
69
+ # Replaces the project's settings; unchanged settings keep their revision. The project's policy
70
+ # and metadata fields have their own methods.
71
+ # @option options [Integer] :revision the revision you read
72
+ # @option options [Hash] :settings the complete settings
73
+ # @option options [Numeric] :search_indexing_budget_units needed when turning on
74
+ # `aiMediaSearchEnabled`: the most indexing units you accept for existing media
75
+ # @return [Hash] `projectId`, `revision` and `settings`.
76
+ def update(options)
77
+ call(options, __method__) { |api, request| request('PUT', project('settings'), body: api, request_options: request) }
78
+ end
79
+ end
80
+
81
+ # The project's metadata fields, in display order. They share the project settings' `revision`;
82
+ # {#ensure} creates or replaces fields by ID and keeps the others. Reach it through
83
+ # {ServerClient#metadata_fields}.
84
+ class MetadataFields < Resource
85
+ # The prefix of this resource's operations in Internal::OPTION_KEYS.
86
+ OPERATIONS = 'metadataFields'
87
+
88
+ # @return [Hash] `projectId`, `revision` and `fields`.
89
+ def list(options = {})
90
+ call(options, __method__) { |_api, request| request('GET', project('metadata-fields'), request_options: request) }
91
+ end
92
+
93
+ # Creates each field, or replaces it by ID. Other fields are kept; new ones are added at the end.
94
+ # @option options [Integer] :revision the revision you read
95
+ # @option options [Array<Hash>] :fields `[{id:, title:, type:, icon:, color:, description:}]`
96
+ # @return [Hash] `projectId`, `revision` and `fields`.
97
+ def ensure(options)
98
+ call(options, __method__) { |api, request| request('PUT', project('metadata-fields'), body: api, request_options: request) }
99
+ end
100
+
101
+ # Deletes a field definition; assets keep the metadata values they hold.
102
+ # @option options [String] :field_id
103
+ # @option options [Integer] :revision the revision you read
104
+ # @return [nil]
105
+ def delete(options)
106
+ call(options, __method__) do |api, request|
107
+ request('DELETE', project("metadata-fields/#{Internal.segment(api['fieldId'])}"),
108
+ query: without(api, 'fieldId'), request_options: request)
109
+ nil
110
+ end
111
+ end
112
+ end
113
+ end
@@ -51,6 +51,12 @@ module CalmLens
51
51
  attr_reader :cdn
52
52
  # @return [Lenses]
53
53
  attr_reader :lenses
54
+ # @return [UploadPresets]
55
+ attr_reader :upload_presets
56
+ # @return [ProjectSettings]
57
+ attr_reader :project_settings
58
+ # @return [MetadataFields]
59
+ attr_reader :metadata_fields
54
60
 
55
61
  # @param api_key [String] a project API key. Keep it on your server.
56
62
  # @param project_id [String]
@@ -100,6 +106,9 @@ module CalmLens
100
106
  @events = Events.new(@session)
101
107
  @cdn = CDN.new(@session)
102
108
  @lenses = Lenses.new(@session)
109
+ @upload_presets = UploadPresets.new(@session)
110
+ @project_settings = ProjectSettings.new(@session)
111
+ @metadata_fields = MetadataFields.new(@session)
103
112
  end
104
113
 
105
114
  # @return [String]
@@ -125,7 +134,7 @@ module CalmLens
125
134
  # them. Definitions this client already saved are not sent again, and sending the same
126
135
  # definition again returns the lens unchanged. More than 100 new definitions are sent in
127
136
  # consecutive requests, in order. {#lenses} manages lenses one by one.
128
- # @option options [Array<Hash>] :lenses `[{identifier:, name:, type:, active:, transform:, ...}]`
137
+ # @option options [Array<Hash>] :lenses `[{identifier:, name:, type:, transform:, ...}]`, each served unless `active: false`
129
138
  # @return [Array<Hash>] the saved lenses, in the given order.
130
139
  def ensure_lenses(options) = @lenses.ensure_lenses(options)
131
140
 
@@ -180,11 +180,10 @@ module CalmLens
180
180
  private
181
181
 
182
182
  # The fields that identify a lens definition, as canonical JSON: an unset description or
183
- # transform is the same definition as an explicit null.
183
+ # transform is the same definition as an explicit null, and an unset `active` as `true`.
184
184
  def lens_key(lens)
185
185
  fields = { 'identifier' => lens['identifier'], 'name' => lens['name'], 'description' => lens['description'],
186
- 'type' => lens['type'], 'transform' => lens['transform'] }
187
- fields['active'] = lens['active'] if lens.key?('active')
186
+ 'type' => lens['type'], 'transform' => lens['transform'], 'active' => lens.fetch('active', true) }
188
187
  JSON.generate(canonical(fields))
189
188
  end
190
189
 
@@ -8,7 +8,7 @@ module CalmLens
8
8
 
9
9
  # Each category's qualifiers.
10
10
  QUALIFIERS = {
11
- "sexual" => ["sexual-pornographic-media", "sexual-nudity", "sexual-activity", "sexual-explicit-language", "sexual-assault-media", "sexual-non-consensual", "sexual-intimate-image-abuse", "sexual-extreme", "sexual-strangulation", "sexual-incest", "sexual-minors", "sexual-solicitation", "sexual-paid", "sexual-adult-promotion", "sexual-revealing-clothing", "sexual-suggestive", "sexual-non-erotic-nudity", "sexual-text", "sexual-historical", "sexual-documentary", "sexual-educational", "sexual-discussion", "sexual-fictional", "sexual-support-prevention"].freeze,
11
+ "sexual" => ["sexual-pornographic-media", "sexual-nudity", "sexual-activity", "sexual-explicit-language", "sexual-assault-media", "sexual-non-consensual", "sexual-intimate-image-abuse", "sexual-extreme", "sexual-strangulation", "sexual-incest", "sexual-minors", "sexual-solicitation", "sexual-paid", "sexual-adult-promotion", "sexual-revealing-clothing", "sexual-suggestive", "sexual-non-erotic-nudity", "sexual-text", "sexual-historical", "sexual-documentary", "sexual-educational", "sexual-discussion", "sexual-fictional", "sexual-artistic", "sexual-support-prevention"].freeze,
12
12
  "violence" => ["violence-high-impact", "violence-attack", "violence-threat", "violence-preparation", "violence-encouragement", "violence-wish", "violence-coded-incitement", "violence-extremism", "violence-injury", "violence-violent-death", "violence-natural-death", "violence-graphic", "violence-decapitation-act", "violence-gore", "violence-weapon", "violence-sport", "violence-text", "violence-historical", "violence-documentary", "violence-educational", "violence-discussion", "violence-fictional", "violence-fantasy", "violence-support-prevention"].freeze,
13
13
  "self-harm" => ["self-harm-age-restricted", "self-harm-thoughts", "self-harm-preparation", "self-harm-act", "self-harm-methods", "self-harm-encouragement", "self-harm-historical", "self-harm-documentary", "self-harm-educational", "self-harm-fictional", "self-harm-support-prevention"].freeze,
14
14
  "harassment" => ["harassment-abuse", "harassment-mild", "harassment-bullying", "harassment-public-target", "harassment-threat", "harassment-sexual", "harassment-encouragement", "harassment-historical", "harassment-documentary", "harassment-educational", "harassment-fictional", "harassment-support-prevention"].freeze,