haveapi 0.32.1 → 0.32.2

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.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -8
  3. data/README.md +3 -422
  4. data/Rakefile +17 -7
  5. data/benchmarks/performance_compare.rb +27 -6
  6. data/doc/cs/client-examples.md +535 -0
  7. data/doc/cs/create-client.md +118 -0
  8. data/doc/cs/json-schema.html +1182 -0
  9. data/doc/cs/protocol.md +595 -0
  10. data/doc/cs/readme.md +432 -0
  11. data/doc/cs/typed-input-validation.md +122 -0
  12. data/doc/{client-examples.md → en/client-examples.md} +20 -10
  13. data/doc/{documentation-ui.md → en/documentation-ui.md} +42 -6
  14. data/doc/{performance.md → en/performance.md} +11 -0
  15. data/doc/{protocol.md → en/protocol.md} +2 -2
  16. data/doc/en/readme.md +423 -0
  17. data/haveapi.gemspec +2 -2
  18. data/lib/haveapi/client_example.rb +18 -5
  19. data/lib/haveapi/client_examples/curl.rb +5 -5
  20. data/lib/haveapi/client_examples/dynamic_helpers.rb +10 -10
  21. data/lib/haveapi/client_examples/go_client.rb +1 -1
  22. data/lib/haveapi/client_examples/http.rb +4 -4
  23. data/lib/haveapi/client_examples/js_client.rb +10 -10
  24. data/lib/haveapi/client_examples/php_client.rb +24 -25
  25. data/lib/haveapi/client_examples/python_client.rb +11 -11
  26. data/lib/haveapi/client_examples/ruby_cli.rb +6 -9
  27. data/lib/haveapi/client_examples/ruby_client.rb +24 -16
  28. data/lib/haveapi/client_examples/rust_client.rb +1 -1
  29. data/lib/haveapi/client_examples/ts_generated_client.rb +1 -1
  30. data/lib/haveapi/documentation_context.rb +14 -2
  31. data/lib/haveapi/documentation_helpers.rb +92 -15
  32. data/lib/haveapi/documentation_locale.rb +65 -0
  33. data/lib/haveapi/documentation_oauth.rb +12 -12
  34. data/lib/haveapi/documentation_search.rb +5 -5
  35. data/lib/haveapi/documentation_validators.rb +43 -33
  36. data/lib/haveapi/locales/cs.yml +468 -0
  37. data/lib/haveapi/locales/en.yml +467 -0
  38. data/lib/haveapi/public/css/documentation.css +8 -4
  39. data/lib/haveapi/public/js/documentation.js +73 -20
  40. data/lib/haveapi/server.rb +32 -47
  41. data/lib/haveapi/shared_documentation_helpers.rb +53 -5
  42. data/lib/haveapi/version.rb +1 -1
  43. data/lib/haveapi/views/doc_layout.erb +3 -2
  44. data/lib/haveapi/views/doc_sidebars/create-client.erb +10 -10
  45. data/lib/haveapi/views/doc_sidebars/json-schema.erb +2 -2
  46. data/lib/haveapi/views/doc_sidebars/protocol.erb +24 -24
  47. data/lib/haveapi/views/documentation/search_form.erb +5 -4
  48. data/lib/haveapi/views/documentation/search_results.erb +6 -6
  49. data/lib/haveapi/views/index.erb +3 -3
  50. data/lib/haveapi/views/main_layout.erb +4 -4
  51. data/lib/haveapi/views/shared_documentation.erb +8 -5
  52. data/lib/haveapi/views/shared_sidebar.erb +11 -11
  53. data/lib/haveapi/views/version_page/auth_body.erb +7 -7
  54. data/lib/haveapi/views/version_page/client_auth.erb +1 -1
  55. data/lib/haveapi/views/version_page/client_example.erb +1 -1
  56. data/lib/haveapi/views/version_page/client_init.erb +1 -1
  57. data/lib/haveapi/views/version_page/client_overview.erb +7 -5
  58. data/lib/haveapi/views/version_page/example_panels.erb +5 -2
  59. data/lib/haveapi/views/version_page/resource_body.erb +24 -24
  60. data/lib/haveapi/views/version_page/resource_directory.erb +2 -2
  61. data/lib/haveapi/views/version_page/usage.erb +5 -5
  62. data/lib/haveapi/views/version_page.erb +11 -11
  63. data/lib/haveapi/views/version_sidebar/auth_nav.erb +2 -2
  64. data/lib/haveapi/views/version_sidebar/authentication_control.erb +8 -8
  65. data/lib/haveapi/views/version_sidebar/authentication_status.erb +1 -1
  66. data/lib/haveapi/views/version_sidebar/brand.erb +16 -5
  67. data/lib/haveapi/views/version_sidebar.erb +33 -18
  68. data/spec/client_examples/assets/documentation_test.cjs +107 -9
  69. data/spec/client_examples/comment_capture.rb +72 -0
  70. data/spec/client_examples/comments_spec.rb +162 -0
  71. data/spec/client_examples/context_spec.rb +18 -2
  72. data/spec/client_examples/detail_spec.rb +68 -0
  73. data/spec/documentation/client_overview_spec.rb +60 -0
  74. data/spec/documentation/example_selection_spec.rb +192 -0
  75. data/spec/documentation/language_settings_spec.rb +132 -0
  76. data/spec/documentation/locale_spec.rb +367 -0
  77. data/spec/documentation/navigation_spec.rb +61 -3
  78. data/spec/documentation/oauth_spec.rb +42 -0
  79. data/spec/documentation/search_index_routes_spec.rb +8 -4
  80. data/spec/documentation/search_spec.rb +1 -1
  81. data/spec/documentation/shared_pages_spec.rb +63 -2
  82. data/spec/documentation/validators_spec.rb +25 -0
  83. data/spec/extensions/exception_mailer_spec.rb +2 -2
  84. data/spec/packaging_spec.rb +68 -9
  85. data/spec/performance/event_baseline_spec.rb +1 -1
  86. data/spec/performance/source_versions_spec.rb +111 -2
  87. metadata +24 -11
  88. /data/doc/{create-client.md → en/create-client.md} +0 -0
  89. /data/doc/{documentation-oauth.md → en/documentation-oauth.md} +0 -0
  90. /data/doc/{json-schema.html → en/json-schema.html} +0 -0
  91. /data/doc/{typed-input-validation.md → en/typed-input-validation.md} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fef6937fc8c3a3f3ae382a0c6032ae6fcc7900ad9fb6480aed9a9f3c43556c7d
4
- data.tar.gz: 4339642a36b69c79396e92fcba4acdd3cb13292a63a0afac90b9dfe22c0d4920
3
+ metadata.gz: c137421451f1521e445bf3e6d0316413a46775fb2a74d527908f4b03814441b9
4
+ data.tar.gz: 8a7a91920805fb36e27f5d8a8791a6b08a5a9721b8c90e49a9a53db42070f3a8
5
5
  SHA512:
6
- metadata.gz: 632a7cb540320f32451ff939e1d6044dd75a8382cd7d32ac0858a1a82533509c1e3935b73904d44f30b3226b4bec337b729d4a870aa4691adcbbfc0ab1c2da9a
7
- data.tar.gz: ed900045161d35036c3dd5002f395f0ca05e2db1a7d12327c74be483c2adafd2b554cc49313fd3a5ea089c29a9a16069f2d41d009dc3130b7784be4b9c588b42
6
+ metadata.gz: 25f59af897385a659412428247f02cf1290f0b56e8cd6bdf071b40eb263d1d750bf6b89329c2bd810d16264eb0520169fc54db2e367230bce26f65cfb6187440
7
+ data.tar.gz: 3ed4c37dd4a6138f9a7e4a8fbb762dd38d1d6cf78422bf0d02d5aa3bc8d6052ee3379d45d010ae8c3566e67e4e0fd68ae14c293c513d035b9e16a7a6bc222142
data/.gitignore CHANGED
@@ -1,8 +1,3 @@
1
- doc/protocol.*
2
- doc/create-client.md
3
- doc/typed-input-validation.md
4
- doc/client-examples.md
5
- doc/documentation-ui.md
6
- doc/documentation-oauth.md
7
- doc/performance.md
8
- doc/json-schema.html
1
+ doc/en/
2
+ doc/cs/
3
+ doc/protocol.png
data/README.md CHANGED
@@ -1,423 +1,4 @@
1
- # HaveAPI
2
- Server-side implementation of the [HaveAPI](https://github.com/vpsfreecz/haveapi)
3
- protocol in Ruby in the form of a framework that can be used to create
4
- self-descriptive RESTful web APIs. The framework features a DSL aimed at
5
- creating API resources, actions and specifying input/output parameters. HaveAPI
6
- handles everything from HTTP communication, authentication, parsing of input
7
- parameters and formatting output, so that users can focus on their bussiness
8
- logic.
1
+ # HaveAPI Ruby server
9
2
 
10
- ## Server framework features
11
- - Handles network communication, authentication, authorization, input/output formats
12
- and parameters, you need only to define resources and actions
13
- - By writing the code you get the documentation for free
14
- - Auto-generated online HTML documentation
15
- - ORM integration
16
- - ActiveRecord
17
- - integrated with controller
18
- - loads and documents validators from models
19
- - eager loading
20
- - easy to support other ORM frameworks
21
-
22
- ## Usage
23
- This text might not be complete or up-to-date, as things still often change.
24
- Full use of HaveAPI may be seen
25
- in [vpsadmin-api](https://github.com/vpsfreecz/vpsadmin-api)
26
- ([online doc](https://api.vpsfree.cz)), which may serve as an example of how
27
- are things meant to be used.
28
-
29
- All resources and actions are represented by classes. They all must be stored
30
- in a module, whose name is later given to HaveAPI. HaveAPI then searches all
31
- classes in this module and builds your API.
32
-
33
- For the purposes of this document, all resources will be in module `MyAPI`.
34
-
35
- ### Example
36
- This is a basic example, it does not show all options and functions.
37
-
38
- Let's assume a model:
39
-
40
- ```ruby
41
- class User < ActiveRecord::Base
42
- validates :login, :full_name, :role, presence: true
43
- validates :login, format: {
44
- with: /[a-zA-Z\.\-]{3,30}/,
45
- message: 'not a valid login'
46
- }, uniqueness: true
47
- validates :role, inclusion: {
48
- in: %w(admin user),
49
- message '%{value} is not a valid role'
50
- }
51
-
52
- # An example authentication with plain text password
53
- def self.authenticate(username, password)
54
- u = User.find_by(login: username)
55
-
56
- if u
57
- u if u.password == password
58
- end
59
- end
60
- end
61
- ```
62
-
63
- Resource user might look like this:
64
- ```ruby
65
- module MyAPI
66
- class User < HaveAPI::Resource
67
- # This resource belongs to version 1.
68
- # It is also possible to put resource to multiple versions, e.g. [1, 2]
69
- version 1
70
-
71
- # Provide description for this resource
72
- desc 'Manage users'
73
-
74
- # ActiveRecord model to load validators from
75
- model ::User
76
-
77
- # Require authentication, this is the default
78
- auth true
79
-
80
- # Create a named group of shared params, that may be later included
81
- # by actions.
82
- params(:id) do
83
- id :id, label: 'User ID'
84
- end
85
-
86
- params(:common) do
87
- string :login, label: 'Login', desc: 'Used for authentication'
88
- string :full_name, label: 'Full name'
89
- string :role, label: 'User role', desc: 'admin or user'
90
- end
91
-
92
- # Actions
93
- # Module HaveAPI::Actions::Default contains helper classes that define
94
- # HTTP methods and routes for generic actions.
95
- class Index < HaveAPI::Actions::Default::Index
96
- desc 'List all users'
97
-
98
- # There are no input parameters
99
-
100
- # Output parameters
101
- output(:object_list) do
102
- use :id
103
- use :common
104
- end
105
-
106
- # Determine if current user can use this action.
107
- # allow/deny immediately returns from this block.
108
- # Default rule is deny.
109
- authorize do |u|
110
- allow if u.role == 'admin'
111
- deny # deny is implicit, so it may be omitted
112
- end
113
-
114
- # Provide example usage
115
- example do
116
- request({})
117
- response({
118
- users: [
119
- {
120
- id: 1,
121
- login: 'myuser',
122
- full_name: 'My Very Name'
123
- }
124
- ]
125
- })
126
- comment 'Get a list of all users like this'
127
- end
128
-
129
- # Helper method returning a query for all users
130
- def query
131
- ::User.all
132
- end
133
-
134
- # This method is called if the request has meta[:count] = true
135
- def count
136
- query.count
137
- end
138
-
139
- # Execute action, return the list
140
- def exec
141
- with_pagination(query)
142
- end
143
- end
144
-
145
- class Create < HaveAPI::Actions::Default::Create
146
- desc 'Create new user'
147
-
148
- input do
149
- use :common
150
- end
151
-
152
- output do
153
- use :id
154
- use :common
155
- end
156
-
157
- authorize do |u|
158
- allow if u.role == 'admin'
159
- deny
160
- end
161
-
162
- example do
163
- request({
164
- user: {
165
- login: 'anotherlogin',
166
- full_name: 'My Very New Name'
167
- }
168
- })
169
- response({
170
- user: {
171
- id: 2
172
- }
173
- })
174
- comment 'Create new user like this'
175
- end
176
-
177
- def exec
178
- user = ::User.new(input)
179
-
180
- if user.save
181
- ok!(user)
182
- else
183
- error!('save failed', user.errors.to_hash)
184
- end
185
- end
186
- end
187
- end
188
- end
189
- ```
190
-
191
- ### What you get
192
- From this piece of code, HaveAPI will generate a self-describing API.
193
- It will contain resource `User` with actions `Index` and `Create`,
194
- using which you can list existing users and create new ones.
195
-
196
- You can use any of the available clients to work with the API.
197
-
198
- ### Run the example
199
- ```ruby
200
- api = HaveAPI::Server.new(MyAPI)
201
-
202
- # Use HTTP basic auth
203
- class BasicAuth < HaveAPI::Authentication::Basic::Provider
204
- def find_user(request, username, password)
205
- User.authenticate(username, password)
206
- end
207
- end
208
-
209
- api.use_version(:all)
210
- api.auth_chain << BasicAuth
211
- api.mount('/')
212
-
213
- api.start!
214
- ```
215
-
216
- This should start the application using WEBrick. Check
217
- [http://localhost:4567](http://localhost:4567).
218
-
219
- - `GET /` - a list of API versions
220
- - `GET /doc` - HaveAPI documentation
221
- - `GET /v1/` - documentation for version 1
222
- - `OPTIONS /` - description for the whole API
223
- - `OPTIONS /v1/` - description for API version 1
224
-
225
- and more.
226
-
227
- ## Localization
228
-
229
- HaveAPI can translate framework-owned response messages, validation errors and
230
- validator descriptions using Ruby `i18n`. The JSON envelope shape does not
231
- change; `message`, `errors` and self-description validator messages are still
232
- plain strings. Existing application-supplied strings passed to `error!` or
233
- custom validator `message:` options are returned unchanged.
234
-
235
- English is the default locale. Czech translations are bundled and can be
236
- selected with `Accept-Language: cs` or a regional tag such as
237
- `Accept-Language: cs-CZ`.
238
-
239
- ```ruby
240
- api = HaveAPI::Server.new(MyAPI)
241
-
242
- api.default_locale = :en
243
- api.available_locales = %i[en cs]
244
- api.locale_header = 'Accept-Language'
245
-
246
- api.locale do |request:, current_user:, default_locale:|
247
- current_user&.language&.code || default_locale
248
- end
249
- ```
250
-
251
- The explicit request header has precedence over the resolver. The resolver is
252
- called again after authentication, so applications can use authenticated user
253
- preferences when the client did not request a locale. If `locale_header` is set
254
- to a custom header name, HaveAPI also allows that header in CORS preflight
255
- responses. The header value uses the same syntax as `Accept-Language`.
256
-
257
- Actions can opt into application translations by passing lazy messages to
258
- `error!` or validator options:
259
-
260
- ```ruby
261
- error!(api_message('my_api.errors.quota_exceeded', limit: max_limit))
262
-
263
- input do
264
- string :name, required: {
265
- message: HaveAPI.message('my_api.validation.name_required')
266
- }
267
- end
268
- ```
269
-
270
- Use `api_t(key, **values)` when an immediate string translation is needed in an
271
- action. Use `HaveAPI.message(key, **values)` in class-level DSL blocks such as
272
- `input`.
273
-
274
- Action parameter labels and descriptions can be translated from the
275
- self-description context. Set `parameter_i18n_scope` to an application locale
276
- namespace and keep the existing English labels/descriptions as fallbacks:
277
-
278
- ```ruby
279
- api.parameter_i18n_scope = 'my_api'
280
-
281
- input do
282
- string :hostname,
283
- label: 'Hostname',
284
- desc: 'VPS hostname'
285
- end
286
- ```
287
-
288
- HaveAPI first looks up the exact action parameter key:
289
-
290
- ```yaml
291
- cs:
292
- my_api:
293
- resources:
294
- vps:
295
- actions:
296
- create:
297
- input:
298
- hostname:
299
- label: "Hostname"
300
- description: "Nazev VPS"
301
- ```
302
-
303
- The generated path is
304
- `resources.<resource_path>.actions.<action>.<input|output>.<name>`.
305
- Metadata parameters include the metadata type and direction, for example
306
- `resources.vps.actions.create.meta.global.output.action_state_id.label`.
307
-
308
- If the exact key is missing, HaveAPI falls back to resource input/output keys,
309
- resource attributes and then shared attributes:
310
-
311
- ```yaml
312
- cs:
313
- my_api:
314
- resources:
315
- vps:
316
- input:
317
- hostname:
318
- label: "Hostname"
319
- description: "Nazev VPS"
320
- output:
321
- hostname:
322
- label: "Hostname"
323
- attributes:
324
- hostname:
325
- label: "Hostname"
326
- attributes:
327
- hostname:
328
- label: "Hostname"
329
- ```
330
-
331
- Choice labels declared through the `choices`/`include` validator use the same
332
- parameter metadata path with `choices.<value>.label` appended. If choices were
333
- declared as a value-to-label map, the declared label is used as the English
334
- fallback. If choices were declared as an array, untranslated self-description
335
- keeps the array form; a locale that supplies labels is returned as the existing
336
- HaveAPI `{value: label}` map form.
337
-
338
- ```yaml
339
- cs:
340
- my_api:
341
- resources:
342
- vps:
343
- attributes:
344
- state:
345
- choices:
346
- running:
347
- label: "bezi"
348
- stopped:
349
- label: "vypnuto"
350
- ```
351
-
352
- Metadata parameters fall back through resource and global metadata keys:
353
-
354
- ```yaml
355
- cs:
356
- my_api:
357
- resources:
358
- vps:
359
- meta:
360
- global:
361
- output:
362
- count:
363
- label: "Return item count"
364
- meta:
365
- global:
366
- output:
367
- count:
368
- label: "Return item count"
369
- ```
370
-
371
- Framework-owned HaveAPI metadata, such as pagination and built-in meta
372
- parameters, is translated by HaveAPI itself and is not looked up in the
373
- application parameter scope. Use `label_key` and `desc_key` when a parameter
374
- needs an explicit key that is not derived from its action location:
375
-
376
- ```ruby
377
- string :hostname,
378
- label: 'Hostname',
379
- desc: 'VPS hostname',
380
- label_key: 'my_api.attributes.hostname.label',
381
- desc_key: 'my_api.attributes.hostname.description'
382
- ```
383
-
384
- HaveAPI also exposes `api.parameter_metadata_i18n_items` for maintenance tools
385
- that generate application locale catalogs from declared parameters.
386
-
387
- ### Run with rackup
388
- Use the same code as above, only the last line would be
389
-
390
- ```ruby
391
- run api.app
392
- ```
393
-
394
- ## Authentication
395
- HaveAPI defines an interface for creating authentication providers.
396
- HTTP basic auth and token providers are built-in.
397
-
398
- Authentication options are self-described. A client can choose what authentication
399
- method it understands and wants to use.
400
-
401
- ## Authorization
402
- HaveAPI provides means for authorizing user access to actions. This process
403
- is not self-described.
404
-
405
- If the user is authenticated when requesting self-description, only allowed
406
- resources, actions and parameters will be returned.
407
-
408
- ## Validation of input data
409
- Because validators are a part of the API's documentation, the clients can
410
- perform client-side validation before the data is sent to the API server.
411
-
412
- ## Blocking actions
413
- Blocking mode is for actions whose execution is not immediate but takes an unspecified
414
- amount of time. HaveAPI protocol allows clients to monitor progress of such actions
415
- or cancel their execution.
416
-
417
- ## Contributing
418
-
419
- 1. Fork it ( https://github.com/vpsfreecz/haveapi/fork )
420
- 2. Create your feature branch (`git checkout -b my-new-feature`)
421
- 3. Commit your changes (`git commit -am 'Add some feature'`)
422
- 4. Push to the branch (`git push origin my-new-feature`)
423
- 5. Create a new Pull Request
3
+ See the [server framework guide](../../doc/en/readme.md) for usage,
4
+ authentication, authorization and localization.
data/Rakefile CHANGED
@@ -9,12 +9,22 @@ require 'haveapi/tasks/yard'
9
9
  desc 'Check shared documentation required in the server gem'
10
10
  task :check_doc do
11
11
  required = %w[
12
- doc/json-schema.html
13
- doc/protocol.md
14
- doc/create-client.md
15
- doc/typed-input-validation.md
16
- doc/client-examples.md
17
12
  doc/protocol.png
13
+ doc/en/readme.md
14
+ doc/en/protocol.md
15
+ doc/en/create-client.md
16
+ doc/en/typed-input-validation.md
17
+ doc/en/client-examples.md
18
+ doc/en/documentation-ui.md
19
+ doc/en/documentation-oauth.md
20
+ doc/en/performance.md
21
+ doc/en/json-schema.html
22
+ doc/cs/readme.md
23
+ doc/cs/protocol.md
24
+ doc/cs/create-client.md
25
+ doc/cs/typed-input-validation.md
26
+ doc/cs/client-examples.md
27
+ doc/cs/json-schema.html
18
28
  ]
19
29
  missing = required.reject { |path| File.file?(path) && File.size?(path) }
20
30
 
@@ -40,11 +50,11 @@ begin
40
50
  '--protected',
41
51
  '--output-dir=html_doc',
42
52
  '--files=doc/*.md',
43
- '--files=doc/*.html'
53
+ '--files=doc/en/*.md',
54
+ '--files=doc/en/*.html'
44
55
  ]
45
56
  t.before = proc do
46
57
  document_hooks.call
47
- render_doc_file('doc/json-schema.erb', 'doc/JSON-Schema.html').call
48
58
  end
49
59
  end
50
60
  rescue LoadError
@@ -245,8 +245,14 @@ module HaveAPI
245
245
  COMPONENTS = Release::VERSIONS
246
246
  SPECS = COMPONENTS.to_h { |path, name| ["#{path.split('/lib/').first}/#{name}.gemspec", name] }.freeze
247
247
  PATHS = (['VERSION'] + COMPONENTS.keys + SPECS.keys).freeze
248
-
249
- def self.capture(contents)
248
+ SERVER_GEMSPEC = 'servers/ruby/haveapi.gemspec'.freeze
249
+ FILE_SELECTION_POLICY = 'documentation-files-v1'.freeze
250
+ FILE_SELECTIONS = [
251
+ " s.files = `git ls-files -z`.split(\"\\x0\") + Dir.glob('doc/*')\n",
252
+ " s.files = `git ls-files -z`.split(\"\\x0\") + Dir.glob('doc/**/*').select { |path| File.file?(path) }\n"
253
+ ].freeze
254
+
255
+ def self.capture(contents, normalize_files: true)
250
256
  raise ContractError, 'Incomplete own source metadata' unless contents.keys.sort == PATHS.sort
251
257
 
252
258
  own = contents.fetch('VERSION').delete_suffix("\n")
@@ -284,9 +290,22 @@ module HaveAPI
284
290
  normalized[path] = text.sub(requirement, "'haveapi-client', '~> #{PLACEHOLDER}'")
285
291
  end
286
292
  end
287
- { 'contents' => contents, 'own_version' => own,
288
- 'raw_sha256' => contents.transform_values { |text| Digest::SHA256.hexdigest(text) },
289
- 'normalized_sha256' => normalized.transform_values { |text| Digest::SHA256.hexdigest(text) } }
293
+ files = normalize_files && documentation_files(normalized.fetch(SERVER_GEMSPEC))
294
+ normalized[SERVER_GEMSPEC] = files if files
295
+ receipt = { 'contents' => contents, 'own_version' => own,
296
+ 'raw_sha256' => contents.transform_values { |text| Digest::SHA256.hexdigest(text) },
297
+ 'normalized_sha256' => normalized.transform_values { |text| Digest::SHA256.hexdigest(text) } }
298
+ receipt['file_selection'] = FILE_SELECTION_POLICY if files
299
+ receipt
300
+ end
301
+
302
+ # This reviewed packaging transition does not change the dependency graph.
303
+ # Unknown or ambiguous declarations retain their original strict bytes.
304
+ def self.documentation_files(text)
305
+ declarations = text.lines.grep(/^\s*s\.files\b/)
306
+ return unless declarations.length == 1 && FILE_SELECTIONS.include?(declarations.first)
307
+
308
+ text.sub(declarations.first, " s.files = DOCUMENTATION_FILES\n")
290
309
  end
291
310
 
292
311
  def self.gemspec(root, path, receipt)
@@ -307,7 +326,9 @@ module HaveAPI
307
326
 
308
327
  def self.validate_inputs(inputs, source)
309
328
  receipt = inputs.fetch('source_versions')
310
- raise ContractError, 'Own source metadata normalization mismatch' unless receipt == capture(receipt.fetch('contents'))
329
+ # Marker-absent legacy receipts used raw file-selection statements.
330
+ expected = capture(receipt.fetch('contents'), normalize_files: receipt.has_key?('file_selection'))
331
+ raise ContractError, 'Own source metadata normalization mismatch' unless receipt == expected
311
332
 
312
333
  receipt.fetch('raw_sha256').each do |path, digest|
313
334
  unless source.fetch('files_sha256').fetch(path) == digest && inputs.fetch('manifests').fetch(path) == receipt.fetch('normalized_sha256').fetch(path)