camille 0.4.0 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1d5aa31349909cebc62309be91631d632419b925d9a6ded594485dfa204e9170
4
- data.tar.gz: d1b1cb24a630a0c109fc1042636372cf193dd135f1ef17784f3907c1a169502e
3
+ metadata.gz: 1c6d8ec2d346e2ff02811be47d900bee304a82f5a96e213cbf5e1c9f96f3fe61
4
+ data.tar.gz: 1a67d813067334b228caa09c9cc10411fa4a3b56eab57610b415c129616a4533
5
5
  SHA512:
6
- metadata.gz: 7d65f32be6f7d8beee2d22b9d5cf5ed4fc84fa91c925605c6aa0be09d51475211ddbee5e23f321892d0f46fa59878af2d9753e3daac7c1e62f2ccf285e8147b6
7
- data.tar.gz: f2195c3c54f6c571abeb2fa3b541160f734c1ecee6adfaec4bc4025ad0be820ae4235028a3c91427aec3c7cbdcc4ba19ba5f7a3e7cb530a3e943913b57955a78
6
+ metadata.gz: 0d977978049f5c5589731135c24c360145c3145d3ded0be2d158ea67546eece82d8af7c07674e1895d03a3071418adc9b1a6a597e5246b22b96d0c7abb2123d3
7
+ data.tar.gz: b4fd67e56fdd11db3c00d1e1ddee3a775c8e3d87ba2423cbe652729ed3dfb3460591d658a89bd1192385c45f0507fbeae5172a03acac65fcb3ad0880fe3b1aa7
data/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ ## 0.4.2
4
+
5
+ ### Added
6
+
7
+ * Supported `transform` for custom types
8
+ * Added default DateTime and Decimal type to install generator
9
+
10
+ ## 0.4.1
11
+
12
+ ### Added
13
+
14
+ * Added utility types Pick and Omit
15
+
16
+ ### Fixed
17
+
18
+ * Prevent loader errors from being silently rescued by Rails in development environment
19
+
20
+ ## 0.4.0
21
+
22
+ The first stable release
data/Gemfile CHANGED
@@ -9,5 +9,4 @@ gem "rake", "~> 13.0"
9
9
 
10
10
  gem "rspec", "~> 3.0"
11
11
 
12
- gem "rails"
13
12
  gem "rspec-rails"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- camille (0.4.0)
4
+ camille (0.4.2)
5
5
  rails (>= 6.1, < 8)
6
6
 
7
7
  GEM
@@ -172,7 +172,6 @@ PLATFORMS
172
172
 
173
173
  DEPENDENCIES
174
174
  camille!
175
- rails
176
175
  rake (~> 13.0)
177
176
  rspec (~> 3.0)
178
177
  rspec-rails
data/README.md CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  ## Why?
4
4
 
5
- Traditionally, the JSON response from a Rails API server isn't typed. So even if we have TypeScript at the front-end, we still have little guarantee that our back-end would return the correct type and structure of data. In order to eliminate type mismatch between both ends, Camille provides a syntax for you to define type schema for your Rails API, and uses these schemas to generate the TypeScript functions for calling the API.
5
+ Traditionally, the JSON response from a Rails API server isn't typed. So even if we have TypeScript at the front-end, we still have little guarantee that our back-end would return the correct type and structure of data.
6
+
7
+ In order to eliminate type mismatch between both ends, Camille provides a syntax for you to define type schema for your Rails API, and uses these schemas to generate the TypeScript functions for calling the API.
6
8
 
7
9
  For example, an endpoint defined in Ruby, where `data` is a controller action,
8
10
 
@@ -84,6 +86,8 @@ data(params: {id: number}): Promise<{name: string}>
84
86
 
85
87
  Therefore, the front-end user is required to provide an `id` when they call this function. And they can expect to get a `name` from the response of this request. There are no more type mismatch between both ends.
86
88
 
89
+ The `params` type for an endpoint is required to be an object type, or a hash in Ruby, while `response` type can be any supported type, for example a `Boolean`.
90
+
87
91
  Camille will automatically add a Rails route for each endpoint. You don't need to do anything other than having the schema file in place.
88
92
 
89
93
  When defining an endpoint, you can also use `post` instead of `get` for non-idempotent requests. However, no other HTTP verbs are supported, because verbs in RESTful like `patch` and `delete` indicate what we do on resources, but in RPC-style design each request is merely a function call that does not concern RESTful resources.
@@ -153,12 +157,13 @@ params(
153
157
  number_literal: 1,
154
158
  string_literal: 'hello',
155
159
  # a custom type we defined above
156
- product: Product
160
+ product: Product,
161
+ # Pick and Omit use [] to enclose parameters instead of <>
162
+ pick: Pick[{a: 1, b: 2}, 'a' | 'b'],
163
+ omit: Omit[Product, 'id']
157
164
  )
158
165
  ```
159
166
 
160
- String literal types and probably enums are planned for the future.
161
-
162
167
  ### TypeScript generation
163
168
 
164
169
  After you have your types and schemas in place, you can visit `/camille/endpoints.ts` in development environment to have the TypeScript request functions generated.
@@ -235,6 +240,10 @@ object:
235
240
  [2]: Expected number, got "3".
236
241
  ```
237
242
 
243
+ ### Reloading
244
+
245
+ Everything in `config/camille/types` and `config/camille/schemas` will automatically reload after changes in development environment, just like other files in Rails.
246
+
238
247
  ## Development
239
248
 
240
249
  Run tests with `bundle exec rake`.
@@ -0,0 +1,9 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec path: '..'
4
+
5
+ gem "rake", "~> 13.0"
6
+ gem "rspec", "~> 3.0"
7
+ gem "rspec-rails"
8
+
9
+ gem 'rails', '~> 6.1.0'
@@ -0,0 +1,9 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec path: '..'
4
+
5
+ gem "rake", "~> 13.0"
6
+ gem "rspec", "~> 3.0"
7
+ gem "rspec-rails"
8
+
9
+ gem 'rails', '~> 7.0.0'
@@ -11,6 +11,15 @@ module Camille
11
11
  Camille::Types::Array.new(self)
12
12
  end
13
13
 
14
+ def transform_and_check value
15
+ transformed = transform value
16
+ [check(value), transformed]
17
+ end
18
+
19
+ def transform value
20
+ value
21
+ end
22
+
14
23
  def self.| other
15
24
  Camille::Type.instance(self) | other
16
25
  end
@@ -15,16 +15,16 @@ module Camille
15
15
  if endpoint = camille_endpoint
16
16
  render_options = args.last
17
17
  if value = render_options[:json]
18
- error = endpoint.response_type.check(value)
18
+ error, transformed = endpoint.response_type.transform_and_check(value)
19
19
  if error
20
20
  string_io = StringIO.new
21
21
  Camille::TypeErrorPrinter.new(error).print(string_io)
22
22
  raise TypeError.new("\nType check failed for response.\n#{string_io.string}")
23
23
  else
24
- if value.is_a? Hash
25
- value.deep_transform_keys!{|k| k.to_s.camelize(:lower)}
24
+ if transformed.is_a? Hash
25
+ transformed.deep_transform_keys!{|k| k.to_s.camelize(:lower)}
26
26
  end
27
- super(json: value)
27
+ super(json: transformed)
28
28
  end
29
29
  else
30
30
  raise ArgumentError.new("Expected key :json for `render` call.")
@@ -35,6 +35,7 @@ module Camille
35
35
  end
36
36
 
37
37
  def process_action(*)
38
+ Camille::Loader.check_and_raise_exception
38
39
  if endpoint = camille_endpoint
39
40
  params.deep_transform_keys!{|key| key.to_s.underscore}
40
41
  super
@@ -0,0 +1,31 @@
1
+ require 'date'
2
+
3
+ # This type is only for testing, to simulate the usage of transform.
4
+ module Camille
5
+ module CustomTypes
6
+ class Date < Camille::Type
7
+ include Camille::Types
8
+
9
+ alias_of(
10
+ type: 'Date',
11
+ value: Number
12
+ )
13
+
14
+ def transform value
15
+ case value
16
+ when ::Date
17
+ timestamp = value.to_time.to_i
18
+ when ::Time
19
+ timestamp = value.to_i
20
+ end
21
+ {
22
+ type: 'Date',
23
+ value: timestamp.to_i * 1000
24
+ }
25
+ end
26
+
27
+ end
28
+ end
29
+ end
30
+
31
+ Camille::Loader.loaded_types.delete(Camille::CustomTypes::Date)
@@ -10,8 +10,9 @@ module Camille
10
10
  copy_file "configuration.rb", "config/camille/configuration.rb"
11
11
  end
12
12
 
13
- def create_types_folder
14
- copy_file ".keep", "config/camille/types/.keep"
13
+ def create_date_time_and_decimal
14
+ copy_file "date_time.rb", "config/camille/types/date_time.rb"
15
+ copy_file "decimal.rb", "config/camille/types/decimal.rb"
15
16
  end
16
17
 
17
18
  def create_schemas_folder
@@ -0,0 +1,12 @@
1
+ using Camille::Syntax
2
+
3
+ class Camille::Types::DateTime < Camille::Type
4
+ include Camille::Types
5
+
6
+ alias_of(String)
7
+
8
+ # transforms a DateTime into a String of ISO 8601 format
9
+ def transform value
10
+ value.as_json
11
+ end
12
+ end
@@ -0,0 +1,16 @@
1
+ using Camille::Syntax
2
+
3
+ class Camille::Types::Decimal < Camille::Type
4
+ include Camille::Types
5
+
6
+ alias_of(Number)
7
+
8
+ # transforms a BigDecimal into a Float so it fits in Number type
9
+ def transform value
10
+ if value.is_a? BigDecimal
11
+ value.to_f
12
+ else
13
+ value
14
+ end
15
+ end
16
+ end
@@ -41,11 +41,23 @@ module Camille
41
41
 
42
42
  def reload_types_and_schemas
43
43
  synchronize do
44
- Camille::Loader.loaded_types.clear
45
- Camille::Loader.loaded_schemas.clear
46
- @zeitwerk_loader.reload
47
- eager_load
48
- construct_controller_name_to_schema_map
44
+ begin
45
+ @exception = nil
46
+ Camille::Loader.loaded_types.clear
47
+ Camille::Loader.loaded_schemas.clear
48
+ @zeitwerk_loader.reload
49
+ eager_load
50
+ construct_controller_name_to_schema_map
51
+ rescue Exception => e
52
+ @exception = e
53
+ raise e
54
+ end
55
+ end
56
+ end
57
+
58
+ def check_and_raise_exception
59
+ if @exception
60
+ raise @exception
49
61
  end
50
62
  end
51
63
 
@@ -3,6 +3,7 @@ require 'action_controller'
3
3
  module Camille
4
4
  class MainController < ActionController::Base
5
5
  def endpoints_ts
6
+ Camille::Loader.check_and_raise_exception
6
7
  render plain: Camille::CodeGenerator.generate_ts
7
8
  end
8
9
  end
@@ -0,0 +1,51 @@
1
+ module Camille
2
+ # shared base class for Pick and Omit
3
+ class PickAndOmit < Camille::BasicType
4
+ class ArgumentError < ::ArgumentError; end
5
+
6
+ def initialize type, keys
7
+ @type = Camille::Type.instance(type)
8
+ case
9
+ when @type.is_a?(Camille::Types::Object)
10
+ @target_object = @type
11
+ when @type.is_a?(Camille::Type) && @type.underlying.is_a?(Camille::Types::Object)
12
+ @target_object = @type.underlying
13
+ else
14
+ raise ArgumentError.new("Currently onle an object type or an alias of object type is supported in #{klass_name}. Got #{type.inspect}.")
15
+ end
16
+
17
+ @keys = Camille::Type.instance(keys)
18
+ case
19
+ when @keys.is_a?(Camille::Types::StringLiteral)
20
+ @keys_array = [@keys.value].map(&:to_sym)
21
+ when @keys.is_a?(Camille::Types::Union)
22
+ unfolded = unfold_union(@keys).flatten
23
+ if unfolded.all?{|x| x.is_a?(Camille::Types::StringLiteral)}
24
+ @keys_array = unfolded.map(&:value).map(&:to_sym)
25
+ else
26
+ raise ArgumentError.new("The second argument of #{klass_name} has to be a string literal or an union of string literals. Got #{keys.inspect}.")
27
+ end
28
+ else
29
+ raise ArgumentError.new("The second argument of #{klass_name} has to be a string literal or an union of string literals. Got #{keys.inspect}.")
30
+ end
31
+ end
32
+
33
+ def literal
34
+ "#{klass_name}<#{@type.literal}, #{@keys.literal}>"
35
+ end
36
+
37
+ def self.[] type, keys
38
+ self.new type, keys
39
+ end
40
+
41
+ private
42
+ def unfold_union type
43
+ if type.is_a?(Camille::Types::Union)
44
+ [unfold_union(type.left), unfold_union(type.right)]
45
+ else
46
+ [type]
47
+ end
48
+ end
49
+
50
+ end
51
+ end
data/lib/camille/type.rb CHANGED
@@ -21,6 +21,11 @@ module Camille
21
21
  @underlying.check value
22
22
  end
23
23
 
24
+ def transform_and_check value
25
+ transformed = transform value
26
+ @underlying.transform_and_check transformed
27
+ end
28
+
24
29
  def self.klass_name
25
30
  name.gsub(/^Camille::Types::/, '')
26
31
  end
@@ -18,6 +18,11 @@ module Camille
18
18
  end
19
19
  end
20
20
 
21
+ def transform_and_check value
22
+ transformed = value.map{|x| @content.transform x}
23
+ [check(transformed), transformed]
24
+ end
25
+
21
26
  def literal
22
27
  "#{@content.literal}[]"
23
28
  end
@@ -3,6 +3,8 @@ module Camille
3
3
  class NumberLiteral < Camille::BasicType
4
4
  class ArgumentError < ::ArgumentError; end
5
5
 
6
+ attr_reader :value
7
+
6
8
  def initialize value
7
9
  if value.is_a?(Integer) || value.is_a?(Float)
8
10
  @value = value
@@ -21,6 +21,29 @@ module Camille
21
21
  end
22
22
  end
23
23
 
24
+ def transform_and_check value
25
+ if value.is_a? Hash
26
+ transform_and_check_results = @fields.map do |key, type|
27
+ [key, type.transform_and_check(value[key])]
28
+ end
29
+
30
+ errors = transform_and_check_results.map do |key, (error, transformed)|
31
+ [key.to_s, error]
32
+ end.select{|x| x[1]}
33
+
34
+ if errors.empty?
35
+ transformed = transform_and_check_results.map do |key, (error, transformed)|
36
+ [key, transformed]
37
+ end.to_h
38
+ [nil, transformed]
39
+ else
40
+ Camille::TypeError.new(**errors.to_h)
41
+ end
42
+ else
43
+ Camille::TypeError.new("Expected hash, got #{value.inspect}.")
44
+ end
45
+ end
46
+
24
47
  def literal
25
48
  "{#{@fields.map{|k,v| "#{ActiveSupport::Inflector.camelize k.to_s, false}: #{v.literal}"}.join(', ')}}"
26
49
  end
@@ -0,0 +1,24 @@
1
+ module Camille
2
+ module Types
3
+ class Omit < PickAndOmit
4
+ def check value
5
+ processed_object.check(value)
6
+ end
7
+
8
+ def transform_and_check value
9
+ processed_object.transform_and_check(value)
10
+ end
11
+
12
+ private
13
+ def klass_name
14
+ 'Omit'
15
+ end
16
+
17
+ def processed_object
18
+ fields = @target_object.fields.reject{|k, _| @keys_array.include?(k)}
19
+ Camille::Types::Object.new(fields)
20
+ end
21
+
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ module Camille
2
+ module Types
3
+ class Pick < PickAndOmit
4
+ def check value
5
+ processed_object.check(value)
6
+ end
7
+
8
+ def transform_and_check value
9
+ processed_object.transform_and_check(value)
10
+ end
11
+
12
+ private
13
+ def klass_name
14
+ 'Pick'
15
+ end
16
+
17
+ def processed_object
18
+ fields = @target_object.fields.select{|k, _| @keys_array.include?(k)}
19
+ Camille::Types::Object.new(fields)
20
+ end
21
+
22
+ end
23
+ end
24
+ end
@@ -3,6 +3,8 @@ module Camille
3
3
  class StringLiteral < Camille::BasicType
4
4
  class ArgumentError < ::ArgumentError; end
5
5
 
6
+ attr_reader :value
7
+
6
8
  def initialize value
7
9
  if value.is_a?(::String)
8
10
  @value = value
@@ -21,6 +21,26 @@ module Camille
21
21
  end
22
22
  end
23
23
 
24
+ def transform_and_check value
25
+ left_transformed = @left.transform value
26
+ left_result = @left.check left_transformed
27
+ if left_result
28
+ right_transformed = @right.transform value
29
+ right_result = @right.check right_transformed
30
+ if right_result
31
+ error = Camille::TypeError.new(
32
+ 'union.left' => left_result,
33
+ 'union.right' => right_result
34
+ )
35
+ [error, nil]
36
+ else
37
+ [nil, right_transformed]
38
+ end
39
+ else
40
+ [nil, left_transformed]
41
+ end
42
+ end
43
+
24
44
  def literal
25
45
  "#{@left.literal} | #{@right.literal}"
26
46
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Camille
4
- VERSION = "0.4.0"
4
+ VERSION = "0.4.2"
5
5
  end
data/lib/camille.rb CHANGED
@@ -17,6 +17,9 @@ require_relative "camille/types/tuple"
17
17
  require_relative "camille/types/any"
18
18
  require_relative "camille/types/number_literal"
19
19
  require_relative "camille/types/string_literal"
20
+ require_relative "camille/pick_and_omit"
21
+ require_relative "camille/types/pick"
22
+ require_relative "camille/types/omit"
20
23
  require_relative "camille/type"
21
24
  require_relative "camille/type_error"
22
25
  require_relative "camille/type_error_printer"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: camille
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - 辻彩
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-03-17 00:00:00.000000000 Z
11
+ date: 2023-03-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -38,28 +38,35 @@ extensions: []
38
38
  extra_rdoc_files: []
39
39
  files:
40
40
  - ".rspec"
41
+ - CHANGELOG.md
41
42
  - Gemfile
42
43
  - Gemfile.lock
43
44
  - README.md
44
45
  - Rakefile
45
46
  - bin/console
46
47
  - bin/setup
48
+ - gemfiles/rails-6.1
49
+ - gemfiles/rails-7.0
47
50
  - lib/camille.rb
48
51
  - lib/camille/basic_type.rb
49
52
  - lib/camille/code_generator.rb
50
53
  - lib/camille/configuration.rb
51
54
  - lib/camille/controller_extension.rb
55
+ - lib/camille/custom_types/date.rb
52
56
  - lib/camille/endpoint.rb
53
57
  - lib/camille/generators/install_generator.rb
54
58
  - lib/camille/generators/schema_generator.rb
55
59
  - lib/camille/generators/templates/.keep
56
60
  - lib/camille/generators/templates/configuration.rb
61
+ - lib/camille/generators/templates/date_time.rb
62
+ - lib/camille/generators/templates/decimal.rb
57
63
  - lib/camille/generators/templates/schema_template.erb
58
64
  - lib/camille/generators/templates/type_template.erb
59
65
  - lib/camille/generators/type_generator.rb
60
66
  - lib/camille/line.rb
61
67
  - lib/camille/loader.rb
62
68
  - lib/camille/main_controller.rb
69
+ - lib/camille/pick_and_omit.rb
63
70
  - lib/camille/railtie.rb
64
71
  - lib/camille/schema.rb
65
72
  - lib/camille/schemas.rb
@@ -75,6 +82,8 @@ files:
75
82
  - lib/camille/types/number.rb
76
83
  - lib/camille/types/number_literal.rb
77
84
  - lib/camille/types/object.rb
85
+ - lib/camille/types/omit.rb
86
+ - lib/camille/types/pick.rb
78
87
  - lib/camille/types/string.rb
79
88
  - lib/camille/types/string_literal.rb
80
89
  - lib/camille/types/tuple.rb