camille 0.3.0 → 0.4.0

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: 5779c76d0ca3c695e1e15d0870d94be5739449f7ad1b9422ab9944d139f3a6d1
4
- data.tar.gz: 855906b19324060e4fb8578451e1fd77f692918f6722a81b9f3fbdc5cbf48fd3
3
+ metadata.gz: 1d5aa31349909cebc62309be91631d632419b925d9a6ded594485dfa204e9170
4
+ data.tar.gz: d1b1cb24a630a0c109fc1042636372cf193dd135f1ef17784f3907c1a169502e
5
5
  SHA512:
6
- metadata.gz: e555aebf740ecd06dcf96928a3aeafe414430a7d439a83679da90d2e60e23a97cd28bc583a473ddab76c60275e31668b76f9220ca2278c21ecceb8f2f11743fa
7
- data.tar.gz: 00bf823aa9f394c1198cd492f42cd716b8bd59c08fe7e704df64aa92cdb927198ad87761f867de9b59153cba96490190292296755ef1642bd9c759820b00ecc0
6
+ metadata.gz: 7d65f32be6f7d8beee2d22b9d5cf5ed4fc84fa91c925605c6aa0be09d51475211ddbee5e23f321892d0f46fa59878af2d9753e3daac7c1e62f2ccf285e8147b6
7
+ data.tar.gz: f2195c3c54f6c571abeb2fa3b541160f734c1ecee6adfaec4bc4025ad0be820ae4235028a3c91427aec3c7cbdcc4ba19ba5f7a3e7cb530a3e943913b57955a78
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- camille (0.3.0)
4
+ camille (0.4.0)
5
5
  rails (>= 6.1, < 8)
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -1,8 +1,31 @@
1
1
  # Camille
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/camille`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ ## Why?
4
4
 
5
- TODO: Delete this and the text above, and describe your gem
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.
6
+
7
+ For example, an endpoint defined in Ruby, where `data` is a controller action,
8
+
9
+ ```ruby
10
+ get :data do
11
+ params(
12
+ id: Number
13
+ )
14
+ response(
15
+ name: String
16
+ )
17
+ end
18
+ ```
19
+
20
+ will become a function in TypeScript:
21
+
22
+ ```typescript
23
+ data(params: {id: number}): Promise<{name: string}>
24
+ ```
25
+
26
+ Therefore, if the front-end requests the API by calling `data`, we have guarantee that `id` is presented in `params`, and Camille will require the response to contain a string `name`, so the front-end can receive the correct type of data.
27
+
28
+ By using these request functions, we also don't need to know about HTTP verbs and paths. It's impossible to have unrecognized routes, since Camille will make sure that each function handled by the correct Rails action.
6
29
 
7
30
  ## Installation
8
31
 
@@ -14,22 +37,208 @@ gem 'camille'
14
37
 
15
38
  And then execute:
16
39
 
17
- $ bundle install
40
+ ```bash
41
+ bundle install
42
+ bundle exec rails g camille:install
43
+ ```
18
44
 
19
- Or install it yourself as:
45
+ ## Usage
20
46
 
21
- $ gem install camille
47
+ ### Schemas
22
48
 
23
- ## Usage
49
+ A schema defines the type of `params` and `response` for a controller action. The following commands will generate schema definition files in `config/camille/schemas`.
24
50
 
25
- TODO: Write usage instructions here
51
+ ```bash
52
+ # to generate a schema for ProductsController
53
+ bundle exec rails g camille:schema products
54
+ # to generate a schema for Api::ProductController
55
+ bundle exec rails g camille:schema api/products
56
+ ```
26
57
 
27
- ## Development
58
+ An example of schema definition:
59
+
60
+ ```ruby
61
+ using Camille::Syntax
62
+
63
+ class Camille::Schemas::Api::Products < Camille::Schema
64
+ include Camille::Types
65
+
66
+ get :data do
67
+ params(
68
+ id: Number
69
+ )
70
+ response(
71
+ name: String
72
+ )
73
+ end
74
+ end
75
+ ```
76
+
77
+ The `Api::Products` schema defines one endpoint `data` and its params and response type. This endpoint corresponds to the `data` action on `Api::ProductsController`. Inside the action, you can assume that `params[:id]` is a number, and you will need to `render json: {name: 'some string'}` in order to pass the typecheck.
78
+
79
+ When generating TypeScript request functions, the `data` endpoint will become a function having the following signature:
80
+
81
+ ```typescript
82
+ data(params: {id: number}): Promise<{name: string}>
83
+ ```
84
+
85
+ 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
+
87
+ 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
+
89
+ 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.
90
+
91
+ ### Custom types
92
+
93
+ In addition to primitive types, you can define custom types in Camille. The following commands will generate type definition files in `config/camille/types`.
94
+
95
+ ```bash
96
+ # to generate a type named Product
97
+ rails g camille:type product
98
+ # to generate a type named Nested::Product
99
+ rails g camille:type nested/product
100
+ ```
101
+
102
+ An example of custom type definition:
103
+
104
+ ```ruby
105
+ using Camille::Syntax
106
+
107
+ class Camille::Types::Product < Camille::Type
108
+ include Camille::Types
109
+
110
+ alias_of(
111
+ id: Number,
112
+ name: String
113
+ )
114
+ end
115
+ ```
116
+
117
+ Each custom type is considered a type alias in TypeScript. And `alias_of` defines what this type is aliasing. In this case, the `Product` type is an alias of an object type having fields `id` as `Number` and `name` as `String`. When generating TypeScript, it will be converted to the following:
118
+
119
+ ```typescript
120
+ type Product = {id: number, name: string}
121
+ ```
28
122
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
123
+ ### Available syntax for types
124
+
125
+ Camille supports most of the type syntax in TypeScript. Below is a list of types that you can use in type and schema definition.
126
+
127
+ ```ruby
128
+ params(
129
+ # primitive types in TypeScript
130
+ number: Number,
131
+ string: String,
132
+ boolean: Boolean,
133
+ null: Null,
134
+ undefined: Undefined,
135
+ any: Any,
136
+ # an array type is a type name followed by '[]'
137
+ array: Number[],
138
+ # an object type looks like hash
139
+ object: {
140
+ field: Number
141
+ },
142
+ # an array of objects also works
143
+ object_array: {
144
+ field: Number
145
+ }[]
146
+ # a union type is two types connected by '|'
147
+ union: Number | String,
148
+ # a tuple type is several types put inside '[]'
149
+ tuple: [Number, String, Boolean],
150
+ # a field followed by '?' is optional, the same as in TypeScript
151
+ optional?: Number,
152
+ # literal types
153
+ number_literal: 1,
154
+ string_literal: 'hello',
155
+ # a custom type we defined above
156
+ product: Product
157
+ )
158
+ ```
159
+
160
+ String literal types and probably enums are planned for the future.
161
+
162
+ ### TypeScript generation
163
+
164
+ 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.
165
+
166
+ An example from our previously defined type and schema will be:
167
+
168
+ ```typescript
169
+ import request from './request'
170
+
171
+ export type Product = {id: number, name: string}
172
+
173
+ export default {
174
+ api: {
175
+ data(params: {id: number}): Promise<{name: string}> {
176
+ return request('get', '/api/products/data', params)
177
+ }
178
+ }
179
+ }
180
+ ```
181
+
182
+ The first line of `import` is configurable as `config.ts_header` in `config/camille/configuration.rb`. You would need to implement a `request` function that performs the HTTP request.
183
+
184
+ ### Conversion between camelCase and snake_case
185
+
186
+ In TypeScript world, people usually use camelCase to name functions and variables, while in Ruby the convention is to use snake_case. Camille will automatically convert between these two when processing request.
187
+
188
+ For example,
189
+
190
+ ```ruby
191
+ get :special_data do
192
+ params(
193
+ long_id: Number
194
+ )
195
+ response(
196
+ long_name: String
197
+ )
198
+ end
199
+ ```
200
+
201
+ will have TS signature:
202
+
203
+ ```typescript
204
+ specialData(params: {longId: number}): Promise<{longName: string}>
205
+ ```
206
+
207
+ In the Rails action you still use `params[:long_id]` to access the parameter and return `long_name` in response.
208
+
209
+ ### Typechecking
210
+
211
+ If a controller action has a corresponding schema, Camille will raise an error if the returned JSON doesn't match the response type specified in the schema.
212
+
213
+ For example for
214
+ ```ruby
215
+ response(
216
+ object: {
217
+ array: Number[]
218
+ }
219
+ )
220
+ ```
221
+
222
+ if we return such a JSON in our action
223
+ ```ruby
224
+ render json: {
225
+ object: {
226
+ array: [1, 2, '3']
227
+ }
228
+ }
229
+ ```
230
+
231
+ Camille will print the following error:
232
+ ```
233
+ object:
234
+ array:
235
+ [2]: Expected number, got "3".
236
+ ```
237
+
238
+ ## Development
30
239
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
240
+ Run tests with `bundle exec rake`.
32
241
 
33
242
  ## Contributing
34
243
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/camille.
244
+ Bug reports and pull requests are welcome on GitHub at https://github.com/onyxblade/camille.
@@ -24,13 +24,18 @@ module Camille
24
24
  end
25
25
 
26
26
  def self.instance value
27
- if value.is_a? ::Hash
27
+ case
28
+ when value.is_a?(::Hash)
28
29
  Camille::Types::Object.new(value)
29
- elsif value.is_a? ::Array
30
+ when value.is_a?(::Array)
30
31
  Camille::Types::Tuple.new(value)
31
- elsif value.is_a? Camille::BasicType
32
+ when value.is_a?(Integer) || value.is_a?(Float)
33
+ Camille::Types::NumberLiteral.new(value)
34
+ when value.is_a?(::String)
35
+ Camille::Types::StringLiteral.new(value)
36
+ when value.is_a?(Camille::BasicType)
32
37
  value
33
- elsif value.is_a?(Class) && value < Camille::BasicType && value.directly_instantiable?
38
+ when value.is_a?(Class) && value < Camille::BasicType && value.directly_instantiable?
34
39
  value.new
35
40
  else
36
41
  raise InvalidTypeError.new("#{value} cannot be converted to a type instance.")
@@ -17,8 +17,9 @@ module Camille
17
17
  if value = render_options[:json]
18
18
  error = endpoint.response_type.check(value)
19
19
  if error
20
- Camille::TypeErrorPrinter.new(error).print
21
- raise TypeError.new("Type check failed for response.")
20
+ string_io = StringIO.new
21
+ Camille::TypeErrorPrinter.new(error).print(string_io)
22
+ raise TypeError.new("\nType check failed for response.\n#{string_io.string}")
22
23
  else
23
24
  if value.is_a? Hash
24
25
  value.deep_transform_keys!{|k| k.to_s.camelize(:lower)}
@@ -10,12 +10,12 @@ module Camille
10
10
  copy_file "configuration.rb", "config/camille/configuration.rb"
11
11
  end
12
12
 
13
- def copy_type_example
14
- copy_file "type_example.rb", "config/camille/types/example.rb"
13
+ def create_types_folder
14
+ copy_file ".keep", "config/camille/types/.keep"
15
15
  end
16
16
 
17
- def copy_schema_example
18
- copy_file "schema_example.rb", "config/camille/schemas/examples.rb"
17
+ def create_schemas_folder
18
+ copy_file ".keep", "config/camille/schemas/.keep"
19
19
  end
20
20
 
21
21
  end
File without changes
@@ -1,4 +1,4 @@
1
- using Camille::CoreExt
1
+ using Camille::Syntax
2
2
 
3
3
  class Camille::Schemas::<%= class_name %> < Camille::Schema
4
4
  include Camille::Types
@@ -1,9 +1,9 @@
1
- using Camille::CoreExt
1
+ using Camille::Syntax
2
2
 
3
3
  class Camille::Types::<%= class_name %> < Camille::Type
4
4
  include Camille::Types
5
5
 
6
- alias_of(
7
- # id: Number
8
- )
6
+ # alias_of(
7
+ # id: Number
8
+ # )
9
9
  end
@@ -0,0 +1,75 @@
1
+ module Camille
2
+ module Syntax
3
+ module NULL_VALUE; end
4
+
5
+ refine ::Hash do
6
+ def [] key = NULL_VALUE
7
+ if key == NULL_VALUE
8
+ Camille::Types::Object.new(self)[]
9
+ else
10
+ super
11
+ end
12
+ end
13
+
14
+ def | other
15
+ Camille::Types::Union.new(Camille::Types::Object.new(self), other)
16
+ end
17
+ end
18
+
19
+ refine ::Array do
20
+ def [] key = NULL_VALUE
21
+ if key == NULL_VALUE
22
+ Camille::Types::Tuple.new(self)[]
23
+ else
24
+ super
25
+ end
26
+ end
27
+
28
+ def | other
29
+ Camille::Types::Union.new(Camille::Types::Tuple.new(self), other)
30
+ end
31
+ end
32
+
33
+ refine Integer do
34
+ def [] key = NULL_VALUE
35
+ if key == NULL_VALUE
36
+ Camille::Types::NumberLiteral.new(self)[]
37
+ else
38
+ super
39
+ end
40
+ end
41
+
42
+ def | other
43
+ Camille::Types::Union.new(Camille::Types::NumberLiteral.new(self), other)
44
+ end
45
+ end
46
+
47
+ refine Float do
48
+ def [] key = NULL_VALUE
49
+ if key == NULL_VALUE
50
+ Camille::Types::NumberLiteral.new(self)[]
51
+ else
52
+ super
53
+ end
54
+ end
55
+
56
+ def | other
57
+ Camille::Types::Union.new(Camille::Types::NumberLiteral.new(self), other)
58
+ end
59
+ end
60
+
61
+ refine ::String do
62
+ def [] key = NULL_VALUE
63
+ if key == NULL_VALUE
64
+ Camille::Types::StringLiteral.new(self)[]
65
+ else
66
+ super
67
+ end
68
+ end
69
+
70
+ def | other
71
+ Camille::Types::Union.new(Camille::Types::StringLiteral.new(self), other)
72
+ end
73
+ end
74
+ end
75
+ end
@@ -18,9 +18,9 @@ module Camille
18
18
  def print_composite_error io, error, indentation
19
19
  error.components.each do |key, error|
20
20
  if error.basic?
21
- io.puts ' ' * indentation + "#{key}: #{error.message}"
21
+ io.puts "\u00A0" * indentation + "#{key}: #{error.message}"
22
22
  else
23
- io.puts ' ' * indentation + "#{key}:"
23
+ io.puts "\u00A0" * indentation + "#{key}:"
24
24
  print_composite_error io, error, indentation + 2
25
25
  end
26
26
  end
@@ -4,7 +4,7 @@ module Camille
4
4
 
5
5
  def check value
6
6
  unless value.is_a?(Integer) || value.is_a?(Float)
7
- Camille::TypeError.new("Expected number, got #{value.inspect}.")
7
+ Camille::TypeError.new("Expected an integer or a float, got #{value.inspect}.")
8
8
  end
9
9
  end
10
10
 
@@ -0,0 +1,25 @@
1
+ module Camille
2
+ module Types
3
+ class NumberLiteral < Camille::BasicType
4
+ class ArgumentError < ::ArgumentError; end
5
+
6
+ def initialize value
7
+ if value.is_a?(Integer) || value.is_a?(Float)
8
+ @value = value
9
+ else
10
+ raise ArgumentError.new("Expecting an integer or a float, got #{value.inspect}")
11
+ end
12
+ end
13
+
14
+ def check value
15
+ unless value == @value
16
+ Camille::TypeError.new("Expected number literal #{@value.inspect}, got #{value.inspect}.")
17
+ end
18
+ end
19
+
20
+ def literal
21
+ @value.to_s
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ module Camille
2
+ module Types
3
+ class StringLiteral < Camille::BasicType
4
+ class ArgumentError < ::ArgumentError; end
5
+
6
+ def initialize value
7
+ if value.is_a?(::String)
8
+ @value = value
9
+ else
10
+ raise ArgumentError.new("Expecting a string, got #{value.inspect}")
11
+ end
12
+ end
13
+
14
+ def check value
15
+ unless value == @value
16
+ Camille::TypeError.new("Expected string literal #{@value.inspect}, got #{value.inspect}.")
17
+ end
18
+ end
19
+
20
+ def literal
21
+ @value.inspect
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Camille
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/camille.rb CHANGED
@@ -15,10 +15,12 @@ require_relative "camille/types/undefined"
15
15
  require_relative "camille/types/union"
16
16
  require_relative "camille/types/tuple"
17
17
  require_relative "camille/types/any"
18
+ require_relative "camille/types/number_literal"
19
+ require_relative "camille/types/string_literal"
18
20
  require_relative "camille/type"
19
21
  require_relative "camille/type_error"
20
22
  require_relative "camille/type_error_printer"
21
- require_relative "camille/core_ext"
23
+ require_relative "camille/syntax"
22
24
  require_relative "camille/endpoint"
23
25
  require_relative "camille/schema"
24
26
  require_relative "camille/schemas"
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.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - 辻彩
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-03-16 00:00:00.000000000 Z
11
+ date: 2023-03-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -49,14 +49,12 @@ files:
49
49
  - lib/camille/code_generator.rb
50
50
  - lib/camille/configuration.rb
51
51
  - lib/camille/controller_extension.rb
52
- - lib/camille/core_ext.rb
53
52
  - lib/camille/endpoint.rb
54
53
  - lib/camille/generators/install_generator.rb
55
54
  - lib/camille/generators/schema_generator.rb
55
+ - lib/camille/generators/templates/.keep
56
56
  - lib/camille/generators/templates/configuration.rb
57
- - lib/camille/generators/templates/schema_example.rb
58
57
  - lib/camille/generators/templates/schema_template.erb
59
- - lib/camille/generators/templates/type_example.rb
60
58
  - lib/camille/generators/templates/type_template.erb
61
59
  - lib/camille/generators/type_generator.rb
62
60
  - lib/camille/line.rb
@@ -65,6 +63,7 @@ files:
65
63
  - lib/camille/railtie.rb
66
64
  - lib/camille/schema.rb
67
65
  - lib/camille/schemas.rb
66
+ - lib/camille/syntax.rb
68
67
  - lib/camille/type.rb
69
68
  - lib/camille/type_error.rb
70
69
  - lib/camille/type_error_printer.rb
@@ -74,8 +73,10 @@ files:
74
73
  - lib/camille/types/boolean.rb
75
74
  - lib/camille/types/null.rb
76
75
  - lib/camille/types/number.rb
76
+ - lib/camille/types/number_literal.rb
77
77
  - lib/camille/types/object.rb
78
78
  - lib/camille/types/string.rb
79
+ - lib/camille/types/string_literal.rb
79
80
  - lib/camille/types/tuple.rb
80
81
  - lib/camille/types/undefined.rb
81
82
  - lib/camille/types/union.rb
@@ -1,33 +0,0 @@
1
- module Camille
2
- module CoreExt
3
- module NULL_VALUE; end
4
-
5
- refine ::Hash do
6
- def [] key = NULL_VALUE
7
- if key == NULL_VALUE
8
- Camille::Types::Object.new(self)[]
9
- else
10
- super
11
- end
12
- end
13
-
14
- def | other
15
- Camille::Types::Union.new(Camille::Types::Object.new(self), other)
16
- end
17
- end
18
-
19
- refine ::Array do
20
- def [] key = NULL_VALUE
21
- if key == NULL_VALUE
22
- Camille::Types::Tuple.new(self)[]
23
- else
24
- super
25
- end
26
- end
27
-
28
- def | other
29
- Camille::Types::Union.new(Camille::Types::Tuple.new(self), other)
30
- end
31
- end
32
- end
33
- end
@@ -1,35 +0,0 @@
1
- using Camille::CoreExt
2
-
3
- class Camille::Schemas::Examples < Camille::Schema
4
- include Camille::Types
5
-
6
- get :find do
7
- params(
8
- id: Number
9
- )
10
-
11
- response(
12
- example?: Example
13
- )
14
- end
15
-
16
- get :list do
17
- response(
18
- examples: Example[]
19
- )
20
- end
21
-
22
- post :update do
23
- params(
24
- id: Number,
25
- example: Example
26
- )
27
-
28
- response(
29
- success: Boolean,
30
- errors: {
31
- message: String
32
- }[]
33
- )
34
- end
35
- end
@@ -1,10 +0,0 @@
1
- using Camille::CoreExt
2
-
3
- class Camille::Types::Example < Camille::Type
4
- include Camille::Types
5
-
6
- alias_of(
7
- id: Number,
8
- name: String,
9
- )
10
- end