camille 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []