Skip to content

Commit 571649f

Browse files
committed
refactor: more declarative API to construct types
1 parent e00b9c5 commit 571649f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+1057
-465
lines changed

lib/anchor/concerns/typeable.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
module Anchor
2+
module Typeable
3+
extend ActiveSupport::Concern
4+
5+
included do
6+
def object(...) = Anchor::Types::Object.new(...)
7+
def property(...) = Anchor::Types::Property.new(...)
8+
def maybe(...) = Anchor::Types::Maybe.new(...)
9+
def array(...) = Anchor::Types::Array.new(...)
10+
def union(...) = Anchor::Types::Union.new(...)
11+
def literal(...) = Anchor::Types::Literal.new(...)
12+
def literals(values) = union(values.map { |value| literal(value) })
13+
def reference(...) = Anchor::Types::Reference.new(...)
14+
def references(names) = union(names.map { |name| reference(name) })
15+
def record(value_type = Anchor::Types::Unknown) = Anchor::Types::Record.new(value_type)
16+
17+
def boolean = Anchor::Types::Boolean
18+
def null = Anchor::Types::Null
19+
def unknown = Anchor::Types::Unknown
20+
def string = Anchor::Types::String
21+
def float = Anchor::Types::Float
22+
def integer = Anchor::Types::Integer
23+
def big_decimal = Anchor::Types::BigDecimal
24+
end
25+
end
26+
end

lib/anchor/config.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ class Config
1111
:array_bracket_notation,
1212
:infer_default_as_non_null,
1313
:ar_comment_to_string,
14-
:infer_ar_enums
14+
:infer_ar_enums,
15+
:rbs
1516

1617
def initialize
1718
@ar_column_to_type = nil
@@ -26,6 +27,7 @@ def initialize
2627
@infer_default_as_non_null = nil
2728
@ar_comment_to_string = nil
2829
@infer_ar_enums = nil
30+
@rbs = "off"
2931
end
3032
end
3133
end
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module Anchor
2+
module Inference
3+
module ActiveRecord
4+
module Infer
5+
end
6+
end
7+
end
8+
end
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
module Anchor::Inference::ActiveRecord::Infer
2+
class Base
3+
include Anchor::Typeable
4+
5+
def initialize(klass)
6+
@klass = klass
7+
end
8+
9+
def self.infer(...) = new(...).infer
10+
def infer = raise NotImplementedError
11+
end
12+
end
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
module Anchor::Inference::ActiveRecord::Infer
2+
class Columns < Base
3+
def infer = object(properties)
4+
5+
private
6+
7+
def properties
8+
@klass.columns_hash.map do |name, column|
9+
next property(name, Anchor.config.ar_column_to_type.call(column)) if Anchor.config.ar_column_to_type
10+
metadata_type = from_sql_type_metadata(column.sql_type_metadata)
11+
column_type = from_column_type(column.type)
12+
13+
type = [metadata_type, column_type, unknown].compact.first
14+
type = column.null ? maybe(type) : type
15+
property(name, type)
16+
end
17+
end
18+
19+
def from_sql_type_metadata(sql_type_metadata)
20+
case sql_type_metadata.sql_type
21+
when "character varying[]", "text[]" then array(string)
22+
end
23+
end
24+
25+
def from_column_type(type)
26+
case type
27+
when :boolean then boolean
28+
when :date then string
29+
when :datetime then string
30+
when :decimal then big_decimal
31+
when :float then float
32+
when :integer then integer
33+
when :json then record
34+
when :jsonb then record
35+
when :string then string
36+
when :text then string
37+
when :time then string
38+
when :uuid then string
39+
end
40+
end
41+
end
42+
end
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module Anchor::Inference::ActiveRecord::Infer
2+
class Enums < Base
3+
def infer = object(properties)
4+
5+
private
6+
7+
def properties
8+
@klass.columns_hash.slice(*defined_enums.keys).merge(defined_enums) do |name, column, enum|
9+
property(name, column.null ? maybe(enum) : enum)
10+
end.values
11+
end
12+
13+
def defined_enums
14+
@defined_enums ||= @klass.defined_enums.transform_values { |enum| literals(enum.values) }
15+
end
16+
end
17+
end
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# TODO: Is attribute_types.keys ⊅ columns_hash.keys possible?
2+
# def superset?(klass) = klass.attribute_types.keys.to_set.superset?(klass.columns_hash.keys.to_set)
3+
# !ActiveRecord::Base.descendants.reject(&:abstract_class?).all? { |k| superset?(k) }
4+
module Anchor::Inference::ActiveRecord::Infer
5+
class Model < Base
6+
module T
7+
include Anchor::Inference::ActiveRecord::Types
8+
end
9+
10+
def infer
11+
res = [serialized, overridden, presence_required, defaulted, column_comments].compact.reduce(columns) do |acc, elem|
12+
elem.wrap(acc)
13+
end
14+
15+
res.overwrite(
16+
rbs.pick(
17+
res.pick_by_value(unknown.singleton_class).keys,
18+
),
19+
keep_description: :left,
20+
)
21+
end
22+
23+
private
24+
25+
def columns
26+
Columns.infer(@klass).overwrite(enums, keep_description: :left)
27+
end
28+
29+
def enums
30+
return object([]) unless Anchor.config.infer_ar_enums
31+
@enum_types ||= Enums.infer(@klass)
32+
end
33+
34+
def column_comments
35+
return unless Anchor.config.use_active_record_comment
36+
T::ColumnComments.new(@klass)
37+
end
38+
39+
def rbs
40+
return @rbs if defined?(@rbs)
41+
return object([]) unless Anchor::Types::Inference::RBS.enabled?
42+
Anchor::Types::Inference::RBS.validate!
43+
@rbs = RBS.infer(@klass)
44+
end
45+
46+
def serialized
47+
T::Serialized.new(@klass)
48+
end
49+
50+
def overridden
51+
T::Overridden.new(@klass)
52+
end
53+
54+
def presence_required
55+
return unless Anchor.config.use_active_record_validations
56+
T::PresenceRequired.new(@klass)
57+
end
58+
59+
def defaulted
60+
return unless Anchor.config.infer_default_as_non_null
61+
T::Defaulted.new(@klass)
62+
end
63+
end
64+
end
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
module Anchor::Inference::ActiveRecord::Infer
2+
class RBS < Base
3+
def infer = object(properties)
4+
5+
private
6+
7+
def properties
8+
included_attributes.map do |method_name|
9+
type = rbs.from_rbs_type(instance.methods[method_name].method_types.first.type.return_type)
10+
Anchor::Types::Property.new(method_name.to_s, type)
11+
end
12+
end
13+
14+
def included_attributes
15+
instance.methods.filter_map do |method_name, method_def|
16+
next if method_def&.method_types&.length != 1
17+
method_name
18+
end
19+
end
20+
21+
def instance
22+
return @instance if defined?(@instance)
23+
@instance ||= rbs.build_instance(@klass)
24+
end
25+
26+
def rbs
27+
@rbs ||= Anchor::Types::Inference::RBS
28+
end
29+
end
30+
end
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module Anchor
2+
module Inference
3+
module ActiveRecord
4+
module Types
5+
end
6+
end
7+
end
8+
end
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module Anchor::Inference::ActiveRecord::Types
2+
class Base
3+
def initialize(klass)
4+
@klass = klass
5+
end
6+
7+
def wrap(t) = raise NotImplementedError
8+
end
9+
end

0 commit comments

Comments
 (0)