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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bc11ed315c4734ac80beb3ae93d6bf723f999f20a9a0aa4a48852887b71c6e62
4
- data.tar.gz: ef779ddcd4a19ab7da7d2993532d8bf1f26b575cd5b4c7fbb286ab33c436b1f6
3
+ metadata.gz: efa884b2cb790a85ad8ae7f2e5462fd2a94d507aad4437c3063af81b333dd2c4
4
+ data.tar.gz: e1a1c1a68397a31521704a2fff6044a3f78997418acf5cd4fd5994c9eb5748c3
5
5
  SHA512:
6
- metadata.gz: 3199c11e71dfbdbc668ce4951fdad1f63621978f7c0ba5973ac5583bcd7e1864d2cd02c4a14a28e9ab97d8bdee3f882a62bdc96998eb742193769591e0e238ba
7
- data.tar.gz: dffe0a0f65c35cda70b1771849e1bb871d27a19b5af27790c5e29f43b47981e61d6e2c2fa9e8f7cefcb6cd8bb83f16e7e775a35ad8b6c5b42234bcf4403cfb9c
6
+ metadata.gz: 2c8c279a7e420762d3586a191b08f4e916fe2827d30a7d76702d4dcd92e466efe0cca29b46a8688f198eb6c49193c2698ef7fe87f7fc85e292489e8bf39a27b0
7
+ data.tar.gz: c4ba05522f14dc3c80f9dabfddacfe562f32475d278b49ddb49104bb76f8cb72d8032547e0cc25d5f8655c87d6ae0ddecb4e5db7cab4058d27b40236b4fba06a
data/Gemfile CHANGED
@@ -8,3 +8,6 @@ gemspec
8
8
  gem "rake", "~> 13.0"
9
9
 
10
10
  gem "rspec", "~> 3.0"
11
+
12
+ gem "rails"
13
+ gem "rspec-rails"
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(:spec)
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
@@ -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
@@ -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,12 @@
1
+ module Camille
2
+ module Types
3
+ class Any < Camille::BasicType
4
+ def check value
5
+ end
6
+
7
+ def literal
8
+ "any"
9
+ end
10
+ end
11
+ end
12
+ 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,15 @@
1
+ module Camille
2
+ module Types
3
+ class Null < Camille::BasicType
4
+ def check value
5
+ unless value == nil
6
+ Camille::TypeError.new("Expected nil, got #{value.inspect}.")
7
+ end
8
+ end
9
+
10
+ def literal
11
+ "null"
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,15 @@
1
+ module Camille
2
+ module Types
3
+ class String < Camille::BasicType
4
+ def check value
5
+ unless value.is_a? ::String
6
+ Camille::TypeError.new("Expected string, got #{value.inspect}.")
7
+ end
8
+ end
9
+
10
+ def literal
11
+ "string"
12
+ end
13
+ end
14
+ end
15
+ 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,15 @@
1
+ module Camille
2
+ module Types
3
+ class Undefined < Camille::BasicType
4
+ def check value
5
+ unless value == nil
6
+ Camille::TypeError.new("Expected nil, got #{value.inspect}.")
7
+ end
8
+ end
9
+
10
+ def literal
11
+ "undefined"
12
+ end
13
+ end
14
+ end
15
+ 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
@@ -0,0 +1,10 @@
1
+ module Camille
2
+ module Types
3
+ def self.literal_lines
4
+ Camille::Loader.loaded_types.sort_by(&:klass_name).map do |type|
5
+ instance = type.new
6
+ Camille::Line.new("type #{instance.literal} = #{instance.underlying.literal}")
7
+ end
8
+ end
9
+ end
10
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Camille
4
- VERSION = "0.0.1"
4
+ VERSION = "0.1.0"
5
5
  end
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
- # Your code goes here...
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.1
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-02-27 00:00:00.000000000 Z
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: []