camille 1.7.0 → 1.8.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: b7393e683a6c060b951c73b888a9e6723cf3333a6a2c1a338e525c95ba00050e
4
- data.tar.gz: 86bd555f6ffdce50614ed180043bc9de94476e2b803896a3d0e15b3a84f90576
3
+ metadata.gz: b669731de34897fd2acb06cac773161fe3118a5203d4c9fd3ae41f5a6fca39e8
4
+ data.tar.gz: bdeca5f3af494575e42fece40de7682f441f43040ac11fe9e88d63516c0fdbc9
5
5
  SHA512:
6
- metadata.gz: 3b9fa70ec2457f286d9627dc62f11d95d743f83b108f98b23f1f2446da9ca743216605f806fb129d12627792493b0a0328d6bcb582ecdb3ad24878bbcc58d8ad
7
- data.tar.gz: 7d8d08956a4f43cde2bf7efc94955626efd983abbf98e2d8ae4c39d20a049f7e2055e08a800c4c67154f55d733109e45acf48545c35aa18deb1fd53eab178d9b
6
+ metadata.gz: d65f75dc0f47bb06962313132d639ed6acd876ef5524cdbc8fb7e9ec55643f3de5b7e7984c372de017e1e3ddfbd161deb7794ee0876f019ef13de401301a4e62
7
+ data.tar.gz: 92cab47708e350863586256efab349a85b4f16572f402b336802f4c035f0f2511cbc406d6215c9b9312cc4958d498d3c7bdd41556f6566118f0271e6ff59d326
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.8.0
4
+
5
+ ### Added
6
+
7
+ * 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.
8
+ * `Camille::Rendered` now marshals as `[fingerprint, json]`, so entries stored with the default `Rails.cache` coder stay valid across changes to the class.
9
+
10
+ ### Changed
11
+
12
+ * Renamed `Camille::Controller::InvalidRenderArgumentError` to `Camille::Controller::RenderArgumentError`, matching the naming of the other controller errors. The old name remains as an alias.
13
+
3
14
  ## 1.7.0
4
15
 
5
16
  ### Fixed
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- camille (1.7.0)
4
+ camille (1.8.0)
5
5
  rails (>= 6.1, < 8.1)
6
6
 
7
7
  GEM
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.
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
@@ -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
@@ -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 InvalidRenderArgumentError < ::ArgumentError; end
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 InvalidRenderArgumentError.new("Expected key :json for `render` call.")
35
+ raise RenderArgumentError.new("Expected key :json for `render` call.")
35
36
  end
36
37
  else
37
38
  super
@@ -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/type.rb CHANGED
@@ -35,6 +35,10 @@ module Camille
35
35
  new.check_params value
36
36
  end
37
37
 
38
+ def self.render! value
39
+ new.render! value
40
+ end
41
+
38
42
  def self.klass_name
39
43
  name.gsub(/^Camille::Types::/, '')
40
44
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Camille
4
- VERSION = "1.7.0"
4
+ VERSION = "1.8.0"
5
5
  end
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.7.0
4
+ version: 1.8.0
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