Skip to content

Commit 9096793

Browse files
committed
feat: initial rbs support
1 parent ff4ee14 commit 9096793

File tree

18 files changed

+212
-24
lines changed

18 files changed

+212
-24
lines changed

lib/anchor/json_schema/serializer.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def type_property(type)
2121
when Anchor::Types::Object, Anchor::Types::Object.singleton_class then serialize_object(type)
2222
when Anchor::Types::Enum.singleton_class then { enum: type.values.map(&:second) }
2323
when Anchor::Types::Unknown.singleton_class then {}
24+
when Anchor::Types::Intersection then {}
2425
else raise RuntimeError
2526
end
2627
end

lib/anchor/resource.rb

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,13 @@ def anchor_attributes_properties(included_fields:)
7979
enum = Anchor.config.infer_ar_enums && !method_defined && _model_class.try(:defined_enums).try(:[], model_method.to_s)
8080
column = !method_defined && _model_class.try(:columns_hash).try(:[], model_method.to_s)
8181

82-
if column
82+
rbs_defined = defined?(::RBS) && ::RBS::VERSION.first == "3"
83+
84+
if resource_method_defined && rbs_defined
85+
Anchor::Types::Inference::RBS.from(name.to_sym, resource_method)
86+
elsif model_method_defined && rbs_defined
87+
Anchor::Types::Inference::RBS.from(_model_class.name.to_sym, model_method.to_sym)
88+
elsif column
8389
type = Anchor::Types::Inference::ActiveRecord::SQL.from(column)
8490

8591
if enum
@@ -109,6 +115,12 @@ def anchor_attributes_properties(included_fields:)
109115
else
110116
type
111117
end
118+
elsif rbs_defined
119+
# TODO: Methods may not be defined on the class at generation time?
120+
[
121+
Anchor::Types::Inference::RBS.from(name.to_sym, resource_method),
122+
Anchor::Types::Inference::RBS.from(_model_class.name.to_sym, model_method.to_sym),
123+
].find { |t| t != Anchor::Types::Unknown } || Anchor::Types::Unknown
112124
else
113125
Anchor::Types::Unknown
114126
end

lib/anchor/type_script/serializer.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ def type_string(type, depth = 1)
1111
when Anchor::Types::Null.singleton_class then "null"
1212
when Anchor::Types::Record, Anchor::Types::Record.singleton_class then "Record<string, #{type_string(type.try(:value_type) || Anchor::Types::Unknown)}>"
1313
when Anchor::Types::Union then type.types.map { |type| type_string(type, depth) }.join(" | ")
14+
when Anchor::Types::Intersection then type.types.map { |type| "(#{type_string(type, depth)})" }.join(" & ")
1415
when Anchor::Types::Maybe then Anchor.config.maybe_as_union ? type_string(Anchor::Types::Union.new([type.type, Anchor::Types::Null]), depth) : "Maybe<#{type_string(type.type, depth)}>"
1516
when Anchor::Types::Array then Anchor.config.array_bracket_notation ? "(#{type_string(type.type, depth)})[]" : "Array<#{type_string(type.type, depth)}>"
1617
when Anchor::Types::Literal then serialize_literal(type.value)

lib/anchor/types.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class Unknown; end
1212
Array = Struct.new(:type)
1313
Literal = Struct.new(:value)
1414
Union = Struct.new(:types)
15+
Intersection = Struct.new(:types)
1516
Reference = Struct.new(:name) do
1617
def anchor_schema_name
1718
name

lib/anchor/types/inference/rbs.rb

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
module Anchor::Types::Inference
2+
module RBS
3+
class << self
4+
# @param class_name [Symbol] e.g. :Exhaustive
5+
# @param method_name [Symbol] e.g. :model_overridden
6+
def from(class_name, method_name)
7+
@loader ||= ::RBS::EnvironmentLoader.new.tap do |l|
8+
l.add(path: Rails.root.join("sig"))
9+
end
10+
11+
@env ||= ::RBS::Environment.from_loader(@loader).resolve_type_names
12+
13+
# TODO: Do we need both the absolute and non-absolute namespaces?
14+
klass = @env.class_decls.keys.find { |kl| [class_name.to_s, "::#{class_name}"].include?(kl.to_s) }
15+
return Types::Unknown unless klass
16+
17+
@builder ||= ::RBS::DefinitionBuilder.new(env: @env)
18+
instance = @builder.build_instance(klass)
19+
20+
instance_method = instance.methods[method_name]
21+
return Types::Unknown unless instance_method
22+
23+
method_types = instance.methods[method_name].method_types
24+
return Types::Unknown unless method_types.length == 1
25+
26+
return_type = method_types.first.type.return_type
27+
from_rbs_type(return_type)
28+
end
29+
30+
private
31+
32+
def from_rbs_type(type)
33+
case type
34+
when ::RBS::Types::ClassInstance then from_class_instance(type)
35+
when ::RBS::Types::Literal then Types::Literal.new(type.literal)
36+
when ::RBS::Types::Bases::Bool then Types::Boolean
37+
when ::RBS::Types::Bases::Nil then Types::Null
38+
when ::RBS::Types::Bases::Void then Types::Unknown
39+
when ::RBS::Types::Bases::Any then Types::Unknown
40+
when ::RBS::Types::Optional then Types::Maybe.new(from_rbs_type(type.type))
41+
when ::RBS::Types::Record then from_record(type)
42+
when ::RBS::Types::Union then from_union(type)
43+
when ::RBS::Types::Intersection then from_intersection(type)
44+
when ::RBS::Types::Tuple then Types::Unknown # TODO
45+
else Types::Unknown
46+
end
47+
end
48+
49+
def from_record(type)
50+
properties = type.fields.map do |name, type|
51+
Types::Property.new(name, from_rbs_type(type))
52+
end
53+
optional_properties = type.optional_fields.map do |name, type|
54+
Types::Property.new(name, from_rbs_type(type), true)
55+
end
56+
Types::Object.new(properties + optional_properties)
57+
end
58+
59+
def from_union(type)
60+
types = type.types.map { |type| from_rbs_type(type) }
61+
Types::Union.new(types)
62+
end
63+
64+
def from_intersection(type)
65+
types = type.types.map { |type| from_rbs_type(type) }
66+
Types::Intersection.new(types)
67+
end
68+
69+
def from_class_instance(type)
70+
case type.name.to_s
71+
when "::String" then Types::String
72+
when "::Numeric" then Types::Float
73+
when "::Integer" then Types::Integer
74+
when "::Float" then Types::Float
75+
when "::BigDecimal" then Types::String
76+
when "::Boolean" then Types::Boolean
77+
when "::Array" then Types::Array.new(from_rbs_type(type.args.first))
78+
else Types::Unknown
79+
end
80+
end
81+
end
82+
end
83+
end

lib/jsonapi-resources-anchor.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
require "anchor/schema"
1313
require "anchor/types/inference/jsonapi"
1414
require "anchor/types/inference/active_record"
15+
require "anchor/types/inference/rbs"
1516
require "anchor/type_script/file_structure"
1617
require "anchor/type_script/types"
1718
require "anchor/type_script/schema_generator"

spec/example/app/resources/exhaustive_resource.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ class AssertedObject < Types::Object
77
end
88

99
attribute :asserted_string, Types::String, description: "My asserted string."
10-
attribute :asserted_number, Types::Integer
10+
attribute :asserted_number
1111
attribute :asserted_boolean, Types::Boolean
12-
attribute :asserted_null, Types::Null
12+
attribute :asserted_null
1313
attribute :asserted_unknown, Types::Unknown
1414
attribute :asserted_object, AssertedObject
1515
attribute :asserted_maybe_object, Types::Maybe.new(AssertedObject)
1616
attribute :asserted_array_record, Types::Array.new(Types::Record.new(Types::Integer))
17-
attribute :asserted_union, Types::Union.new([Types::String, Types::Float])
17+
attribute :asserted_union
1818
attribute :asserted_union_array, Types::Array.new(Types::Union.new([Types::String, Types::Float]))
1919
attribute :with_description, Types::String, description: "This is a provided description."
2020
attribute :inferred_unknown

spec/example/db/schema.rb

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
class Exhaustive < ApplicationRecord
2-
def model_overridden: () -> String
2+
def model_overridden: () -> 'model_overridden'
33
end
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
class ApplicationResource
2+
end

0 commit comments

Comments
 (0)