camille 1.7.0 → 1.8.1
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 +4 -4
- data/CHANGELOG.md +18 -0
- data/Gemfile.lock +1 -1
- data/README.md +25 -1
- data/camille.gemspec +36 -0
- data/lib/camille/basic_type.rb +14 -0
- data/lib/camille/controller.rb +3 -2
- data/lib/camille/pick_and_omit.rb +4 -0
- data/lib/camille/rendered.rb +17 -1
- data/lib/camille/testing.rb +4 -0
- data/lib/camille/type.rb +4 -0
- data/lib/camille/types/record.rb +34 -23
- data/lib/camille/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 43d1b00b15c6fb8dd557ad587bc0113f56328571ae309bf01e9f16a90cf6cc6e
|
|
4
|
+
data.tar.gz: f2f994348c8466586b6444885ea225446f15e45fac89f2942aa88ab135287195
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: be0fba9d87641faa8df13542937860f4d04d2cbfb5ec65b7e3ace488aff7d373f6c83d5601fe1aa046e6d6b0df6ab565cc8a5fbe9669a4084613822a6af3c328
|
|
7
|
+
data.tar.gz: 426ab4e50a688e656552d512bed7b9f324bab52e23165d461abdba7843228a80ab9e2956d7232664a0e0b9f953061316104f4e59544dfdc569b4455e6c0e2efe
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.8.1
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
* `Record`, `Pick` and `Omit` now implement `check_params`, so nested objects inside them get their camelCase keys converted instead of failing with errors like `Expected array, got nil`. Record keys are still not converted.
|
|
8
|
+
* `response.data` in `camille/testing` no longer type checks non-200 responses, matching `Camille::Controller#render`, which only checks and converts keys for 200 responses. Non-200 bodies are returned as-is.
|
|
9
|
+
|
|
10
|
+
## 1.8.0
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
* Added `render!` to every type (`Camille::Types::Product.render!(value)`), which checks a value and returns a `Camille::Rendered`, raising `Camille::BasicType::RenderError` on a type mismatch. A `Rendered` can be cached and placed into a later response, where the type accepts it by fingerprint without re-checking and its JSON is spliced verbatim. See "Caching rendered fragments" in the README.
|
|
15
|
+
* `Camille::Rendered` now marshals as `[fingerprint, json]`, so entries stored with the default `Rails.cache` coder stay valid across changes to the class.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
* Renamed `Camille::Controller::InvalidRenderArgumentError` to `Camille::Controller::RenderArgumentError`, matching the naming of the other controller errors. The old name remains as an alias.
|
|
20
|
+
|
|
3
21
|
## 1.7.0
|
|
4
22
|
|
|
5
23
|
### Fixed
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -259,6 +259,30 @@ object:
|
|
|
259
259
|
array[2]: Expected number, got "3".
|
|
260
260
|
```
|
|
261
261
|
|
|
262
|
+
### Caching rendered fragments
|
|
263
|
+
|
|
264
|
+
Type checking and key conversion run on every `render`. For data that is expensive to build and shared across requests, you can check it once and cache the result as a `Camille::Rendered`:
|
|
265
|
+
|
|
266
|
+
```ruby
|
|
267
|
+
rendered = Camille::Types::Product.render!(serialize(product))
|
|
268
|
+
# => Camille::Rendered with the type's fingerprint and the final JSON string
|
|
269
|
+
Rails.cache.write("product/#{product.id}/#{Camille::Types::Product.new.fingerprint}", rendered)
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
`render!` raises `Camille::BasicType::RenderError` if the value doesn't match the type. A `Rendered` can be placed anywhere a value of that type is expected:
|
|
273
|
+
|
|
274
|
+
```ruby
|
|
275
|
+
render json: {
|
|
276
|
+
products: Rails.cache.read_multi(*keys).values # Product[]
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
The type accepts it by comparing fingerprints instead of re-checking, and `to_json` splices the stored string verbatim, so no parsing or re-serialization happens on the cached fragments.
|
|
281
|
+
|
|
282
|
+
A `Rendered` is immutable and opaque. If a fragment needs to be modified before rendering, cache the plain hash instead and let Camille check it as usual; the two forms are a per-fragment choice. Keep volatile or per-user fields outside cached fragments, and include the type's `fingerprint` in the cache key so entries are invalidated when the type changes.
|
|
283
|
+
|
|
284
|
+
`Rendered` defines `marshal_dump`/`marshal_load` as `[fingerprint, json]`, so the default `Rails.cache` coder stores just the two strings. If you use the `:message_pack` cache serializer, register the class with `ActiveSupport::MessagePack::CacheSerializer` using the same pair.
|
|
285
|
+
|
|
262
286
|
### Reloading
|
|
263
287
|
|
|
264
288
|
Everything in `config/camille/types` and `config/camille/schemas` will automatically reload after changes in development environment, just like other files in Rails.
|
|
@@ -280,7 +304,7 @@ get '/products/data'
|
|
|
280
304
|
expect(response.data[:product][:available_stock]).to eq(1)
|
|
281
305
|
```
|
|
282
306
|
|
|
283
|
-
If
|
|
307
|
+
Since `Camille::Controller#render` only type checks and converts keys for 200 responses, `response.data` returns non-200 bodies as-is without validation. If a 200 response body fails the type check the helper raises `Camille::Testing::ResponseTypeError`. If the route has no Camille endpoint it raises `Camille::Testing::MissingEndpointError`.
|
|
284
308
|
|
|
285
309
|
## Versioning
|
|
286
310
|
|
data/camille.gemspec
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/camille/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "camille"
|
|
7
|
+
spec.version = Camille::VERSION
|
|
8
|
+
spec.authors = ["merely"]
|
|
9
|
+
spec.email = ["git@merely.ca"]
|
|
10
|
+
spec.license = "MIT"
|
|
11
|
+
|
|
12
|
+
spec.summary = "Typed API schema for Rails with TypeScript codegen"
|
|
13
|
+
spec.description = ""
|
|
14
|
+
spec.homepage = "https://github.com/onyxblade/camille"
|
|
15
|
+
spec.required_ruby_version = ">= 2.6.0"
|
|
16
|
+
|
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
18
|
+
spec.metadata["source_code_uri"] = "https://github.com/onyxblade/camille"
|
|
19
|
+
|
|
20
|
+
# Specify which files should be added to the gem when it is released.
|
|
21
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
22
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
23
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
24
|
+
(f == __FILE__) || f.match(%r{\A(?:(?:test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
spec.bindir = "exe"
|
|
28
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
29
|
+
spec.require_paths = ["lib"]
|
|
30
|
+
|
|
31
|
+
# Uncomment to register a new dependency of your gem
|
|
32
|
+
spec.add_dependency "rails", ">= 6.1", "< 8.1"
|
|
33
|
+
|
|
34
|
+
# For more information and examples about making a new gem, checkout our
|
|
35
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
|
36
|
+
end
|
data/lib/camille/basic_type.rb
CHANGED
|
@@ -4,6 +4,7 @@ module Camille
|
|
|
4
4
|
# This class specifies the methods available for all types includeing built-in and custom ones.
|
|
5
5
|
class BasicType
|
|
6
6
|
class InvalidTypeError < ::ArgumentError; end
|
|
7
|
+
class RenderError < ::StandardError; end
|
|
7
8
|
|
|
8
9
|
module CheckRendered
|
|
9
10
|
def check value
|
|
@@ -25,6 +26,19 @@ module Camille
|
|
|
25
26
|
@fingerprint = Digest::MD5.hexdigest self.class.name
|
|
26
27
|
end
|
|
27
28
|
|
|
29
|
+
# Checks `value` and returns a `Camille::Rendered`, raising `RenderError` with
|
|
30
|
+
# the printed type error if the check fails.
|
|
31
|
+
def render! value
|
|
32
|
+
result = check(value)
|
|
33
|
+
if result.type_error?
|
|
34
|
+
string_io = StringIO.new
|
|
35
|
+
Camille::TypeErrorPrinter.new(result).print(string_io)
|
|
36
|
+
raise RenderError.new("\nType check failed for render!.\n#{string_io.string}")
|
|
37
|
+
else
|
|
38
|
+
result.render
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
28
42
|
def | other
|
|
29
43
|
Camille::Types::Union.new(self, other)
|
|
30
44
|
end
|
data/lib/camille/controller.rb
CHANGED
|
@@ -3,7 +3,8 @@ module Camille
|
|
|
3
3
|
class TypeError < ::StandardError; end
|
|
4
4
|
class ParamsTypeError < TypeError; end
|
|
5
5
|
class ResponseTypeError < TypeError; end
|
|
6
|
-
class
|
|
6
|
+
class RenderArgumentError < ::ArgumentError; end
|
|
7
|
+
InvalidRenderArgumentError = RenderArgumentError
|
|
7
8
|
class MissingRenderError < ::StandardError; end
|
|
8
9
|
|
|
9
10
|
def camille_schema
|
|
@@ -31,7 +32,7 @@ module Camille
|
|
|
31
32
|
super(json: rendered)
|
|
32
33
|
end
|
|
33
34
|
else
|
|
34
|
-
raise
|
|
35
|
+
raise RenderArgumentError.new("Expected key :json for `render` call.")
|
|
35
36
|
end
|
|
36
37
|
else
|
|
37
38
|
super
|
data/lib/camille/rendered.rb
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
module Camille
|
|
2
|
+
# A JSON string that has already passed a type check for the type identified by
|
|
3
|
+
# `fingerprint`. Any type will accept a `Rendered` with a matching fingerprint
|
|
4
|
+
# without re-checking it, and `to_json` splices the string verbatim, so a
|
|
5
|
+
# `Rendered` can be stored (e.g. in `Rails.cache`) and placed into a later
|
|
6
|
+
# response as an opaque, immutable fragment.
|
|
2
7
|
class Rendered
|
|
3
8
|
attr_reader :fingerprint, :json
|
|
4
9
|
|
|
@@ -10,5 +15,16 @@ module Camille
|
|
|
10
15
|
def to_json options = nil
|
|
11
16
|
@json
|
|
12
17
|
end
|
|
18
|
+
|
|
19
|
+
# Marshal (and therefore the default `Rails.cache` coder) stores only the two
|
|
20
|
+
# strings, so cached entries stay valid across changes to this class's
|
|
21
|
+
# internals.
|
|
22
|
+
def marshal_dump
|
|
23
|
+
[@fingerprint, @json]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def marshal_load array
|
|
27
|
+
@fingerprint, @json = array
|
|
28
|
+
end
|
|
13
29
|
end
|
|
14
|
-
end
|
|
30
|
+
end
|
data/lib/camille/testing.rb
CHANGED
|
@@ -24,6 +24,10 @@ module Camille
|
|
|
24
24
|
"No camille endpoint for #{controller_class_name}##{action}."
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# Camille::Controller#render only type checks and converts keys for
|
|
28
|
+
# 200 responses, so mirror that here and return non-200 bodies as-is.
|
|
29
|
+
return deep_indifferent(parsed_body) unless status == 200
|
|
30
|
+
|
|
27
31
|
result = endpoint.response_type.check_params(parsed_body)
|
|
28
32
|
if result.type_error?
|
|
29
33
|
io = StringIO.new
|
data/lib/camille/type.rb
CHANGED
data/lib/camille/types/record.rb
CHANGED
|
@@ -11,28 +11,14 @@ module Camille
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def check value
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
results = value.map.with_index do |(k, v), index|
|
|
17
|
-
[index, check_pair(k, v)]
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
errors = results.map do |index, result|
|
|
21
|
-
if result.instance_of?(Camille::TypeError)
|
|
22
|
-
["record[#{index}]", result]
|
|
23
|
-
else
|
|
24
|
-
nil
|
|
25
|
-
end
|
|
26
|
-
end.compact
|
|
14
|
+
check_with_method(value, :check)
|
|
15
|
+
end
|
|
27
16
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
else
|
|
34
|
-
Camille::TypeError.new("Expected hash, got #{value.inspect}.")
|
|
35
|
-
end
|
|
17
|
+
# Record keys are dynamic, so they are never converted between
|
|
18
|
+
# camelCase and snake_case. Only the values are checked with
|
|
19
|
+
# `check_params` so nested objects get their keys converted.
|
|
20
|
+
def check_params value
|
|
21
|
+
check_with_method(value, :check_params)
|
|
36
22
|
end
|
|
37
23
|
|
|
38
24
|
def self.[] key, value
|
|
@@ -44,9 +30,34 @@ module Camille
|
|
|
44
30
|
end
|
|
45
31
|
|
|
46
32
|
private
|
|
47
|
-
def
|
|
33
|
+
def check_with_method value, method_name
|
|
34
|
+
if value.is_a? ::Hash
|
|
35
|
+
|
|
36
|
+
results = value.map.with_index do |(k, v), index|
|
|
37
|
+
[index, check_pair(k, v, method_name)]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
errors = results.map do |index, result|
|
|
41
|
+
if result.instance_of?(Camille::TypeError)
|
|
42
|
+
["record[#{index}]", result]
|
|
43
|
+
else
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
end.compact
|
|
47
|
+
|
|
48
|
+
if errors.empty?
|
|
49
|
+
Camille::Checked.new(fingerprint, results.map{|_, result| [result[0].value, result[1].value]}.to_h)
|
|
50
|
+
else
|
|
51
|
+
Camille::TypeError.new(**errors.to_h)
|
|
52
|
+
end
|
|
53
|
+
else
|
|
54
|
+
Camille::TypeError.new("Expected hash, got #{value.inspect}.")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def check_pair key, value, method_name
|
|
48
59
|
key_result = @key.check key
|
|
49
|
-
value_result = @value.
|
|
60
|
+
value_result = @value.public_send(method_name, value)
|
|
50
61
|
|
|
51
62
|
if key_result.checked? && value_result.checked?
|
|
52
63
|
[key_result, value_result]
|
data/lib/camille/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: camille
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.8.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- merely
|
|
@@ -48,6 +48,7 @@ files:
|
|
|
48
48
|
- benchmarks/returning_multiple_values.rb
|
|
49
49
|
- bin/console
|
|
50
50
|
- bin/setup
|
|
51
|
+
- camille.gemspec
|
|
51
52
|
- gemfiles/rails-7.2
|
|
52
53
|
- gemfiles/rails-8.0
|
|
53
54
|
- lib/camille.rb
|