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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bc11ed315c4734ac80beb3ae93d6bf723f999f20a9a0aa4a48852887b71c6e62
4
- data.tar.gz: ef779ddcd4a19ab7da7d2993532d8bf1f26b575cd5b4c7fbb286ab33c436b1f6
3
+ metadata.gz: f5911a8f4d1519a2a373ebee118bce4f110d64f5eaeafece5c8c8e46580b5c20
4
+ data.tar.gz: b59a4600bd145350d1217219b0074ffb6e80d8cdc97318995b6ba2b0bffe7450
5
5
  SHA512:
6
- metadata.gz: 3199c11e71dfbdbc668ce4951fdad1f63621978f7c0ba5973ac5583bcd7e1864d2cd02c4a14a28e9ab97d8bdee3f882a62bdc96998eb742193769591e0e238ba
7
- data.tar.gz: dffe0a0f65c35cda70b1771849e1bb871d27a19b5af27790c5e29f43b47981e61d6e2c2fa9e8f7cefcb6cd8bb83f16e7e775a35ad8b6c5b42234bcf4403cfb9c
6
+ metadata.gz: 3088068a948ca92e1a6bc90690c5f27d5e2c788c3f76e2ba40b5ebf62b7c3fdffac809411941a3f05a77ace636ce16bf7742c12c1ae5700c4b45d7bfb9fdddfb
7
+ data.tar.gz: c6f0a0b72216805bf591bc4289172e69382f702ccc14d161a3b63267dc83816a47045e9d61884029349543aac5ecf5aeb43bbc82f0c95ef23ff7df569037b8b6
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,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(: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,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
@@ -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,13 @@
1
+ require 'action_controller'
2
+
3
+ module Camille
4
+ class MainController < ActionController::Base
5
+ def endpoints_ts
6
+ if Rails.env.development?
7
+ render plain: Camille::CodeGenerator.generate_ts
8
+ else
9
+ head 404
10
+ end
11
+ end
12
+ end
13
+ 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
@@ -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.2.0"
5
5
  end
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
- # Your code goes here...
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.1
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-02-27 00:00:00.000000000 Z
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: []