camille 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +3 -0
- data/Gemfile.lock +155 -0
- data/Rakefile +16 -1
- data/lib/camille/basic_type.rb +40 -0
- data/lib/camille/code_generator.rb +26 -0
- data/lib/camille/configuration.rb +22 -0
- data/lib/camille/controller_extension.rb +49 -0
- data/lib/camille/core_ext.rb +33 -0
- data/lib/camille/endpoint.rb +50 -0
- data/lib/camille/line.rb +43 -0
- data/lib/camille/loader.rb +123 -0
- data/lib/camille/railtie.rb +24 -0
- data/lib/camille/schema.rb +60 -0
- data/lib/camille/schemas.rb +47 -0
- data/lib/camille/type.rb +45 -0
- data/lib/camille/type_error.rb +21 -0
- data/lib/camille/type_error_printer.rb +29 -0
- data/lib/camille/types/any.rb +12 -0
- data/lib/camille/types/array.rb +26 -0
- data/lib/camille/types/boolean.rb +15 -0
- data/lib/camille/types/null.rb +15 -0
- data/lib/camille/types/number.rb +16 -0
- data/lib/camille/types/object.rb +46 -0
- data/lib/camille/types/string.rb +15 -0
- data/lib/camille/types/tuple.rb +29 -0
- data/lib/camille/types/undefined.rb +15 -0
- data/lib/camille/types/union.rb +29 -0
- data/lib/camille/types.rb +10 -0
- data/lib/camille/version.rb +1 -1
- data/lib/camille.rb +35 -1
- metadata +65 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: efa884b2cb790a85ad8ae7f2e5462fd2a94d507aad4437c3063af81b333dd2c4
|
4
|
+
data.tar.gz: e1a1c1a68397a31521704a2fff6044a3f78997418acf5cd4fd5994c9eb5748c3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2c8c279a7e420762d3586a191b08f4e916fe2827d30a7d76702d4dcd92e466efe0cca29b46a8688f198eb6c49193c2698ef7fe87f7fc85e292489e8bf39a27b0
|
7
|
+
data.tar.gz: c4ba05522f14dc3c80f9dabfddacfe562f32475d278b49ddb49104bb76f8cb72d8032547e0cc25d5f8655c87d6ae0ddecb4e5db7cab4058d27b40236b4fba06a
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -2,12 +2,149 @@ PATH
|
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
4
|
camille (0.1.0)
|
5
|
+
listen (~> 3.3)
|
6
|
+
rails (>= 6.1, < 8)
|
5
7
|
|
6
8
|
GEM
|
7
9
|
remote: https://rubygems.org/
|
8
10
|
specs:
|
11
|
+
actioncable (7.0.4.2)
|
12
|
+
actionpack (= 7.0.4.2)
|
13
|
+
activesupport (= 7.0.4.2)
|
14
|
+
nio4r (~> 2.0)
|
15
|
+
websocket-driver (>= 0.6.1)
|
16
|
+
actionmailbox (7.0.4.2)
|
17
|
+
actionpack (= 7.0.4.2)
|
18
|
+
activejob (= 7.0.4.2)
|
19
|
+
activerecord (= 7.0.4.2)
|
20
|
+
activestorage (= 7.0.4.2)
|
21
|
+
activesupport (= 7.0.4.2)
|
22
|
+
mail (>= 2.7.1)
|
23
|
+
net-imap
|
24
|
+
net-pop
|
25
|
+
net-smtp
|
26
|
+
actionmailer (7.0.4.2)
|
27
|
+
actionpack (= 7.0.4.2)
|
28
|
+
actionview (= 7.0.4.2)
|
29
|
+
activejob (= 7.0.4.2)
|
30
|
+
activesupport (= 7.0.4.2)
|
31
|
+
mail (~> 2.5, >= 2.5.4)
|
32
|
+
net-imap
|
33
|
+
net-pop
|
34
|
+
net-smtp
|
35
|
+
rails-dom-testing (~> 2.0)
|
36
|
+
actionpack (7.0.4.2)
|
37
|
+
actionview (= 7.0.4.2)
|
38
|
+
activesupport (= 7.0.4.2)
|
39
|
+
rack (~> 2.0, >= 2.2.0)
|
40
|
+
rack-test (>= 0.6.3)
|
41
|
+
rails-dom-testing (~> 2.0)
|
42
|
+
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
43
|
+
actiontext (7.0.4.2)
|
44
|
+
actionpack (= 7.0.4.2)
|
45
|
+
activerecord (= 7.0.4.2)
|
46
|
+
activestorage (= 7.0.4.2)
|
47
|
+
activesupport (= 7.0.4.2)
|
48
|
+
globalid (>= 0.6.0)
|
49
|
+
nokogiri (>= 1.8.5)
|
50
|
+
actionview (7.0.4.2)
|
51
|
+
activesupport (= 7.0.4.2)
|
52
|
+
builder (~> 3.1)
|
53
|
+
erubi (~> 1.4)
|
54
|
+
rails-dom-testing (~> 2.0)
|
55
|
+
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
56
|
+
activejob (7.0.4.2)
|
57
|
+
activesupport (= 7.0.4.2)
|
58
|
+
globalid (>= 0.3.6)
|
59
|
+
activemodel (7.0.4.2)
|
60
|
+
activesupport (= 7.0.4.2)
|
61
|
+
activerecord (7.0.4.2)
|
62
|
+
activemodel (= 7.0.4.2)
|
63
|
+
activesupport (= 7.0.4.2)
|
64
|
+
activestorage (7.0.4.2)
|
65
|
+
actionpack (= 7.0.4.2)
|
66
|
+
activejob (= 7.0.4.2)
|
67
|
+
activerecord (= 7.0.4.2)
|
68
|
+
activesupport (= 7.0.4.2)
|
69
|
+
marcel (~> 1.0)
|
70
|
+
mini_mime (>= 1.1.0)
|
71
|
+
activesupport (7.0.4.2)
|
72
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
73
|
+
i18n (>= 1.6, < 2)
|
74
|
+
minitest (>= 5.1)
|
75
|
+
tzinfo (~> 2.0)
|
76
|
+
builder (3.2.4)
|
77
|
+
concurrent-ruby (1.2.2)
|
78
|
+
crass (1.0.6)
|
79
|
+
date (3.3.3)
|
9
80
|
diff-lcs (1.5.0)
|
81
|
+
erubi (1.12.0)
|
82
|
+
ffi (1.15.5)
|
83
|
+
globalid (1.1.0)
|
84
|
+
activesupport (>= 5.0)
|
85
|
+
i18n (1.12.0)
|
86
|
+
concurrent-ruby (~> 1.0)
|
87
|
+
listen (3.8.0)
|
88
|
+
rb-fsevent (~> 0.10, >= 0.10.3)
|
89
|
+
rb-inotify (~> 0.9, >= 0.9.10)
|
90
|
+
loofah (2.19.1)
|
91
|
+
crass (~> 1.0.2)
|
92
|
+
nokogiri (>= 1.5.9)
|
93
|
+
mail (2.8.1)
|
94
|
+
mini_mime (>= 0.1.1)
|
95
|
+
net-imap
|
96
|
+
net-pop
|
97
|
+
net-smtp
|
98
|
+
marcel (1.0.2)
|
99
|
+
method_source (1.0.0)
|
100
|
+
mini_mime (1.1.2)
|
101
|
+
minitest (5.17.0)
|
102
|
+
net-imap (0.3.4)
|
103
|
+
date
|
104
|
+
net-protocol
|
105
|
+
net-pop (0.1.2)
|
106
|
+
net-protocol
|
107
|
+
net-protocol (0.2.1)
|
108
|
+
timeout
|
109
|
+
net-smtp (0.3.3)
|
110
|
+
net-protocol
|
111
|
+
nio4r (2.5.8)
|
112
|
+
nokogiri (1.14.2-x86_64-linux)
|
113
|
+
racc (~> 1.4)
|
114
|
+
racc (1.6.2)
|
115
|
+
rack (2.2.6.3)
|
116
|
+
rack-test (2.0.2)
|
117
|
+
rack (>= 1.3)
|
118
|
+
rails (7.0.4.2)
|
119
|
+
actioncable (= 7.0.4.2)
|
120
|
+
actionmailbox (= 7.0.4.2)
|
121
|
+
actionmailer (= 7.0.4.2)
|
122
|
+
actionpack (= 7.0.4.2)
|
123
|
+
actiontext (= 7.0.4.2)
|
124
|
+
actionview (= 7.0.4.2)
|
125
|
+
activejob (= 7.0.4.2)
|
126
|
+
activemodel (= 7.0.4.2)
|
127
|
+
activerecord (= 7.0.4.2)
|
128
|
+
activestorage (= 7.0.4.2)
|
129
|
+
activesupport (= 7.0.4.2)
|
130
|
+
bundler (>= 1.15.0)
|
131
|
+
railties (= 7.0.4.2)
|
132
|
+
rails-dom-testing (2.0.3)
|
133
|
+
activesupport (>= 4.2.0)
|
134
|
+
nokogiri (>= 1.6)
|
135
|
+
rails-html-sanitizer (1.5.0)
|
136
|
+
loofah (~> 2.19, >= 2.19.1)
|
137
|
+
railties (7.0.4.2)
|
138
|
+
actionpack (= 7.0.4.2)
|
139
|
+
activesupport (= 7.0.4.2)
|
140
|
+
method_source
|
141
|
+
rake (>= 12.2)
|
142
|
+
thor (~> 1.0)
|
143
|
+
zeitwerk (~> 2.5)
|
10
144
|
rake (13.0.6)
|
145
|
+
rb-fsevent (0.11.2)
|
146
|
+
rb-inotify (0.10.1)
|
147
|
+
ffi (~> 1.0)
|
11
148
|
rspec (3.12.0)
|
12
149
|
rspec-core (~> 3.12.0)
|
13
150
|
rspec-expectations (~> 3.12.0)
|
@@ -20,15 +157,33 @@ GEM
|
|
20
157
|
rspec-mocks (3.12.3)
|
21
158
|
diff-lcs (>= 1.2.0, < 2.0)
|
22
159
|
rspec-support (~> 3.12.0)
|
160
|
+
rspec-rails (6.0.1)
|
161
|
+
actionpack (>= 6.1)
|
162
|
+
activesupport (>= 6.1)
|
163
|
+
railties (>= 6.1)
|
164
|
+
rspec-core (~> 3.11)
|
165
|
+
rspec-expectations (~> 3.11)
|
166
|
+
rspec-mocks (~> 3.11)
|
167
|
+
rspec-support (~> 3.11)
|
23
168
|
rspec-support (3.12.0)
|
169
|
+
thor (1.2.1)
|
170
|
+
timeout (0.3.2)
|
171
|
+
tzinfo (2.0.6)
|
172
|
+
concurrent-ruby (~> 1.0)
|
173
|
+
websocket-driver (0.7.5)
|
174
|
+
websocket-extensions (>= 0.1.0)
|
175
|
+
websocket-extensions (0.1.5)
|
176
|
+
zeitwerk (2.6.7)
|
24
177
|
|
25
178
|
PLATFORMS
|
26
179
|
x86_64-linux
|
27
180
|
|
28
181
|
DEPENDENCIES
|
29
182
|
camille!
|
183
|
+
rails
|
30
184
|
rake (~> 13.0)
|
31
185
|
rspec (~> 3.0)
|
186
|
+
rspec-rails
|
32
187
|
|
33
188
|
BUNDLED WITH
|
34
189
|
2.2.33
|
data/Rakefile
CHANGED
@@ -3,6 +3,21 @@
|
|
3
3
|
require "bundler/gem_tasks"
|
4
4
|
require "rspec/core/rake_task"
|
5
5
|
|
6
|
-
RSpec::Core::RakeTask.new(:
|
6
|
+
spec_gem_task = RSpec::Core::RakeTask.new(:spec_gem)
|
7
|
+
spec_gem_task.exclude_pattern = 'spec/camille/rails/*_spec.rb'
|
8
|
+
|
9
|
+
spec_rails_task = RSpec::Core::RakeTask.new(:spec_rails)
|
10
|
+
spec_rails_task.pattern = 'spec/camille/rails/*_spec.rb'
|
7
11
|
|
8
12
|
task default: :spec
|
13
|
+
|
14
|
+
task :spec do
|
15
|
+
Rake::Task[:spec_gem].invoke
|
16
|
+
|
17
|
+
ENV['RAILS_ENV'] = 'test'
|
18
|
+
Rake::Task[:spec_rails].invoke
|
19
|
+
|
20
|
+
ENV['RAILS_ENV'] = 'development'
|
21
|
+
Rake::Task[:spec_rails].reenable
|
22
|
+
Rake::Task[:spec_rails].invoke
|
23
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Camille
|
2
|
+
# This class specifies the methods available for all types includeing built-in and custom ones.
|
3
|
+
class BasicType
|
4
|
+
class InvalidTypeError < ::ArgumentError; end
|
5
|
+
|
6
|
+
def | other
|
7
|
+
Camille::Types::Union.new(self, other)
|
8
|
+
end
|
9
|
+
|
10
|
+
def []
|
11
|
+
Camille::Types::Array.new(self)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.| other
|
15
|
+
Camille::Type.instance(self) | other
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.[]
|
19
|
+
Camille::Type.instance(self)[]
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.directly_instantiable?
|
23
|
+
instance_method(:initialize).arity == 0
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.instance value
|
27
|
+
if value.is_a? ::Hash
|
28
|
+
Camille::Types::Object.new(value)
|
29
|
+
elsif value.is_a? ::Array
|
30
|
+
Camille::Types::Tuple.new(value)
|
31
|
+
elsif value.is_a? Camille::BasicType
|
32
|
+
value
|
33
|
+
elsif value.is_a?(Class) && value < Camille::BasicType && value.directly_instantiable?
|
34
|
+
value.new
|
35
|
+
else
|
36
|
+
raise InvalidTypeError.new("#{value} cannot be converted to a type instance.")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
|
3
|
+
module Camille
|
4
|
+
module CodeGenerator
|
5
|
+
def self.generate_ts
|
6
|
+
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
|
17
|
+
io.string
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.generate_ts_file
|
21
|
+
if Camille::Configuration.ts_location
|
22
|
+
File.open(Camille::Configuration.ts_location, 'w'){|f| f.write generate_ts}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
|
2
|
+
module Camille
|
3
|
+
class Configuration
|
4
|
+
class << self
|
5
|
+
def ts_header= string
|
6
|
+
@ts_header = string
|
7
|
+
end
|
8
|
+
|
9
|
+
def ts_header
|
10
|
+
@ts_header
|
11
|
+
end
|
12
|
+
|
13
|
+
def ts_location= string
|
14
|
+
@ts_location = string
|
15
|
+
end
|
16
|
+
|
17
|
+
def ts_location
|
18
|
+
@ts_location
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Camille
|
2
|
+
module ControllerExtension
|
3
|
+
class TypeError < ::StandardError; end
|
4
|
+
class ArgumentError < ::ArgumentError; end
|
5
|
+
|
6
|
+
def camille_schema
|
7
|
+
@camille_schema ||= Camille::Loader.controller_name_to_schema_map[self.class.name]
|
8
|
+
end
|
9
|
+
|
10
|
+
def camille_endpoint
|
11
|
+
camille_schema && camille_schema.endpoints[action_name.to_sym]
|
12
|
+
end
|
13
|
+
|
14
|
+
def render *args
|
15
|
+
if endpoint = camille_endpoint
|
16
|
+
render_options = args.last
|
17
|
+
if value = render_options[:json]
|
18
|
+
error = endpoint.response_type.check(value)
|
19
|
+
if error
|
20
|
+
Camille::TypeErrorPrinter.new(error).print
|
21
|
+
raise TypeError.new("Type check failed for response.")
|
22
|
+
else
|
23
|
+
if value.is_a? Hash
|
24
|
+
value.deep_transform_keys!{|k| k.to_s.camelize(:lower)}
|
25
|
+
end
|
26
|
+
super(json: value)
|
27
|
+
end
|
28
|
+
else
|
29
|
+
raise ArgumentError.new("Expected key :json for `render` call.")
|
30
|
+
end
|
31
|
+
else
|
32
|
+
super
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def process_action(*)
|
37
|
+
if exception = Camille::Loader.exception
|
38
|
+
raise exception
|
39
|
+
end
|
40
|
+
if endpoint = camille_endpoint
|
41
|
+
params.deep_transform_keys!{|key| key.to_s.underscore}
|
42
|
+
super
|
43
|
+
else
|
44
|
+
super
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,33 @@
|
|
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
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Camille
|
2
|
+
class Endpoint
|
3
|
+
class ArgumentError < ::ArgumentError; end
|
4
|
+
class UnknownResponseError < ::ArgumentError; end
|
5
|
+
|
6
|
+
attr_reader :params_type, :response_type, :verb, :name, :schema
|
7
|
+
|
8
|
+
def initialize schema, verb, name
|
9
|
+
@verb = verb
|
10
|
+
@name = name
|
11
|
+
@schema = schema
|
12
|
+
end
|
13
|
+
|
14
|
+
def signature
|
15
|
+
unless @response_type
|
16
|
+
raise UnknownResponseError.new("Endpoint lacking a `response` definition.")
|
17
|
+
end
|
18
|
+
if @params_type
|
19
|
+
"#{ActiveSupport::Inflector.camelize @name, false}(params: #{@params_type.literal}): Promise<#{@response_type.literal}>"
|
20
|
+
else
|
21
|
+
"#{ActiveSupport::Inflector.camelize @name, false}(): Promise<#{@response_type.literal}>"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def path
|
26
|
+
"#{@schema.path}/#{name}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def function
|
30
|
+
if @params_type
|
31
|
+
"#{signature}{ return request('#{@verb}', '#{path}', params) }"
|
32
|
+
else
|
33
|
+
"#{signature}{ return request('#{@verb}', '#{path}', {}) }"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
def params type
|
39
|
+
if type.is_a?(::Hash)
|
40
|
+
@params_type = Camille::Type.instance type
|
41
|
+
else
|
42
|
+
raise ArgumentError.new("`params` requires a hash as input, got #{type.inspect}.")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def response type
|
47
|
+
@response_type = Camille::Type.instance type
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/camille/line.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
module Camille
|
2
|
+
# lines with indentation
|
3
|
+
class Line
|
4
|
+
attr_reader :string, :indent
|
5
|
+
|
6
|
+
def initialize string, indent = 0
|
7
|
+
@string = string
|
8
|
+
@indent = indent
|
9
|
+
end
|
10
|
+
|
11
|
+
def do_indent
|
12
|
+
@indent += 2
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
def == other
|
17
|
+
@string == other.string && @indent == other.indent
|
18
|
+
end
|
19
|
+
|
20
|
+
def prepend str
|
21
|
+
@string = str + @string
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def append str
|
26
|
+
@string = @string + str
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_s
|
31
|
+
' ' * @indent + @string
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.join lines, delimiter
|
35
|
+
size = lines.size
|
36
|
+
lines.each_with_index do |line, index|
|
37
|
+
if index < size - 1
|
38
|
+
line.append(delimiter)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'monitor'
|
3
|
+
|
4
|
+
module Camille
|
5
|
+
module Loader
|
6
|
+
extend MonitorMixin
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def setup_zeitwerk_loader app
|
10
|
+
synchronize do
|
11
|
+
loader = Zeitwerk::Loader.new
|
12
|
+
loader.enable_reloading if !app.config.cache_classes
|
13
|
+
loader.push_dir "#{app.root}/config/camille/types", namespace: Camille::Types
|
14
|
+
loader.push_dir "#{app.root}/config/camille/schemas", namespace: Camille::Schemas
|
15
|
+
|
16
|
+
loader.setup
|
17
|
+
@zeitwerk_loader = loader
|
18
|
+
@configuration_location = "#{app.root}/config/camille/configuration.rb"
|
19
|
+
|
20
|
+
eager_load
|
21
|
+
construct_controller_name_to_schema_map
|
22
|
+
Camille::CodeGenerator.generate_ts_file
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def eager_load
|
27
|
+
@eager_loading = true
|
28
|
+
load @configuration_location
|
29
|
+
@zeitwerk_loader.eager_load
|
30
|
+
@eager_loading = false
|
31
|
+
end
|
32
|
+
|
33
|
+
def eager_loading?
|
34
|
+
@eager_loading
|
35
|
+
end
|
36
|
+
|
37
|
+
def reload_types_and_schemas
|
38
|
+
synchronize do
|
39
|
+
Camille::Loader.loaded_types.clear
|
40
|
+
Camille::Loader.loaded_schemas.clear
|
41
|
+
@zeitwerk_loader.reload
|
42
|
+
eager_load
|
43
|
+
construct_controller_name_to_schema_map
|
44
|
+
Camille::CodeGenerator.generate_ts_file
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def register_routes router_context
|
49
|
+
if exception = Camille::Loader.exception
|
50
|
+
raise exception
|
51
|
+
end
|
52
|
+
Camille::Loader.loaded_schemas.each do |schema|
|
53
|
+
schema.endpoints.each do |name, endpoint|
|
54
|
+
router_context.public_send(endpoint.verb, endpoint.path, controller: schema.path.gsub(/^\//, ''), action: endpoint.name, as: false)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def loaded_types
|
60
|
+
synchronize do
|
61
|
+
@loaded_types ||= []
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def loaded_schemas
|
66
|
+
synchronize do
|
67
|
+
@loaded_schemas ||= []
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def controller_name_to_schema_map
|
72
|
+
synchronize do
|
73
|
+
@controller_name_to_schema_map ||= {}
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def construct_controller_name_to_schema_map
|
78
|
+
synchronize do
|
79
|
+
controller_name_to_schema_map.clear
|
80
|
+
loaded_schemas.each do |schema|
|
81
|
+
controller_class_name = "#{schema.klass_name}Controller"
|
82
|
+
controller_name_to_schema_map[controller_class_name] = schema
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def reload
|
88
|
+
synchronize do
|
89
|
+
begin
|
90
|
+
@exception = nil
|
91
|
+
reload_types_and_schemas
|
92
|
+
Rails.application.reload_routes!
|
93
|
+
|
94
|
+
# just for spec
|
95
|
+
@last_reload = Time.now
|
96
|
+
rescue Exception => e
|
97
|
+
Rails.logger.error e
|
98
|
+
@exception = e
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def exception
|
104
|
+
synchronize do
|
105
|
+
@exception
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def setup_listen app
|
110
|
+
if !app.config.cache_classes
|
111
|
+
require 'listen'
|
112
|
+
listener = Listen.to("#{app.root}/config/camille") do |changed|
|
113
|
+
#puts "Change detected. Camille reloading..."
|
114
|
+
Camille::Loader.reload
|
115
|
+
end
|
116
|
+
listener.start
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require "rails"
|
2
|
+
|
3
|
+
module Camille
|
4
|
+
class Railtie < ::Rails::Railtie
|
5
|
+
|
6
|
+
initializer "camille.configure_rails" do |app|
|
7
|
+
ActionController::API.include(Camille::ControllerExtension)
|
8
|
+
ActionController::Base.include(Camille::ControllerExtension)
|
9
|
+
|
10
|
+
Camille::Loader.setup_zeitwerk_loader(app)
|
11
|
+
Camille::Loader.setup_listen(app)
|
12
|
+
|
13
|
+
app.routes.prepend do
|
14
|
+
Camille::Loader.register_routes(self)
|
15
|
+
end
|
16
|
+
|
17
|
+
app.reloader.to_run do
|
18
|
+
require_unload_lock!
|
19
|
+
Camille::Loader.reload
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Camille
|
2
|
+
class Schema
|
3
|
+
class AlreadyDefinedError < ::ArgumentError; end
|
4
|
+
|
5
|
+
include Camille::Types
|
6
|
+
|
7
|
+
def self.endpoints
|
8
|
+
@endpoints ||= {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.const_missing name
|
12
|
+
if Camille::Loader.eager_loading?
|
13
|
+
Camille::Types.const_get(name)
|
14
|
+
else
|
15
|
+
super
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.path
|
20
|
+
"/#{ActiveSupport::Inflector.underscore klass_name}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.klass_name
|
24
|
+
self.name.gsub(/^Camille::Schemas::/, '')
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.literal_lines
|
28
|
+
[
|
29
|
+
Camille::Line.new('{'),
|
30
|
+
*endpoints.map do |k, e|
|
31
|
+
Camille::Line.new("#{e.function},")
|
32
|
+
end.map(&:do_indent),
|
33
|
+
Camille::Line.new('}')
|
34
|
+
]
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.inherited klass
|
38
|
+
Camille::Loader.loaded_schemas << klass
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
def self.define_endpoint verb, name, &block
|
43
|
+
if endpoints[name]
|
44
|
+
raise AlreadyDefinedError.new("Endpoint `#{name}` has already been defined.")
|
45
|
+
end
|
46
|
+
endpoint = Camille::Endpoint.new self, verb, name
|
47
|
+
endpoint.instance_exec &block
|
48
|
+
|
49
|
+
endpoints[name] = endpoint
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.get name, &block
|
53
|
+
define_endpoint :get, name, &block
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.post name, &block
|
57
|
+
define_endpoint :post, name, &block
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Camille
|
2
|
+
module Schemas
|
3
|
+
def self.literal_lines
|
4
|
+
[
|
5
|
+
Camille::Line.new('{'),
|
6
|
+
tree_literal_lines(namespace_tree).map(&:do_indent),
|
7
|
+
Camille::Line.new('}')
|
8
|
+
]
|
9
|
+
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("#{ActiveSupport::Inflector.camelize(key, false)}: {"),
|
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("#{ActiveSupport::Inflector.camelize(key, false)}: ")
|
41
|
+
lines.last.append(',')
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end.flatten
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/camille/type.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
module Camille
|
2
|
+
# This class represents all custom types defined by the user.
|
3
|
+
class Type < BasicType
|
4
|
+
class NotImplementedError < ::NotImplementedError; end
|
5
|
+
include Camille::Types
|
6
|
+
|
7
|
+
attr_reader :underlying
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
raise NotImplementedError.new("Missing `alias_of` definition for type.")
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.alias_of type
|
14
|
+
underlying = Camille::Type.instance(type)
|
15
|
+
|
16
|
+
define_method(:initialize) do
|
17
|
+
@underlying = underlying
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def check value
|
22
|
+
@underlying.check value
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.klass_name
|
26
|
+
name.gsub(/^Camille::Types::/, '')
|
27
|
+
end
|
28
|
+
|
29
|
+
def literal
|
30
|
+
self.class.klass_name.gsub(/::/, '_')
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.const_missing name
|
34
|
+
if Camille::Loader.eager_loading?
|
35
|
+
Camille::Types.const_get(name)
|
36
|
+
else
|
37
|
+
super
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.inherited klass
|
42
|
+
Camille::Loader.loaded_types << klass
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Camille
|
2
|
+
class TypeError
|
3
|
+
class ArgumentError < ::ArgumentError; end
|
4
|
+
|
5
|
+
attr_reader :message, :components
|
6
|
+
|
7
|
+
def initialize message = nil, **components
|
8
|
+
if message.is_a? String
|
9
|
+
@message = message
|
10
|
+
elsif !components.empty?
|
11
|
+
@components = components
|
12
|
+
else
|
13
|
+
raise ArgumentError.new("Expecting one string or one hash.")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def basic?
|
18
|
+
!!@message
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Camille
|
2
|
+
class TypeErrorPrinter
|
3
|
+
INDENTATION = 2
|
4
|
+
|
5
|
+
def initialize error
|
6
|
+
@error = error
|
7
|
+
end
|
8
|
+
|
9
|
+
def print io = STDOUT
|
10
|
+
if @error.basic?
|
11
|
+
io.puts @error.message
|
12
|
+
else
|
13
|
+
print_composite_error io, @error, 0
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
def print_composite_error io, error, indentation
|
19
|
+
error.components.each do |key, error|
|
20
|
+
if error.basic?
|
21
|
+
io.puts ' ' * indentation + "#{key}: #{error.message}"
|
22
|
+
else
|
23
|
+
io.puts ' ' * indentation + "#{key}:"
|
24
|
+
print_composite_error io, error, indentation + 2
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Camille
|
2
|
+
module Types
|
3
|
+
class Array < Camille::BasicType
|
4
|
+
attr_reader :content
|
5
|
+
|
6
|
+
def initialize content
|
7
|
+
@content = Camille::Type.instance content
|
8
|
+
end
|
9
|
+
|
10
|
+
def check value
|
11
|
+
if value.is_a? ::Array
|
12
|
+
errors = value.map.with_index{|e, i| ["array[#{i}]", @content.check(e)]}.select{|x| x[1]}
|
13
|
+
unless errors.empty?
|
14
|
+
Camille::TypeError.new(**errors.to_h)
|
15
|
+
end
|
16
|
+
else
|
17
|
+
Camille::TypeError.new("Expected array, got #{value.inspect}.")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def literal
|
22
|
+
"#{@content.literal}[]"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Camille
|
2
|
+
module Types
|
3
|
+
class Boolean < Camille::BasicType
|
4
|
+
def check value
|
5
|
+
unless value == false || value == true
|
6
|
+
Camille::TypeError.new("Expected boolean, got #{value.inspect}.")
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def literal
|
11
|
+
"boolean"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Camille
|
2
|
+
module Types
|
3
|
+
class Number < Camille::BasicType
|
4
|
+
|
5
|
+
def check value
|
6
|
+
unless value.is_a?(Integer) || value.is_a?(Float)
|
7
|
+
Camille::TypeError.new("Expected number, got #{value.inspect}.")
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def literal
|
12
|
+
"number"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Camille
|
2
|
+
module Types
|
3
|
+
class Object < Camille::BasicType
|
4
|
+
attr_reader :fields
|
5
|
+
|
6
|
+
def initialize fields
|
7
|
+
@fields = normalize_fields fields
|
8
|
+
end
|
9
|
+
|
10
|
+
def check value
|
11
|
+
if value.is_a? Hash
|
12
|
+
errors = @fields.map do |key, type|
|
13
|
+
[key.to_s, type.check(value[key])]
|
14
|
+
end.select{|x| x[1]}
|
15
|
+
|
16
|
+
unless errors.empty?
|
17
|
+
Camille::TypeError.new(**errors.to_h)
|
18
|
+
end
|
19
|
+
else
|
20
|
+
Camille::TypeError.new("Expected hash, got #{value.inspect}.")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def literal
|
25
|
+
"{#{@fields.map{|k,v| "#{ActiveSupport::Inflector.camelize k.to_s, false}: #{v.literal}"}.join(', ')}}"
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
def normalize_fields fields
|
30
|
+
fields.map do |key, value|
|
31
|
+
type = Camille::Type.instance(value)
|
32
|
+
if key.end_with?('?')
|
33
|
+
new_key = remove_question_mark(key)
|
34
|
+
[new_key, type | Camille::Types::Undefined.new]
|
35
|
+
else
|
36
|
+
[key, type]
|
37
|
+
end
|
38
|
+
end.to_h
|
39
|
+
end
|
40
|
+
|
41
|
+
def remove_question_mark sym
|
42
|
+
sym.to_s.gsub(/\?$/, '').to_sym
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Camille
|
2
|
+
module Types
|
3
|
+
class Tuple < Camille::BasicType
|
4
|
+
attr_reader :elements
|
5
|
+
|
6
|
+
def initialize elements
|
7
|
+
@elements = elements.map{|e| Camille::Type.instance e}
|
8
|
+
end
|
9
|
+
|
10
|
+
def check value
|
11
|
+
if value.is_a? ::Array
|
12
|
+
errors = @elements.map.with_index do |type, index|
|
13
|
+
["tuple[#{index}]", type.check(value[index])]
|
14
|
+
end.select{|x| x[1]}
|
15
|
+
|
16
|
+
unless errors.empty?
|
17
|
+
Camille::TypeError.new(**errors.to_h)
|
18
|
+
end
|
19
|
+
else
|
20
|
+
Camille::TypeError.new("Expected array, got #{value.inspect}.")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def literal
|
25
|
+
"[#{elements.map(&:literal).join(', ')}]"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Camille
|
2
|
+
module Types
|
3
|
+
class Union < Camille::BasicType
|
4
|
+
attr_reader :left, :right
|
5
|
+
|
6
|
+
def initialize left, right
|
7
|
+
@left = Camille::Type.instance left
|
8
|
+
@right = Camille::Type.instance right
|
9
|
+
end
|
10
|
+
|
11
|
+
def check value
|
12
|
+
left_result = @left.check value
|
13
|
+
if left_result
|
14
|
+
right_result = @right.check value
|
15
|
+
if right_result
|
16
|
+
Camille::TypeError.new(
|
17
|
+
'union.left' => left_result,
|
18
|
+
'union.right' => right_result
|
19
|
+
)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def literal
|
25
|
+
"#{@left.literal} | #{@right.literal}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/camille/version.rb
CHANGED
data/lib/camille.rb
CHANGED
@@ -1,8 +1,42 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "active_support"
|
4
|
+
|
3
5
|
require_relative "camille/version"
|
6
|
+
require_relative "camille/basic_type"
|
7
|
+
require_relative "camille/types"
|
8
|
+
require_relative "camille/types/number"
|
9
|
+
require_relative "camille/types/string"
|
10
|
+
require_relative "camille/types/boolean"
|
11
|
+
require_relative "camille/types/array"
|
12
|
+
require_relative "camille/types/object"
|
13
|
+
require_relative "camille/types/null"
|
14
|
+
require_relative "camille/types/undefined"
|
15
|
+
require_relative "camille/types/union"
|
16
|
+
require_relative "camille/types/tuple"
|
17
|
+
require_relative "camille/types/any"
|
18
|
+
require_relative "camille/type"
|
19
|
+
require_relative "camille/type_error"
|
20
|
+
require_relative "camille/type_error_printer"
|
21
|
+
require_relative "camille/core_ext"
|
22
|
+
require_relative "camille/endpoint"
|
23
|
+
require_relative "camille/schema"
|
24
|
+
require_relative "camille/schemas"
|
25
|
+
require_relative "camille/line"
|
26
|
+
require_relative "camille/railtie"
|
27
|
+
require_relative "camille/controller_extension"
|
28
|
+
require_relative "camille/loader"
|
29
|
+
require_relative "camille/configuration"
|
30
|
+
require_relative "camille/code_generator"
|
4
31
|
|
5
32
|
module Camille
|
6
33
|
class Error < StandardError; end
|
7
|
-
|
34
|
+
|
35
|
+
def self.configure &block
|
36
|
+
Camille::Configuration.instance_eval &block
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.generate_ts
|
40
|
+
Camille::CodeGenerator.generate_ts
|
41
|
+
end
|
8
42
|
end
|
metadata
CHANGED
@@ -1,15 +1,49 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: camille
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- 辻彩
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
12
|
-
dependencies:
|
11
|
+
date: 2023-03-10 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '6.1'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '8'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '6.1'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '8'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: listen
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '3.3'
|
40
|
+
type: :runtime
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '3.3'
|
13
47
|
description: ''
|
14
48
|
email:
|
15
49
|
- cichol@live.cn
|
@@ -25,9 +59,35 @@ files:
|
|
25
59
|
- bin/console
|
26
60
|
- bin/setup
|
27
61
|
- lib/camille.rb
|
62
|
+
- lib/camille/basic_type.rb
|
63
|
+
- lib/camille/code_generator.rb
|
64
|
+
- lib/camille/configuration.rb
|
65
|
+
- lib/camille/controller_extension.rb
|
66
|
+
- lib/camille/core_ext.rb
|
67
|
+
- lib/camille/endpoint.rb
|
68
|
+
- lib/camille/line.rb
|
69
|
+
- lib/camille/loader.rb
|
70
|
+
- lib/camille/railtie.rb
|
71
|
+
- lib/camille/schema.rb
|
72
|
+
- lib/camille/schemas.rb
|
73
|
+
- lib/camille/type.rb
|
74
|
+
- lib/camille/type_error.rb
|
75
|
+
- lib/camille/type_error_printer.rb
|
76
|
+
- lib/camille/types.rb
|
77
|
+
- lib/camille/types/any.rb
|
78
|
+
- lib/camille/types/array.rb
|
79
|
+
- lib/camille/types/boolean.rb
|
80
|
+
- lib/camille/types/null.rb
|
81
|
+
- lib/camille/types/number.rb
|
82
|
+
- lib/camille/types/object.rb
|
83
|
+
- lib/camille/types/string.rb
|
84
|
+
- lib/camille/types/tuple.rb
|
85
|
+
- lib/camille/types/undefined.rb
|
86
|
+
- lib/camille/types/union.rb
|
28
87
|
- lib/camille/version.rb
|
29
88
|
homepage: https://github.com/onyxblade/camille
|
30
|
-
licenses:
|
89
|
+
licenses:
|
90
|
+
- MIT
|
31
91
|
metadata:
|
32
92
|
homepage_uri: https://github.com/onyxblade/camille
|
33
93
|
source_code_uri: https://github.com/onyxblade/camille
|
@@ -49,5 +109,5 @@ requirements: []
|
|
49
109
|
rubygems_version: 3.2.33
|
50
110
|
signing_key:
|
51
111
|
specification_version: 4
|
52
|
-
summary:
|
112
|
+
summary: Typed API schema for Rails with TypeScript codegen
|
53
113
|
test_files: []
|