camille 1.1.0 → 1.3.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: 9cce4fa44ca836262ebc8da14ad045ee0b5ea2ce832fd6852e863280c04b6658
4
- data.tar.gz: cfc0c1797ce19035efd81de11b1176154f50575b3d4a90834ef6de517ef1ea4b
3
+ metadata.gz: a996817e4aeefb69d5eca3c893c5ce8a5fb8edebf63a24c80913c5ea436aeadd
4
+ data.tar.gz: b7b70010798e0578b8e1ce44bc50610b412b1de49ea7aad53db0313fb4c3b89b
5
5
  SHA512:
6
- metadata.gz: f502c8340be0ecb804982d811a35072188e7cb1fdc6602cc2c986cea94228caaae2d650d4379c3ba413299186bd8753befc51f6e6943d720384a841b876425f5
7
- data.tar.gz: cf6fc100a3e11ade528d88d5fbe89451c37866a218b7ddb2c0b0fc18645b7ceb40dd393b813af80ac22908d545017fa6bfe74927010a67ce1fb4c35b7ff8928d
6
+ metadata.gz: cf8fc03e76741bc2515a5cc5fdeb869e8faa4bdb2292d4e68eeddaa2e7c597619de36a708aae04575abee1feec7efb07788e293b22ff4cf78e87ca68d3903ad3
7
+ data.tar.gz: 3bb4f395f92419c56ccd8b21b85d0da2dc1588530a03e0024a4e236053c97440031b06feb6593cce0012e95fc66b54653e3f1ea2894d7e37618c344ab0c282af
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.3.0
4
+
5
+ ### Added
6
+
7
+ * Added option to check controller params type: `config.check_params`.
8
+
9
+ ## 1.2.0
10
+
11
+ ### Added
12
+
13
+ * Added `TypeLiteralGenerator` and `SchemaLiteralGenerator` to allow partial endpoint generation with `CodeGenerator`.
14
+
3
15
  ## 1.1.0
4
16
 
5
17
  ### Added
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- camille (1.0.0)
4
+ camille (1.2.0)
5
5
  rails (>= 6.1, < 8.1)
6
6
 
7
7
  GEM
@@ -41,6 +41,10 @@ module Camille
41
41
  raise NotImplementedError
42
42
  end
43
43
 
44
+ def check_params value
45
+ check value
46
+ end
47
+
44
48
  def self.| other
45
49
  Camille::Type.instance(self) | other
46
50
  end
@@ -53,6 +57,10 @@ module Camille
53
57
  Camille::Type.instance(self)[]
54
58
  end
55
59
 
60
+ def self.check_params value
61
+ Camille::Type.instance(self).check_params value
62
+ end
63
+
56
64
  def self.directly_instantiable?
57
65
  instance_method(:initialize).arity == 0
58
66
  end
@@ -1,21 +1,38 @@
1
1
  require 'stringio'
2
2
 
3
3
  module Camille
4
- module CodeGenerator
5
- def self.generate_ts
4
+ class CodeGenerator
5
+ def initialize types_literal_lines: Camille::Types.literal_lines, schemas_literal_lines: Camille::Schemas.literal_lines
6
+ @types_literal_lines = types_literal_lines
7
+ @schemas_literal_lines = schemas_literal_lines
8
+ end
9
+
10
+ def generate_ts
6
11
  io = StringIO.new
7
- io.puts Camille::Configuration.ts_header
8
- io.puts
9
- Camille::Types.literal_lines.each do |line|
10
- io.puts "export #{line}"
11
- end
12
- io.puts
13
- io.print "export default "
14
- Camille::Schemas.literal_lines.each do |line|
15
- io.puts line
16
- end
12
+ generate_header io
13
+ generate_types io
14
+ generate_schemas io
17
15
  io.string
18
16
  end
19
17
 
18
+ private
19
+ def generate_header io
20
+ io.puts Camille::Configuration.ts_header
21
+ io.puts
22
+ end
23
+
24
+ def generate_types io
25
+ @types_literal_lines.each do |line|
26
+ io.puts "export #{line}"
27
+ end
28
+ io.puts
29
+ end
30
+
31
+ def generate_schemas io
32
+ io.print "export default "
33
+ @schemas_literal_lines.each do |line|
34
+ io.puts line
35
+ end
36
+ end
20
37
  end
21
38
  end
@@ -3,7 +3,7 @@ module Camille
3
3
  class Configuration
4
4
  class << self
5
5
  attr_reader :response_key_converter, :params_key_converter
6
- attr_accessor :ts_header, :ts_location
6
+ attr_accessor :ts_header, :ts_location, :check_params
7
7
 
8
8
  def load_default_configurations
9
9
  self.response_key_converter = lambda do |string|
@@ -13,6 +13,8 @@ module Camille
13
13
  self.params_key_converter = lambda do |string|
14
14
  string.underscore
15
15
  end
16
+
17
+ self.check_params = false
16
18
  end
17
19
 
18
20
  def response_key_converter= lambda
@@ -43,7 +43,26 @@ module Camille
43
43
  Camille::Loader.check_and_raise_exception
44
44
  if endpoint = camille_endpoint
45
45
  begin
46
- params.deep_transform_keys!{|key| Camille::Configuration.params_key_converter.call(key.to_s)}
46
+ if Camille::Configuration.check_params
47
+ # New way: check params against the endpoint's params_type if it exists
48
+ if endpoint.params_type
49
+ params_to_check = params.to_unsafe_h
50
+ check_result = endpoint.params_type.check_params(params_to_check)
51
+
52
+ if check_result.type_error?
53
+ string_io = StringIO.new
54
+ Camille::TypeErrorPrinter.new(check_result).print(string_io)
55
+ raise TypeError.new("\nType check failed for params.\n#{string_io.string}")
56
+ else
57
+ # Use the checked value which already has snake_case keys
58
+ self.params = ActionController::Parameters.new(check_result.value)
59
+ end
60
+ end
61
+ else
62
+ # Old way: just transform keys
63
+ params.deep_transform_keys!{|key| Camille::Configuration.params_key_converter.call(key.to_s)}
64
+ end
65
+
47
66
  result = super
48
67
  # When there's no `render` call, Rails will return status 204
49
68
  if response.status == 204
@@ -4,4 +4,9 @@ Camille.configure do |config|
4
4
  // DO NOT EDIT! This file is automatically generated.
5
5
  import request from './request'
6
6
  EOF
7
+
8
+ # Enable type checking for controller parameters.
9
+ # When enabled, params will be validated against the schema and automatically
10
+ # converted from camelCase to snake_case.
11
+ config.check_params = true
7
12
  end
@@ -4,7 +4,7 @@ module Camille
4
4
  class MainController < ActionController::Base
5
5
  def endpoints_ts
6
6
  Camille::Loader.check_and_raise_exception
7
- render plain: Camille::CodeGenerator.generate_ts
7
+ render plain: Camille::CodeGenerator.new.generate_ts
8
8
  end
9
9
  end
10
10
  end
@@ -0,0 +1,51 @@
1
+ module Camille
2
+ class SchemaLiteralGenerator
3
+ def initialize schemas
4
+ @schemas = schemas
5
+ end
6
+
7
+ def literal_lines
8
+ [
9
+ Camille::Line.new('{'),
10
+ tree_literal_lines(namespace_tree).map(&:do_indent),
11
+ Camille::Line.new('}')
12
+ ]
13
+ end
14
+
15
+ private
16
+ def namespace_tree
17
+ tree = {}
18
+
19
+ @schemas.sort_by(&:klass_name).each do |s|
20
+ path = s.klass_name.split('::')
21
+ *namespaces, schema_name = path
22
+
23
+ level = namespaces.inject(tree) do |level, namespace|
24
+ level[namespace] ||= {}
25
+ level[namespace]
26
+ end
27
+
28
+ level[schema_name] = s
29
+ end
30
+
31
+ tree
32
+ end
33
+
34
+ def tree_literal_lines tree
35
+ tree.map do |key, value|
36
+ if value.is_a? ::Hash
37
+ [
38
+ Camille::Line.new("#{Camille::Configuration.response_key_converter.call(key.to_s)}: {"),
39
+ tree_literal_lines(value).map(&:do_indent),
40
+ Camille::Line.new('},')
41
+ ]
42
+ else
43
+ value.literal_lines.tap do |lines|
44
+ lines.first.prepend("#{Camille::Configuration.response_key_converter.call(key.to_s)}: ")
45
+ lines.last.append(',')
46
+ end
47
+ end
48
+ end.flatten
49
+ end
50
+ end
51
+ end
@@ -1,47 +1,7 @@
1
1
  module Camille
2
2
  module Schemas
3
3
  def self.literal_lines
4
- [
5
- Camille::Line.new('{'),
6
- tree_literal_lines(namespace_tree).map(&:do_indent),
7
- Camille::Line.new('}')
8
- ]
4
+ Camille::SchemaLiteralGenerator.new(Camille::Loader.loaded_schemas).literal_lines
9
5
  end
10
-
11
- private
12
- def self.namespace_tree
13
- tree = {}
14
-
15
- Camille::Loader.loaded_schemas.sort_by(&:klass_name).each do |s|
16
- path = s.klass_name.split('::')
17
- *namespaces, schema_name = path
18
-
19
- level = namespaces.inject(tree) do |level, namespace|
20
- level[namespace] ||= {}
21
- level[namespace]
22
- end
23
-
24
- level[schema_name] = s
25
- end
26
-
27
- tree
28
- end
29
-
30
- def self.tree_literal_lines tree
31
- tree.map do |key, value|
32
- if value.is_a? ::Hash
33
- [
34
- Camille::Line.new("#{Camille::Configuration.response_key_converter.call(key.to_s)}: {"),
35
- tree_literal_lines(value).map(&:do_indent),
36
- Camille::Line.new('},')
37
- ]
38
- else
39
- value.literal_lines.tap do |lines|
40
- lines.first.prepend("#{Camille::Configuration.response_key_converter.call(key.to_s)}: ")
41
- lines.last.append(',')
42
- end
43
- end
44
- end.flatten
45
- end
46
6
  end
47
7
  end
data/lib/camille/type.rb CHANGED
@@ -23,10 +23,18 @@ module Camille
23
23
  @underlying.check value
24
24
  end
25
25
 
26
+ def check_params value
27
+ @underlying.check_params value
28
+ end
29
+
26
30
  def self.check value
27
31
  new.check value
28
32
  end
29
33
 
34
+ def self.check_params value
35
+ new.check_params value
36
+ end
37
+
30
38
  def self.klass_name
31
39
  name.gsub(/^Camille::Types::/, '')
32
40
  end
@@ -0,0 +1,14 @@
1
+ module Camille
2
+ class TypeLiteralGenerator
3
+ def initialize types
4
+ @types = types
5
+ end
6
+
7
+ def literal_lines
8
+ @types.sort_by(&:klass_name).map do |type|
9
+ instance = type.new
10
+ Camille::Line.new("type #{instance.literal} = #{instance.underlying.literal}")
11
+ end
12
+ end
13
+ end
14
+ end
@@ -9,32 +9,41 @@ module Camille
9
9
  end
10
10
 
11
11
  def check value
12
- if value.is_a? ::Array
13
- results = value.map.with_index do |element, index|
14
- [index, @content.check(element)]
15
- end
16
-
17
- errors = results.map do |index, result|
18
- if result.type_error?
19
- ["array[#{index}]", result]
20
- else
21
- nil
22
- end
23
- end.compact
12
+ check_with_method(value, :check)
13
+ end
24
14
 
25
- if errors.empty?
26
- Camille::Checked.new(fingerprint, results.map{|_, checked| checked.value})
27
- else
28
- Camille::TypeError.new(**errors.to_h)
29
- end
30
- else
31
- Camille::TypeError.new("Expected array, got #{value.inspect}.")
32
- end
15
+ def check_params value
16
+ check_with_method(value, :check_params)
33
17
  end
34
18
 
35
19
  def literal
36
20
  "#{@content.literal}[]"
37
21
  end
22
+
23
+ private
24
+ def check_with_method value, method_name
25
+ if value.is_a? ::Array
26
+ results = value.map.with_index do |element, index|
27
+ [index, @content.public_send(method_name, element)]
28
+ end
29
+
30
+ errors = results.map do |index, result|
31
+ if result.type_error?
32
+ ["array[#{index}]", result]
33
+ else
34
+ nil
35
+ end
36
+ end.compact
37
+
38
+ if errors.empty?
39
+ Camille::Checked.new(fingerprint, results.map{|_, checked| checked.value})
40
+ else
41
+ Camille::TypeError.new(**errors.to_h)
42
+ end
43
+ else
44
+ Camille::TypeError.new("Expected array, got #{value.inspect}.")
45
+ end
46
+ end
38
47
  end
39
48
  end
40
49
  end
@@ -12,27 +12,36 @@ module Camille
12
12
  end
13
13
 
14
14
  def check value
15
- left_result = @left.check value
16
- if left_result.type_error?
17
- Camille::TypeError.new(
18
- 'intersection.left' => left_result
19
- )
20
- else
21
- right_result = @right.check left_result.value
22
- if right_result.type_error?
23
- Camille::TypeError.new(
24
- 'intersection.right' => right_result
25
- )
26
- else
27
- Camille::Checked.new(fingerprint, right_result.value)
28
- end
29
- end
15
+ check_with_method(value, :check)
16
+ end
17
+
18
+ def check_params value
19
+ check_with_method(value, :check_params)
30
20
  end
31
21
 
32
22
  def literal
33
23
  "(#{@left.literal} & #{@right.literal})"
34
24
  end
35
25
 
26
+ private
27
+ def check_with_method value, method_name
28
+ left_result = @left.public_send(method_name, value)
29
+ if left_result.type_error?
30
+ Camille::TypeError.new(
31
+ 'intersection.left' => left_result
32
+ )
33
+ else
34
+ right_result = @right.public_send(method_name, left_result.value)
35
+ if right_result.type_error?
36
+ Camille::TypeError.new(
37
+ 'intersection.right' => right_result
38
+ )
39
+ else
40
+ Camille::Checked.new(fingerprint, right_result.value)
41
+ end
42
+ end
43
+ end
44
+
36
45
  end
37
46
  end
38
47
  end
@@ -47,6 +47,52 @@ module Camille
47
47
  end
48
48
  end
49
49
 
50
+ def check_params value
51
+ if value.is_a? Hash
52
+ # Convert camelCase keys to snake_case
53
+ converted_value = value.transform_keys do |key|
54
+ Camille::KeyConverter.convert_params_key(key.to_s)
55
+ end
56
+
57
+ fields_with_string_keys = @fields.transform_keys(&:to_s)
58
+ optional_keys_as_strings = @optional_keys.map(&:to_s)
59
+
60
+ # Now check using the regular check method which expects snake_case
61
+ keys = (fields_with_string_keys.keys + converted_value.keys).uniq
62
+ keys_to_check, keys_to_skip = keys.partition{|key| fields_with_string_keys[key]}
63
+
64
+ results = keys_to_check.map do |key|
65
+ type = fields_with_string_keys[key]
66
+ if optional_keys_as_strings.include?(key) && converted_value[key].nil?
67
+ nil
68
+ else
69
+ [key, type.check_params(converted_value[key])]
70
+ end
71
+ end.compact
72
+
73
+ errors = results.map do |key, result|
74
+ if result.type_error?
75
+ [key.to_s, result]
76
+ else
77
+ nil
78
+ end
79
+ end.compact
80
+
81
+ skipped_pairs = keys_to_skip.map do |key|
82
+ [key, converted_value[key]]
83
+ end
84
+
85
+ if errors.empty?
86
+ object = Hash[results.map{|key, checked| [key, checked.value]}.concat(skipped_pairs).to_h]
87
+ Camille::Checked.new(fingerprint, object)
88
+ else
89
+ Camille::TypeError.new(**errors.to_h)
90
+ end
91
+ else
92
+ Camille::TypeError.new("Expected hash, got #{value.inspect}.")
93
+ end
94
+ end
95
+
50
96
  def literal
51
97
  "{#{@fields.map{|k,v| "#{literal_key k}: #{v.literal}"}.join(', ')}}"
52
98
  end
@@ -9,32 +9,41 @@ module Camille
9
9
  end
10
10
 
11
11
  def check value
12
- if value.is_a?(::Array) && value.size == @elements.size
13
- results = @elements.map.with_index do |type, index|
14
- [index, type.check(value[index])]
15
- end
16
-
17
- errors = results.map do |index, result|
18
- if result.type_error?
19
- ["tuple[#{index}]", result]
20
- else
21
- nil
22
- end
23
- end.compact
12
+ check_with_method(value, :check)
13
+ end
24
14
 
25
- if errors.empty?
26
- Camille::Checked.new(fingerprint, results.map{|_, checked| checked.value})
27
- else
28
- Camille::TypeError.new(**errors.to_h)
29
- end
30
- else
31
- Camille::TypeError.new("Expected array of size #{@elements.size}, got #{value.inspect}.")
32
- end
15
+ def check_params value
16
+ check_with_method(value, :check_params)
33
17
  end
34
18
 
35
19
  def literal
36
20
  "[#{elements.map(&:literal).join(', ')}]"
37
21
  end
22
+
23
+ private
24
+ def check_with_method value, method_name
25
+ if value.is_a?(::Array) && value.size == @elements.size
26
+ results = @elements.map.with_index do |type, index|
27
+ [index, type.public_send(method_name, value[index])]
28
+ end
29
+
30
+ errors = results.map do |index, result|
31
+ if result.type_error?
32
+ ["tuple[#{index}]", result]
33
+ else
34
+ nil
35
+ end
36
+ end.compact
37
+
38
+ if errors.empty?
39
+ Camille::Checked.new(fingerprint, results.map{|_, checked| checked.value})
40
+ else
41
+ Camille::TypeError.new(**errors.to_h)
42
+ end
43
+ else
44
+ Camille::TypeError.new("Expected array of size #{@elements.size}, got #{value.inspect}.")
45
+ end
46
+ end
38
47
  end
39
48
  end
40
49
  end
@@ -10,25 +10,34 @@ module Camille
10
10
  end
11
11
 
12
12
  def check value
13
- left_result = @left.check value
14
- if left_result.type_error?
15
- right_result = @right.check value
16
- if right_result.type_error?
17
- Camille::TypeError.new(
18
- 'union.left' => left_result,
19
- 'union.right' => right_result
20
- )
21
- else
22
- Camille::Checked.new(fingerprint, right_result.value)
23
- end
24
- else
25
- Camille::Checked.new(fingerprint, left_result.value)
26
- end
13
+ check_with_method(value, :check)
14
+ end
15
+
16
+ def check_params value
17
+ check_with_method(value, :check_params)
27
18
  end
28
19
 
29
20
  def literal
30
21
  "(#{@left.literal} | #{@right.literal})"
31
22
  end
23
+
24
+ private
25
+ def check_with_method value, method_name
26
+ left_result = @left.public_send(method_name, value)
27
+ if left_result.type_error?
28
+ right_result = @right.public_send(method_name, value)
29
+ if right_result.type_error?
30
+ Camille::TypeError.new(
31
+ 'union.left' => left_result,
32
+ 'union.right' => right_result
33
+ )
34
+ else
35
+ Camille::Checked.new(fingerprint, right_result.value)
36
+ end
37
+ else
38
+ Camille::Checked.new(fingerprint, left_result.value)
39
+ end
40
+ end
32
41
  end
33
42
  end
34
43
  end
data/lib/camille/types.rb CHANGED
@@ -1,10 +1,7 @@
1
1
  module Camille
2
2
  module Types
3
3
  def self.literal_lines
4
- Camille::Loader.loaded_types.sort_by(&:klass_name).map do |type|
5
- instance = type.new
6
- Camille::Line.new("type #{instance.literal} = #{instance.underlying.literal}")
7
- end
4
+ Camille::TypeLiteralGenerator.new(Camille::Loader.loaded_types).literal_lines
8
5
  end
9
6
  end
10
7
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Camille
4
- VERSION = "1.1.0"
4
+ VERSION = "1.3.0"
5
5
  end
data/lib/camille.rb CHANGED
@@ -40,6 +40,8 @@ require_relative "camille/railtie"
40
40
  require_relative "camille/controller"
41
41
  require_relative "camille/loader"
42
42
  require_relative "camille/configuration"
43
+ require_relative "camille/type_literal_generator"
44
+ require_relative "camille/schema_literal_generator"
43
45
  require_relative "camille/code_generator"
44
46
  require_relative "camille/main_controller"
45
47
  require_relative "camille/key_converter"
@@ -57,6 +59,6 @@ module Camille
57
59
  end
58
60
 
59
61
  def self.generate_ts
60
- Camille::CodeGenerator.generate_ts
62
+ Camille::CodeGenerator.new.generate_ts
61
63
  end
62
64
  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.1.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - merely
@@ -76,11 +76,13 @@ files:
76
76
  - lib/camille/railtie.rb
77
77
  - lib/camille/rendered.rb
78
78
  - lib/camille/schema.rb
79
+ - lib/camille/schema_literal_generator.rb
79
80
  - lib/camille/schemas.rb
80
81
  - lib/camille/syntax.rb
81
82
  - lib/camille/type.rb
82
83
  - lib/camille/type_error.rb
83
84
  - lib/camille/type_error_printer.rb
85
+ - lib/camille/type_literal_generator.rb
84
86
  - lib/camille/types.rb
85
87
  - lib/camille/types/any.rb
86
88
  - lib/camille/types/array.rb