camille 0.0.1 → 0.2.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 +147 -0
- data/Rakefile +16 -1
- data/lib/camille/basic_type.rb +40 -0
- data/lib/camille/code_generator.rb +22 -0
- data/lib/camille/configuration.rb +22 -0
- data/lib/camille/controller_extension.rb +46 -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 +92 -0
- data/lib/camille/main_controller.rb +13 -0
- data/lib/camille/railtie.rb +33 -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 +36 -1
- metadata +52 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f5911a8f4d1519a2a373ebee118bce4f110d64f5eaeafece5c8c8e46580b5c20
|
4
|
+
data.tar.gz: b59a4600bd145350d1217219b0074ffb6e80d8cdc97318995b6ba2b0bffe7450
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3088068a948ca92e1a6bc90690c5f27d5e2c788c3f76e2ba40b5ebf62b7c3fdffac809411941a3f05a77ace636ce16bf7742c12c1ae5700c4b45d7bfb9fdddfb
|
7
|
+
data.tar.gz: c6f0a0b72216805bf591bc4289172e69382f702ccc14d161a3b63267dc83816a47045e9d61884029349543aac5ecf5aeb43bbc82f0c95ef23ff7df569037b8b6
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -2,11 +2,140 @@ PATH
|
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
4
|
camille (0.1.0)
|
5
|
+
rails (>= 6.1, < 8)
|
5
6
|
|
6
7
|
GEM
|
7
8
|
remote: https://rubygems.org/
|
8
9
|
specs:
|
10
|
+
actioncable (7.0.4.2)
|
11
|
+
actionpack (= 7.0.4.2)
|
12
|
+
activesupport (= 7.0.4.2)
|
13
|
+
nio4r (~> 2.0)
|
14
|
+
websocket-driver (>= 0.6.1)
|
15
|
+
actionmailbox (7.0.4.2)
|
16
|
+
actionpack (= 7.0.4.2)
|
17
|
+
activejob (= 7.0.4.2)
|
18
|
+
activerecord (= 7.0.4.2)
|
19
|
+
activestorage (= 7.0.4.2)
|
20
|
+
activesupport (= 7.0.4.2)
|
21
|
+
mail (>= 2.7.1)
|
22
|
+
net-imap
|
23
|
+
net-pop
|
24
|
+
net-smtp
|
25
|
+
actionmailer (7.0.4.2)
|
26
|
+
actionpack (= 7.0.4.2)
|
27
|
+
actionview (= 7.0.4.2)
|
28
|
+
activejob (= 7.0.4.2)
|
29
|
+
activesupport (= 7.0.4.2)
|
30
|
+
mail (~> 2.5, >= 2.5.4)
|
31
|
+
net-imap
|
32
|
+
net-pop
|
33
|
+
net-smtp
|
34
|
+
rails-dom-testing (~> 2.0)
|
35
|
+
actionpack (7.0.4.2)
|
36
|
+
actionview (= 7.0.4.2)
|
37
|
+
activesupport (= 7.0.4.2)
|
38
|
+
rack (~> 2.0, >= 2.2.0)
|
39
|
+
rack-test (>= 0.6.3)
|
40
|
+
rails-dom-testing (~> 2.0)
|
41
|
+
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
42
|
+
actiontext (7.0.4.2)
|
43
|
+
actionpack (= 7.0.4.2)
|
44
|
+
activerecord (= 7.0.4.2)
|
45
|
+
activestorage (= 7.0.4.2)
|
46
|
+
activesupport (= 7.0.4.2)
|
47
|
+
globalid (>= 0.6.0)
|
48
|
+
nokogiri (>= 1.8.5)
|
49
|
+
actionview (7.0.4.2)
|
50
|
+
activesupport (= 7.0.4.2)
|
51
|
+
builder (~> 3.1)
|
52
|
+
erubi (~> 1.4)
|
53
|
+
rails-dom-testing (~> 2.0)
|
54
|
+
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
55
|
+
activejob (7.0.4.2)
|
56
|
+
activesupport (= 7.0.4.2)
|
57
|
+
globalid (>= 0.3.6)
|
58
|
+
activemodel (7.0.4.2)
|
59
|
+
activesupport (= 7.0.4.2)
|
60
|
+
activerecord (7.0.4.2)
|
61
|
+
activemodel (= 7.0.4.2)
|
62
|
+
activesupport (= 7.0.4.2)
|
63
|
+
activestorage (7.0.4.2)
|
64
|
+
actionpack (= 7.0.4.2)
|
65
|
+
activejob (= 7.0.4.2)
|
66
|
+
activerecord (= 7.0.4.2)
|
67
|
+
activesupport (= 7.0.4.2)
|
68
|
+
marcel (~> 1.0)
|
69
|
+
mini_mime (>= 1.1.0)
|
70
|
+
activesupport (7.0.4.2)
|
71
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
72
|
+
i18n (>= 1.6, < 2)
|
73
|
+
minitest (>= 5.1)
|
74
|
+
tzinfo (~> 2.0)
|
75
|
+
builder (3.2.4)
|
76
|
+
concurrent-ruby (1.2.2)
|
77
|
+
crass (1.0.6)
|
78
|
+
date (3.3.3)
|
9
79
|
diff-lcs (1.5.0)
|
80
|
+
erubi (1.12.0)
|
81
|
+
globalid (1.1.0)
|
82
|
+
activesupport (>= 5.0)
|
83
|
+
i18n (1.12.0)
|
84
|
+
concurrent-ruby (~> 1.0)
|
85
|
+
loofah (2.19.1)
|
86
|
+
crass (~> 1.0.2)
|
87
|
+
nokogiri (>= 1.5.9)
|
88
|
+
mail (2.8.1)
|
89
|
+
mini_mime (>= 0.1.1)
|
90
|
+
net-imap
|
91
|
+
net-pop
|
92
|
+
net-smtp
|
93
|
+
marcel (1.0.2)
|
94
|
+
method_source (1.0.0)
|
95
|
+
mini_mime (1.1.2)
|
96
|
+
minitest (5.17.0)
|
97
|
+
net-imap (0.3.4)
|
98
|
+
date
|
99
|
+
net-protocol
|
100
|
+
net-pop (0.1.2)
|
101
|
+
net-protocol
|
102
|
+
net-protocol (0.2.1)
|
103
|
+
timeout
|
104
|
+
net-smtp (0.3.3)
|
105
|
+
net-protocol
|
106
|
+
nio4r (2.5.8)
|
107
|
+
nokogiri (1.14.2-x86_64-linux)
|
108
|
+
racc (~> 1.4)
|
109
|
+
racc (1.6.2)
|
110
|
+
rack (2.2.6.3)
|
111
|
+
rack-test (2.0.2)
|
112
|
+
rack (>= 1.3)
|
113
|
+
rails (7.0.4.2)
|
114
|
+
actioncable (= 7.0.4.2)
|
115
|
+
actionmailbox (= 7.0.4.2)
|
116
|
+
actionmailer (= 7.0.4.2)
|
117
|
+
actionpack (= 7.0.4.2)
|
118
|
+
actiontext (= 7.0.4.2)
|
119
|
+
actionview (= 7.0.4.2)
|
120
|
+
activejob (= 7.0.4.2)
|
121
|
+
activemodel (= 7.0.4.2)
|
122
|
+
activerecord (= 7.0.4.2)
|
123
|
+
activestorage (= 7.0.4.2)
|
124
|
+
activesupport (= 7.0.4.2)
|
125
|
+
bundler (>= 1.15.0)
|
126
|
+
railties (= 7.0.4.2)
|
127
|
+
rails-dom-testing (2.0.3)
|
128
|
+
activesupport (>= 4.2.0)
|
129
|
+
nokogiri (>= 1.6)
|
130
|
+
rails-html-sanitizer (1.5.0)
|
131
|
+
loofah (~> 2.19, >= 2.19.1)
|
132
|
+
railties (7.0.4.2)
|
133
|
+
actionpack (= 7.0.4.2)
|
134
|
+
activesupport (= 7.0.4.2)
|
135
|
+
method_source
|
136
|
+
rake (>= 12.2)
|
137
|
+
thor (~> 1.0)
|
138
|
+
zeitwerk (~> 2.5)
|
10
139
|
rake (13.0.6)
|
11
140
|
rspec (3.12.0)
|
12
141
|
rspec-core (~> 3.12.0)
|
@@ -20,15 +149,33 @@ GEM
|
|
20
149
|
rspec-mocks (3.12.3)
|
21
150
|
diff-lcs (>= 1.2.0, < 2.0)
|
22
151
|
rspec-support (~> 3.12.0)
|
152
|
+
rspec-rails (6.0.1)
|
153
|
+
actionpack (>= 6.1)
|
154
|
+
activesupport (>= 6.1)
|
155
|
+
railties (>= 6.1)
|
156
|
+
rspec-core (~> 3.11)
|
157
|
+
rspec-expectations (~> 3.11)
|
158
|
+
rspec-mocks (~> 3.11)
|
159
|
+
rspec-support (~> 3.11)
|
23
160
|
rspec-support (3.12.0)
|
161
|
+
thor (1.2.1)
|
162
|
+
timeout (0.3.2)
|
163
|
+
tzinfo (2.0.6)
|
164
|
+
concurrent-ruby (~> 1.0)
|
165
|
+
websocket-driver (0.7.5)
|
166
|
+
websocket-extensions (>= 0.1.0)
|
167
|
+
websocket-extensions (0.1.5)
|
168
|
+
zeitwerk (2.6.7)
|
24
169
|
|
25
170
|
PLATFORMS
|
26
171
|
x86_64-linux
|
27
172
|
|
28
173
|
DEPENDENCIES
|
29
174
|
camille!
|
175
|
+
rails
|
30
176
|
rake (~> 13.0)
|
31
177
|
rspec (~> 3.0)
|
178
|
+
rspec-rails
|
32
179
|
|
33
180
|
BUNDLED WITH
|
34
181
|
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,22 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
|
3
|
+
module Camille
|
4
|
+
module CodeGenerator
|
5
|
+
def self.generate_ts
|
6
|
+
io = StringIO.new
|
7
|
+
io.puts "// This file is automatically generated."
|
8
|
+
io.puts Camille::Configuration.ts_header
|
9
|
+
io.puts
|
10
|
+
Camille::Types.literal_lines.each do |line|
|
11
|
+
io.puts "export #{line}"
|
12
|
+
end
|
13
|
+
io.puts
|
14
|
+
io.print "export default "
|
15
|
+
Camille::Schemas.literal_lines.each do |line|
|
16
|
+
io.puts line
|
17
|
+
end
|
18
|
+
io.string
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
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,46 @@
|
|
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 endpoint = camille_endpoint
|
38
|
+
params.deep_transform_keys!{|key| key.to_s.underscore}
|
39
|
+
super
|
40
|
+
else
|
41
|
+
super
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
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,92 @@
|
|
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
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def eager_load
|
26
|
+
@eager_loading = true
|
27
|
+
load @configuration_location
|
28
|
+
@zeitwerk_loader.eager_load
|
29
|
+
@eager_loading = false
|
30
|
+
end
|
31
|
+
|
32
|
+
def eager_loading?
|
33
|
+
@eager_loading
|
34
|
+
end
|
35
|
+
|
36
|
+
def reload_types_and_schemas
|
37
|
+
synchronize do
|
38
|
+
Camille::Loader.loaded_types.clear
|
39
|
+
Camille::Loader.loaded_schemas.clear
|
40
|
+
@zeitwerk_loader.reload
|
41
|
+
eager_load
|
42
|
+
construct_controller_name_to_schema_map
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def register_routes router_context
|
47
|
+
Camille::Loader.loaded_schemas.each do |schema|
|
48
|
+
schema.endpoints.each do |name, endpoint|
|
49
|
+
router_context.public_send(endpoint.verb, endpoint.path, controller: schema.path.gsub(/^\//, ''), action: endpoint.name, as: false)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def loaded_types
|
55
|
+
synchronize do
|
56
|
+
@loaded_types ||= []
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def loaded_schemas
|
61
|
+
synchronize do
|
62
|
+
@loaded_schemas ||= []
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def controller_name_to_schema_map
|
67
|
+
synchronize do
|
68
|
+
@controller_name_to_schema_map ||= {}
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def construct_controller_name_to_schema_map
|
73
|
+
synchronize do
|
74
|
+
controller_name_to_schema_map.clear
|
75
|
+
loaded_schemas.each do |schema|
|
76
|
+
controller_class_name = "#{schema.klass_name}Controller"
|
77
|
+
controller_name_to_schema_map[controller_class_name] = schema
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def reload
|
83
|
+
synchronize do
|
84
|
+
reload_types_and_schemas
|
85
|
+
Rails.application.reload_routes!
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,33 @@
|
|
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
|
+
|
12
|
+
app.routes.prepend do
|
13
|
+
get '/camille/endpoints.ts' => 'camille/main#endpoints_ts'
|
14
|
+
Camille::Loader.register_routes(self)
|
15
|
+
end
|
16
|
+
|
17
|
+
dir = "#{Rails.root}/config/camille"
|
18
|
+
|
19
|
+
update_checker = ActiveSupport::FileUpdateChecker.new([], {dir => ['rb']}) do
|
20
|
+
Camille::Loader.reload
|
21
|
+
end
|
22
|
+
|
23
|
+
app.reloaders << update_checker
|
24
|
+
|
25
|
+
app.reloader.to_run do
|
26
|
+
require_unload_lock!
|
27
|
+
update_checker.execute_if_updated
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
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,43 @@
|
|
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"
|
31
|
+
require_relative "camille/main_controller"
|
4
32
|
|
5
33
|
module Camille
|
6
34
|
class Error < StandardError; end
|
7
|
-
|
35
|
+
|
36
|
+
def self.configure &block
|
37
|
+
Camille::Configuration.instance_eval &block
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.generate_ts
|
41
|
+
Camille::CodeGenerator.generate_ts
|
42
|
+
end
|
8
43
|
end
|
metadata
CHANGED
@@ -1,15 +1,35 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: camille
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.2.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-14 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'
|
13
33
|
description: ''
|
14
34
|
email:
|
15
35
|
- cichol@live.cn
|
@@ -25,9 +45,36 @@ files:
|
|
25
45
|
- bin/console
|
26
46
|
- bin/setup
|
27
47
|
- lib/camille.rb
|
48
|
+
- lib/camille/basic_type.rb
|
49
|
+
- lib/camille/code_generator.rb
|
50
|
+
- lib/camille/configuration.rb
|
51
|
+
- lib/camille/controller_extension.rb
|
52
|
+
- lib/camille/core_ext.rb
|
53
|
+
- lib/camille/endpoint.rb
|
54
|
+
- lib/camille/line.rb
|
55
|
+
- lib/camille/loader.rb
|
56
|
+
- lib/camille/main_controller.rb
|
57
|
+
- lib/camille/railtie.rb
|
58
|
+
- lib/camille/schema.rb
|
59
|
+
- lib/camille/schemas.rb
|
60
|
+
- lib/camille/type.rb
|
61
|
+
- lib/camille/type_error.rb
|
62
|
+
- lib/camille/type_error_printer.rb
|
63
|
+
- lib/camille/types.rb
|
64
|
+
- lib/camille/types/any.rb
|
65
|
+
- lib/camille/types/array.rb
|
66
|
+
- lib/camille/types/boolean.rb
|
67
|
+
- lib/camille/types/null.rb
|
68
|
+
- lib/camille/types/number.rb
|
69
|
+
- lib/camille/types/object.rb
|
70
|
+
- lib/camille/types/string.rb
|
71
|
+
- lib/camille/types/tuple.rb
|
72
|
+
- lib/camille/types/undefined.rb
|
73
|
+
- lib/camille/types/union.rb
|
28
74
|
- lib/camille/version.rb
|
29
75
|
homepage: https://github.com/onyxblade/camille
|
30
|
-
licenses:
|
76
|
+
licenses:
|
77
|
+
- MIT
|
31
78
|
metadata:
|
32
79
|
homepage_uri: https://github.com/onyxblade/camille
|
33
80
|
source_code_uri: https://github.com/onyxblade/camille
|
@@ -49,5 +96,5 @@ requirements: []
|
|
49
96
|
rubygems_version: 3.2.33
|
50
97
|
signing_key:
|
51
98
|
specification_version: 4
|
52
|
-
summary:
|
99
|
+
summary: Typed API schema for Rails with TypeScript codegen
|
53
100
|
test_files: []
|