boba 0.1.12 → 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 +4 -4
- data/LICENSE +1 -1
- data/README.md +4 -2
- data/lib/boba/version.rb +1 -1
- data/lib/tapioca/dsl/compilers/action_mcp_tool.rb +115 -0
- data/lib/tapioca/dsl/compilers/acts_as_taggable_on.rb +167 -0
- data/lib/tapioca/dsl/compilers/friendly_id.rb +123 -0
- data/lib/tapioca/dsl/compilers/money_rails.rb +20 -3
- data/lib/tapioca/dsl/compilers/paper_trail.rb +93 -0
- data/lib/tapioca/dsl/compilers/ransack.rb +89 -0
- data/lib/tapioca/dsl/compilers/ruby_llm.rb +94 -0
- metadata +16 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fc8f00fb09f7dc829a0f4c174ef8a3916068328de2868496e5430c83475c9e02
|
|
4
|
+
data.tar.gz: fa0393e8df72dfa4fe170dfe2c6ede7d07f308e257b3a5a347f875a6eba9915a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 948d54418c7e911be046e53504ebf66ebb2c1190d8633643b01aadc5dbd12d137a9b7fc3d1da33ddab2298cdad40d9edcfe03ce4d9b0fe04dc59d83b17d67618
|
|
7
|
+
data.tar.gz: 83921a7f8c4285fb0fe36d65dcb565ca3cc01c421df4e123cec5abb97afa17f3393a08dcf10977f4653190511669c1f5211bbda4a324f030804d3ff19d684953
|
data/LICENSE
CHANGED
data/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Boba
|
|
2
2
|
|
|
3
|
+
> This project has moved from `angellist/boba` to [`stathis-alexander/boba`](https://github.com/stathis-alexander/boba).
|
|
4
|
+
|
|
3
5
|
> :warning: This software is currently under active development. It should not be considered stable until 1.0.0.
|
|
4
6
|
|
|
5
7
|
Boba is a collection of compilers for Sorbet & Tapioca.
|
|
@@ -8,7 +10,7 @@ Tapioca is very opinionated about what types of compilers or changes are accepte
|
|
|
8
10
|
|
|
9
11
|
### Available Compilers
|
|
10
12
|
|
|
11
|
-
See [the compilers manual](https://github.com/
|
|
13
|
+
See [the compilers manual](https://github.com/stathis-alexander/boba/blob/main/manual/compilers.md) for a list of available compilers.
|
|
12
14
|
|
|
13
15
|
## Usage
|
|
14
16
|
|
|
@@ -82,7 +84,7 @@ We try to stay as up-to-date with Tapioca versions as possible, but can sometime
|
|
|
82
84
|
|
|
83
85
|
## Contributing
|
|
84
86
|
|
|
85
|
-
Bugs and feature requests are welcome and should be [filed as issues on github](https://github.com/
|
|
87
|
+
Bugs and feature requests are welcome and should be [filed as issues on github](https://github.com/stathis-alexander/boba/issues).
|
|
86
88
|
|
|
87
89
|
### New Compilers
|
|
88
90
|
|
data/lib/boba/version.rb
CHANGED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
return unless defined?(ActionMCP::Tool)
|
|
5
|
+
|
|
6
|
+
module Tapioca
|
|
7
|
+
module Dsl
|
|
8
|
+
module Compilers
|
|
9
|
+
# `Tapioca::Dsl::Compilers::ActionMCPTool` decorates RBI files for tools of the `actionmcp` gem.
|
|
10
|
+
# https://github.com/seuros/action_mcp
|
|
11
|
+
#
|
|
12
|
+
# `property` and `collection` declare an Active Model attribute, so Tapioca's own
|
|
13
|
+
# `ActiveModelAttributes` compiler already generates a reader for each of them — but always nilable and,
|
|
14
|
+
# for collections, always `T::Array[T.untyped]`. A tool knows more than the attribute does: a property
|
|
15
|
+
# declared `required: true` is validated before `perform` runs, and a collection knows its item type.
|
|
16
|
+
#
|
|
17
|
+
# For example, with the following tool:
|
|
18
|
+
#
|
|
19
|
+
# ~~~rb
|
|
20
|
+
# class SearchTool < ApplicationMCPTool
|
|
21
|
+
# property :query, type: "string", required: true
|
|
22
|
+
# property :limit, type: "integer"
|
|
23
|
+
# collection :tags, type: "string"
|
|
24
|
+
# end
|
|
25
|
+
# ~~~
|
|
26
|
+
#
|
|
27
|
+
# This compiler will produce the RBI file `search_tool.rbi` with the following content:
|
|
28
|
+
#
|
|
29
|
+
# ~~~rbi
|
|
30
|
+
# # search_tool.rbi
|
|
31
|
+
# # typed: true
|
|
32
|
+
# class SearchTool
|
|
33
|
+
# sig { returns(::String) }
|
|
34
|
+
# def query; end
|
|
35
|
+
#
|
|
36
|
+
# sig { returns(T.nilable(T::Array[::String])) }
|
|
37
|
+
# def tags; end
|
|
38
|
+
# end
|
|
39
|
+
# ~~~
|
|
40
|
+
#
|
|
41
|
+
# `limit` is left to the `ActiveModelAttributes` compiler: nothing is known about it beyond the
|
|
42
|
+
# attribute type. The readers generated here sit on the class itself, so they take precedence over the
|
|
43
|
+
# ones in `GeneratedAttributeMethods` without conflicting with them.
|
|
44
|
+
class ActionMCPTool < Tapioca::Dsl::Compiler
|
|
45
|
+
ConstantType = type_member { { fixed: T.class_of(::ActionMCP::Tool) } }
|
|
46
|
+
|
|
47
|
+
JSON_TYPES = {
|
|
48
|
+
"string" => "::String",
|
|
49
|
+
"integer" => "::Integer",
|
|
50
|
+
"number" => "::Float",
|
|
51
|
+
"boolean" => "T::Boolean",
|
|
52
|
+
"object" => "T::Hash[::String, T.untyped]",
|
|
53
|
+
}.freeze #: Hash[String, String]
|
|
54
|
+
|
|
55
|
+
# @override
|
|
56
|
+
#: -> void
|
|
57
|
+
def decorate
|
|
58
|
+
methods = readers_to_narrow
|
|
59
|
+
return if methods.empty?
|
|
60
|
+
|
|
61
|
+
root.create_path(constant) do |tool|
|
|
62
|
+
methods.each do |name, type|
|
|
63
|
+
tool.create_method(name, return_type: type)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
class << self
|
|
69
|
+
# @override
|
|
70
|
+
#: -> Enumerable[Module[top]]
|
|
71
|
+
def gather_constants
|
|
72
|
+
descendants_of(::ActionMCP::Tool).reject { |tool| T.unsafe(tool).abstract? }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
# Only properties the attribute type cannot describe on its own: required ones, whose reader is not
|
|
79
|
+
# nilable, and collections, whose item type is known.
|
|
80
|
+
#: -> Hash[String, String]
|
|
81
|
+
def readers_to_narrow
|
|
82
|
+
required = T.unsafe(constant)._required_properties.map(&:to_s)
|
|
83
|
+
|
|
84
|
+
T.unsafe(constant)._schema_properties.each_with_object({}) do |(name, definition), methods|
|
|
85
|
+
property_name = name.to_s
|
|
86
|
+
type = type_for(definition)
|
|
87
|
+
next unless type
|
|
88
|
+
|
|
89
|
+
property_required = required.include?(property_name)
|
|
90
|
+
next unless property_required || definition[:type].to_s == "array"
|
|
91
|
+
|
|
92
|
+
methods[property_name] = property_required ? type : "T.nilable(#{type})"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
#: (Hash[Symbol, untyped] definition) -> String?
|
|
97
|
+
def type_for(definition)
|
|
98
|
+
json_type = definition[:type].to_s
|
|
99
|
+
return array_type_for(definition) if json_type == "array"
|
|
100
|
+
|
|
101
|
+
JSON_TYPES[json_type]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
#: (Hash[Symbol, untyped] definition) -> String?
|
|
105
|
+
def array_type_for(definition)
|
|
106
|
+
item_type = definition.dig(:items, :type).to_s
|
|
107
|
+
item = JSON_TYPES[item_type]
|
|
108
|
+
return unless item
|
|
109
|
+
|
|
110
|
+
"T::Array[#{item}]"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
return unless defined?(ActsAsTaggableOn)
|
|
5
|
+
|
|
6
|
+
module Tapioca
|
|
7
|
+
module Dsl
|
|
8
|
+
module Compilers
|
|
9
|
+
# `Tapioca::Dsl::Compilers::ActsAsTaggableOn` decorates RBI files for models using the
|
|
10
|
+
# `acts-as-taggable-on` gem. https://github.com/mbleigh/acts-as-taggable-on
|
|
11
|
+
#
|
|
12
|
+
# `acts_as_taggable_on` itself needs no compiler: the gem extends `ActiveRecord::Base` from an
|
|
13
|
+
# `ActiveSupport.on_load(:active_record)` hook, and anything that loads `ActiveRecord::Base` during gem
|
|
14
|
+
# RBI generation fires it, so tapioca records the extend in the gem RBI. What only exists per model is
|
|
15
|
+
# what the declaration then installs on the declaring class: the five mixin pairs that carry the
|
|
16
|
+
# tagging API, and one set of accessors per tag context.
|
|
17
|
+
#
|
|
18
|
+
# For example, with the following `ActiveRecord::Base` subclass:
|
|
19
|
+
#
|
|
20
|
+
# ~~~rb
|
|
21
|
+
# class Post < ApplicationRecord
|
|
22
|
+
# acts_as_taggable_on :tags
|
|
23
|
+
# end
|
|
24
|
+
# ~~~
|
|
25
|
+
#
|
|
26
|
+
# This compiler will produce the RBI file `post.rbi` with the following content:
|
|
27
|
+
#
|
|
28
|
+
# ~~~rbi
|
|
29
|
+
# # post.rbi
|
|
30
|
+
# # typed: true
|
|
31
|
+
# class Post
|
|
32
|
+
# include ActsAsTaggableOn::Taggable::Core
|
|
33
|
+
# extend ActsAsTaggableOn::Taggable::Core::ClassMethods
|
|
34
|
+
# include ActsAsTaggableOn::Taggable::Collection
|
|
35
|
+
# extend ActsAsTaggableOn::Taggable::Collection::ClassMethods
|
|
36
|
+
#
|
|
37
|
+
# sig { returns(::ActsAsTaggableOn::TagList) }
|
|
38
|
+
# def all_tags_list; end
|
|
39
|
+
#
|
|
40
|
+
# sig { params(options: T.untyped).returns(T.untyped) }
|
|
41
|
+
# def find_related_on_tags(options = {}); end
|
|
42
|
+
#
|
|
43
|
+
# sig { params(options: T.untyped).returns(T.untyped) }
|
|
44
|
+
# def find_related_tags(options = {}); end
|
|
45
|
+
#
|
|
46
|
+
# sig { params(klass: T.untyped, options: T.untyped).returns(T.untyped) }
|
|
47
|
+
# def find_related_tags_for(klass, options = {}); end
|
|
48
|
+
#
|
|
49
|
+
# sig { params(options: T.untyped).returns(T.untyped) }
|
|
50
|
+
# def tag_counts(options = {}); end
|
|
51
|
+
#
|
|
52
|
+
# sig { returns(::ActsAsTaggableOn::TagList) }
|
|
53
|
+
# def tag_list; end
|
|
54
|
+
#
|
|
55
|
+
# sig { params(new_tags: T.untyped).returns(T.untyped) }
|
|
56
|
+
# def tag_list=(new_tags); end
|
|
57
|
+
#
|
|
58
|
+
# sig { params(owner: T.untyped).returns(::ActsAsTaggableOn::TagList) }
|
|
59
|
+
# def tags_from(owner); end
|
|
60
|
+
#
|
|
61
|
+
# sig { params(limit: Integer).returns(T.untyped) }
|
|
62
|
+
# def top_tags(limit = 10); end
|
|
63
|
+
#
|
|
64
|
+
# class << self
|
|
65
|
+
# sig { params(options: T.untyped).returns(T.untyped) }
|
|
66
|
+
# def tag_counts(options = {}); end
|
|
67
|
+
#
|
|
68
|
+
# sig { params(limit: Integer).returns(T.untyped) }
|
|
69
|
+
# def top_tags(limit = 10); end
|
|
70
|
+
# end
|
|
71
|
+
# end
|
|
72
|
+
# ~~~
|
|
73
|
+
#
|
|
74
|
+
# The mixins are declared rather than re-implemented, so `tagged_with`, `tag_list_on` and the rest keep
|
|
75
|
+
# the signatures they have in the gem RBI. Only the per-context methods, which exist for the contexts of
|
|
76
|
+
# this model alone, are generated.
|
|
77
|
+
class ActsAsTaggableOn < Tapioca::Dsl::Compiler
|
|
78
|
+
ConstantType = type_member { { fixed: T.class_of(::ActiveRecord::Base) } }
|
|
79
|
+
|
|
80
|
+
# Every `acts_as_taggable_on` call installs this set; the names are fixed, so there is nothing to
|
|
81
|
+
# discover by walking ancestors.
|
|
82
|
+
MIXINS = ["Core", "Collection", "Caching", "Ownership", "Related"] #: Array[String]
|
|
83
|
+
|
|
84
|
+
# @override
|
|
85
|
+
#: -> void
|
|
86
|
+
def decorate
|
|
87
|
+
tag_types = T.unsafe(constant).tag_types.map(&:to_s)
|
|
88
|
+
return if tag_types.empty?
|
|
89
|
+
|
|
90
|
+
root.create_path(constant) do |model|
|
|
91
|
+
MIXINS.each do |mixin|
|
|
92
|
+
model.create_include("ActsAsTaggableOn::Taggable::#{mixin}")
|
|
93
|
+
model.create_extend("ActsAsTaggableOn::Taggable::#{mixin}::ClassMethods")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
tag_types.each do |tag_type|
|
|
97
|
+
create_context_methods(model, tag_type)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
class << self
|
|
103
|
+
# @override
|
|
104
|
+
#: -> Enumerable[Module[top]]
|
|
105
|
+
def gather_constants
|
|
106
|
+
descendants_of(::ActiveRecord::Base)
|
|
107
|
+
.reject(&:abstract_class?)
|
|
108
|
+
.select { |model| model.respond_to?(:taggable?) && T.unsafe(model).taggable? }
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
#: (RBI::Scope model, String tag_type) -> void
|
|
115
|
+
def create_context_methods(model, tag_type)
|
|
116
|
+
singular = tag_type.singularize
|
|
117
|
+
|
|
118
|
+
model.create_method("#{singular}_list", return_type: "::ActsAsTaggableOn::TagList")
|
|
119
|
+
model.create_method(
|
|
120
|
+
"#{singular}_list=",
|
|
121
|
+
parameters: [create_param("new_tags", type: "T.untyped")],
|
|
122
|
+
return_type: "T.untyped",
|
|
123
|
+
)
|
|
124
|
+
model.create_method("all_#{tag_type}_list", return_type: "::ActsAsTaggableOn::TagList")
|
|
125
|
+
model.create_method(
|
|
126
|
+
"#{tag_type}_from",
|
|
127
|
+
parameters: [create_param("owner", type: "T.untyped")],
|
|
128
|
+
return_type: "::ActsAsTaggableOn::TagList",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# `Related` builds these off the same context list; `find_related_on_` is an alias of the first.
|
|
132
|
+
# They answer a relation of whichever class was asked for, so the return stays untyped.
|
|
133
|
+
["find_related_#{tag_type}", "find_related_on_#{tag_type}"].each do |name|
|
|
134
|
+
model.create_method(
|
|
135
|
+
name,
|
|
136
|
+
parameters: [create_opt_param("options", type: "T.untyped", default: "{}")],
|
|
137
|
+
return_type: "T.untyped",
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
model.create_method(
|
|
141
|
+
"find_related_#{tag_type}_for",
|
|
142
|
+
parameters: [
|
|
143
|
+
create_param("klass", type: "T.untyped"),
|
|
144
|
+
create_opt_param("options", type: "T.untyped", default: "{}"),
|
|
145
|
+
],
|
|
146
|
+
return_type: "T.untyped",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
[false, true].each do |class_method|
|
|
150
|
+
model.create_method(
|
|
151
|
+
"#{singular}_counts",
|
|
152
|
+
parameters: [create_opt_param("options", type: "T.untyped", default: "{}")],
|
|
153
|
+
return_type: "T.untyped",
|
|
154
|
+
class_method: class_method,
|
|
155
|
+
)
|
|
156
|
+
model.create_method(
|
|
157
|
+
"top_#{tag_type}",
|
|
158
|
+
parameters: [create_opt_param("limit", type: "Integer", default: "10")],
|
|
159
|
+
return_type: "T.untyped",
|
|
160
|
+
class_method: class_method,
|
|
161
|
+
)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
return unless defined?(FriendlyId)
|
|
5
|
+
|
|
6
|
+
require "tapioca/dsl/helpers/active_record_constants_helper"
|
|
7
|
+
|
|
8
|
+
module Tapioca
|
|
9
|
+
module Dsl
|
|
10
|
+
module Compilers
|
|
11
|
+
# `Tapioca::Dsl::Compilers::FriendlyId` decorates RBI files for models using the `friendly_id` gem.
|
|
12
|
+
# https://github.com/norman/friendly_id
|
|
13
|
+
#
|
|
14
|
+
# A model opts in with `extend FriendlyId`, but the modules that carry the DSL are mixed in from
|
|
15
|
+
# `self.extended` and `Configuration#use` hooks at runtime, so Sorbet sees none of them: not the
|
|
16
|
+
# `friendly_id` declaration itself, not `friendly`, and not the instance methods the `use:` modules add.
|
|
17
|
+
#
|
|
18
|
+
# For example, with the following `ActiveRecord::Base` subclass:
|
|
19
|
+
#
|
|
20
|
+
# ~~~rb
|
|
21
|
+
# class Post < ApplicationRecord
|
|
22
|
+
# extend FriendlyId
|
|
23
|
+
#
|
|
24
|
+
# friendly_id :title, use: :slugged
|
|
25
|
+
# end
|
|
26
|
+
# ~~~
|
|
27
|
+
#
|
|
28
|
+
# This compiler will produce the RBI file `post.rbi` with the following content:
|
|
29
|
+
#
|
|
30
|
+
# ~~~rbi
|
|
31
|
+
# # post.rbi
|
|
32
|
+
# # typed: true
|
|
33
|
+
# class Post
|
|
34
|
+
# include FriendlyId::Slugged
|
|
35
|
+
# include FriendlyId::Model
|
|
36
|
+
# include FriendlyId::Reserved
|
|
37
|
+
# include FriendlyId::UnfriendlyUtils
|
|
38
|
+
# extend FriendlyId::Base
|
|
39
|
+
#
|
|
40
|
+
# module GeneratedAssociationRelationMethods
|
|
41
|
+
# sig { returns(PrivateAssociationRelation) }
|
|
42
|
+
# def friendly; end
|
|
43
|
+
# end
|
|
44
|
+
#
|
|
45
|
+
# module GeneratedRelationMethods
|
|
46
|
+
# sig { returns(PrivateRelation) }
|
|
47
|
+
# def friendly; end
|
|
48
|
+
# end
|
|
49
|
+
# end
|
|
50
|
+
# ~~~
|
|
51
|
+
#
|
|
52
|
+
# The mixins are taken from the model as it is actually configured, so a model using `use: :history`
|
|
53
|
+
# or `use: :scoped` gets the modules that go with it. Their signatures already live in the gem RBI,
|
|
54
|
+
# which leaves nothing to hand-write here. `friendly` is declared on the relation modules as well
|
|
55
|
+
# because `ActiveRecord::Relation` reaches it by delegating to the model class.
|
|
56
|
+
class FriendlyId < Tapioca::Dsl::Compiler
|
|
57
|
+
include Helpers::ActiveRecordConstantsHelper
|
|
58
|
+
|
|
59
|
+
ConstantType = type_member { { fixed: T.class_of(::ActiveRecord::Base) } }
|
|
60
|
+
|
|
61
|
+
# @override
|
|
62
|
+
#: -> void
|
|
63
|
+
def decorate
|
|
64
|
+
root.create_path(constant) do |model|
|
|
65
|
+
model_mixins.each { |name| model.create_include(name) }
|
|
66
|
+
model_class_mixins.each { |name| model.create_extend(name) }
|
|
67
|
+
|
|
68
|
+
target_modules.each do |module_name, return_type|
|
|
69
|
+
model.create_module(module_name).create_method("friendly", return_type: return_type)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class << self
|
|
75
|
+
# @override
|
|
76
|
+
#: -> Enumerable[Module[top]]
|
|
77
|
+
def gather_constants
|
|
78
|
+
descendants_of(::ActiveRecord::Base)
|
|
79
|
+
.reject(&:abstract_class?)
|
|
80
|
+
.select { |model| model.singleton_class.include?(::FriendlyId::Base) }
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
#: -> Array[String]
|
|
87
|
+
def model_mixins
|
|
88
|
+
friendly_id_modules(constant.ancestors - ::Object.ancestors)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
#: -> Array[String]
|
|
92
|
+
def model_class_mixins
|
|
93
|
+
friendly_id_modules(constant.singleton_class.ancestors - ::Object.singleton_class.ancestors)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# `FriendlyId::ObjectUtils` and friends are mixed into `Object` when the gem is required, so they
|
|
97
|
+
# are already part of the gem RBI. Only the modules the model itself picked up are of interest here.
|
|
98
|
+
#: (Array[Module[top]] ancestors) -> Array[String]
|
|
99
|
+
def friendly_id_modules(ancestors)
|
|
100
|
+
ancestors.filter_map do |ancestor|
|
|
101
|
+
name = ancestor.name
|
|
102
|
+
next unless name
|
|
103
|
+
next unless name.start_with?("FriendlyId::")
|
|
104
|
+
|
|
105
|
+
name
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
#: -> Array[[String, String]]
|
|
110
|
+
def target_modules
|
|
111
|
+
if compiler_enabled?("ActiveRecordRelations")
|
|
112
|
+
[
|
|
113
|
+
[RelationMethodsModuleName, RelationClassName],
|
|
114
|
+
[AssociationRelationMethodsModuleName, AssociationRelationClassName],
|
|
115
|
+
]
|
|
116
|
+
else
|
|
117
|
+
[[RelationMethodsModuleName, "T.untyped"]]
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -33,10 +33,13 @@ module Tapioca
|
|
|
33
33
|
# include MoneyRailsGeneratedMethods
|
|
34
34
|
#
|
|
35
35
|
# module MoneyRailsGeneratedMethods
|
|
36
|
+
# sig { returns(::Money::Currency) }
|
|
37
|
+
# def currency_for_price; end
|
|
38
|
+
#
|
|
36
39
|
# sig { returns(::Money) }
|
|
37
40
|
# def price; end
|
|
38
41
|
#
|
|
39
|
-
# sig { params(value: ::Money).returns(::Money) }
|
|
42
|
+
# sig { params(value: T.any(::Money, ::Numeric, ::String)).returns(::Money) }
|
|
40
43
|
# def price=(value); end
|
|
41
44
|
# end
|
|
42
45
|
# end
|
|
@@ -44,6 +47,11 @@ module Tapioca
|
|
|
44
47
|
class MoneyRails < Tapioca::Dsl::Compiler
|
|
45
48
|
include RBIHelper
|
|
46
49
|
|
|
50
|
+
# `write_monetized` calls `value.to_money(...)` for anything that is not already a `Money`,
|
|
51
|
+
# so the writer accepts any object money-rails knows how to monetize, not only `Money`.
|
|
52
|
+
# https://github.com/RubyMoney/money-rails/blob/main/lib/money-rails/active_record/monetizable.rb
|
|
53
|
+
SetterValueType = "T.any(::Money, ::Numeric, ::String)"
|
|
54
|
+
|
|
47
55
|
ConstantType = type_member do
|
|
48
56
|
{
|
|
49
57
|
fixed: T.all(
|
|
@@ -90,8 +98,10 @@ module Tapioca
|
|
|
90
98
|
constant.monetized_attributes.each do |attribute_name, column_name|
|
|
91
99
|
if column_type_option.untyped?
|
|
92
100
|
type_name = "T.untyped"
|
|
101
|
+
setter_type_name = "T.untyped"
|
|
93
102
|
else
|
|
94
103
|
type_name = "::Money"
|
|
104
|
+
setter_type_name = SetterValueType
|
|
95
105
|
|
|
96
106
|
nilable_attribute = if constant < ::ActiveRecord::Base && column_type_option.persisted?
|
|
97
107
|
Boba::ActiveRecord::AttributeService.nilable_attribute?(
|
|
@@ -103,7 +113,10 @@ module Tapioca
|
|
|
103
113
|
true
|
|
104
114
|
end
|
|
105
115
|
|
|
106
|
-
|
|
116
|
+
if nilable_attribute
|
|
117
|
+
type_name = as_nilable_type(type_name)
|
|
118
|
+
setter_type_name = as_nilable_type(setter_type_name)
|
|
119
|
+
end
|
|
107
120
|
end
|
|
108
121
|
|
|
109
122
|
# Model: monetize :amount_cents
|
|
@@ -112,9 +125,13 @@ module Tapioca
|
|
|
112
125
|
instance_module.create_method(attribute_name, return_type: type_name)
|
|
113
126
|
instance_module.create_method(
|
|
114
127
|
"#{attribute_name}=",
|
|
115
|
-
parameters: [create_param("value", type:
|
|
128
|
+
parameters: [create_param("value", type: setter_type_name)],
|
|
116
129
|
return_type: type_name,
|
|
117
130
|
)
|
|
131
|
+
instance_module.create_method(
|
|
132
|
+
"currency_for_#{attribute_name}",
|
|
133
|
+
return_type: "::Money::Currency",
|
|
134
|
+
)
|
|
118
135
|
end
|
|
119
136
|
|
|
120
137
|
klass << instance_module
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
return unless defined?(PaperTrail)
|
|
5
|
+
|
|
6
|
+
module Tapioca
|
|
7
|
+
module Dsl
|
|
8
|
+
module Compilers
|
|
9
|
+
# `Tapioca::Dsl::Compilers::PaperTrail` decorates RBI files for models versioned with the `paper_trail`
|
|
10
|
+
# gem. https://github.com/paper-trail-gem/paper_trail
|
|
11
|
+
#
|
|
12
|
+
# `PaperTrail::Model` is included into `ActiveRecord::Base` once, by the gem itself, so a
|
|
13
|
+
# `sorbet/tapioca/require.rb` carrying `require "paper_trail/frameworks/active_record"` is enough for
|
|
14
|
+
# `has_paper_trail` to land in the gem RBI — no compiler needed for that part. What no shim can express
|
|
15
|
+
# is what `has_paper_trail` then does to the model that declares it, at runtime: it includes
|
|
16
|
+
# `PaperTrail::Model::InstanceMethods` into that class and defines two accessors on it, one of them
|
|
17
|
+
# under a name the model chooses.
|
|
18
|
+
#
|
|
19
|
+
# For example, with the following `ActiveRecord::Base` subclass:
|
|
20
|
+
#
|
|
21
|
+
# ~~~rb
|
|
22
|
+
# class Post < ApplicationRecord
|
|
23
|
+
# has_paper_trail
|
|
24
|
+
# end
|
|
25
|
+
# ~~~
|
|
26
|
+
#
|
|
27
|
+
# This compiler will produce the RBI file `post.rbi` with the following content:
|
|
28
|
+
#
|
|
29
|
+
# ~~~rbi
|
|
30
|
+
# # post.rbi
|
|
31
|
+
# # typed: true
|
|
32
|
+
# class Post
|
|
33
|
+
# include PaperTrail::Model::InstanceMethods
|
|
34
|
+
#
|
|
35
|
+
# sig { returns(T.nilable(::PaperTrail::Version)) }
|
|
36
|
+
# def version; end
|
|
37
|
+
#
|
|
38
|
+
# sig { params(value: T.nilable(::PaperTrail::Version)).returns(T.nilable(::PaperTrail::Version)) }
|
|
39
|
+
# def version=(value); end
|
|
40
|
+
#
|
|
41
|
+
# sig { returns(T.nilable(::String)) }
|
|
42
|
+
# def paper_trail_event; end
|
|
43
|
+
#
|
|
44
|
+
# sig { params(value: T.nilable(::String)).returns(T.nilable(::String)) }
|
|
45
|
+
# def paper_trail_event=(value); end
|
|
46
|
+
# end
|
|
47
|
+
# ~~~
|
|
48
|
+
#
|
|
49
|
+
# `version` holds the version record the model was reified from; it is named by the `:version` option
|
|
50
|
+
# and typed by `versions: { class_name: ... }`, so both are only knowable per model. The `versions`
|
|
51
|
+
# association is declared with `has_many`, which Tapioca's own association compiler already sees.
|
|
52
|
+
class PaperTrail < Tapioca::Dsl::Compiler
|
|
53
|
+
ConstantType = type_member { { fixed: T.class_of(::ActiveRecord::Base) } }
|
|
54
|
+
|
|
55
|
+
# @override
|
|
56
|
+
#: -> void
|
|
57
|
+
def decorate
|
|
58
|
+
version_class_name = T.unsafe(constant).version_class_name.to_s.delete_prefix("::")
|
|
59
|
+
reified_type = "T.nilable(::#{version_class_name})"
|
|
60
|
+
version_name = T.unsafe(constant).version_association_name.to_s
|
|
61
|
+
|
|
62
|
+
root.create_path(constant) do |model|
|
|
63
|
+
model.create_include("PaperTrail::Model::InstanceMethods")
|
|
64
|
+
create_accessor(model, version_name, reified_type)
|
|
65
|
+
create_accessor(model, "paper_trail_event", "T.nilable(::String)")
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
class << self
|
|
70
|
+
# @override
|
|
71
|
+
#: -> Enumerable[Module[top]]
|
|
72
|
+
def gather_constants
|
|
73
|
+
descendants_of(::ActiveRecord::Base)
|
|
74
|
+
.reject(&:abstract_class?)
|
|
75
|
+
.select { |model| model.include?(::PaperTrail::Model::InstanceMethods) }
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
#: (RBI::Scope model, String name, String type) -> void
|
|
82
|
+
def create_accessor(model, name, type)
|
|
83
|
+
model.create_method(name, return_type: type)
|
|
84
|
+
model.create_method(
|
|
85
|
+
"#{name}=",
|
|
86
|
+
parameters: [create_param("value", type: type)],
|
|
87
|
+
return_type: type,
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
return unless defined?(Ransack)
|
|
5
|
+
|
|
6
|
+
require "tapioca/dsl/helpers/active_record_constants_helper"
|
|
7
|
+
|
|
8
|
+
module Tapioca
|
|
9
|
+
module Dsl
|
|
10
|
+
module Compilers
|
|
11
|
+
# `Tapioca::Dsl::Compilers::Ransack` decorates RBI files for models searchable with the `ransack` gem.
|
|
12
|
+
# https://github.com/activerecord-hackery/ransack
|
|
13
|
+
#
|
|
14
|
+
# The class methods (`ransack`, `ransacker`, `ransackable_attributes`, ...) need no compiler: ransack
|
|
15
|
+
# extends `ActiveRecord::Base` from an `ActiveSupport.on_load(:active_record)` hook, and anything that
|
|
16
|
+
# loads `ActiveRecord::Base` during gem RBI generation fires it, so tapioca records the extend in the
|
|
17
|
+
# gem RBI. What stays invisible is the relation side: `ActiveRecord::Relation` reaches those methods by
|
|
18
|
+
# delegating to the model class, which Sorbet cannot follow.
|
|
19
|
+
#
|
|
20
|
+
# For example, with the following `ActiveRecord::Base` subclass:
|
|
21
|
+
#
|
|
22
|
+
# ~~~rb
|
|
23
|
+
# class Post < ApplicationRecord
|
|
24
|
+
# end
|
|
25
|
+
# ~~~
|
|
26
|
+
#
|
|
27
|
+
# This compiler will produce the RBI file `post.rbi` with the following content:
|
|
28
|
+
#
|
|
29
|
+
# ~~~rbi
|
|
30
|
+
# # post.rbi
|
|
31
|
+
# # typed: true
|
|
32
|
+
# class Post
|
|
33
|
+
# module GeneratedAssociationRelationMethods
|
|
34
|
+
# sig { params(params: T.untyped, options: T.untyped).returns(::Ransack::Search) }
|
|
35
|
+
# def ransack(params = nil, options = nil); end
|
|
36
|
+
#
|
|
37
|
+
# sig { params(params: T.untyped, options: T.untyped).returns(::Ransack::Search) }
|
|
38
|
+
# def ransack!(params = nil, options = nil); end
|
|
39
|
+
# end
|
|
40
|
+
#
|
|
41
|
+
# module GeneratedRelationMethods
|
|
42
|
+
# sig { params(params: T.untyped, options: T.untyped).returns(::Ransack::Search) }
|
|
43
|
+
# def ransack(params = nil, options = nil); end
|
|
44
|
+
#
|
|
45
|
+
# sig { params(params: T.untyped, options: T.untyped).returns(::Ransack::Search) }
|
|
46
|
+
# def ransack!(params = nil, options = nil); end
|
|
47
|
+
# end
|
|
48
|
+
# end
|
|
49
|
+
# ~~~
|
|
50
|
+
#
|
|
51
|
+
# The signatures are the ones the gem RBI already carries for
|
|
52
|
+
# `Ransack::Adapters::ActiveRecord::Base`; only the delegation is re-stated.
|
|
53
|
+
class Ransack < Tapioca::Dsl::Compiler
|
|
54
|
+
include Helpers::ActiveRecordConstantsHelper
|
|
55
|
+
|
|
56
|
+
ConstantType = type_member { { fixed: T.class_of(::ActiveRecord::Base) } }
|
|
57
|
+
|
|
58
|
+
# @override
|
|
59
|
+
#: -> void
|
|
60
|
+
def decorate
|
|
61
|
+
root.create_path(constant) do |model|
|
|
62
|
+
[RelationMethodsModuleName, AssociationRelationMethodsModuleName].each do |module_name|
|
|
63
|
+
relation_methods_module = model.create_module(module_name)
|
|
64
|
+
|
|
65
|
+
["ransack", "ransack!"].each do |method_name|
|
|
66
|
+
relation_methods_module.create_method(
|
|
67
|
+
method_name,
|
|
68
|
+
parameters: [
|
|
69
|
+
create_opt_param("params", type: "T.untyped", default: "nil"),
|
|
70
|
+
create_opt_param("options", type: "T.untyped", default: "nil"),
|
|
71
|
+
],
|
|
72
|
+
return_type: "::Ransack::Search",
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
class << self
|
|
80
|
+
# @override
|
|
81
|
+
#: -> Enumerable[Module[top]]
|
|
82
|
+
def gather_constants
|
|
83
|
+
descendants_of(::ActiveRecord::Base).reject(&:abstract_class?)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
return unless defined?(RubyLLM::ActiveRecord)
|
|
5
|
+
|
|
6
|
+
module Tapioca
|
|
7
|
+
module Dsl
|
|
8
|
+
module Compilers
|
|
9
|
+
# `Tapioca::Dsl::Compilers::RubyLLM` decorates RBI files for models using the `acts_as_*` DSL of the
|
|
10
|
+
# `ruby_llm` gem. https://github.com/crmne/ruby_llm
|
|
11
|
+
#
|
|
12
|
+
# The `acts_as_*` class methods themselves are out of reach of a compiler and of gem RBI generation
|
|
13
|
+
# alike: the gem includes `RubyLLM::ActiveRecord::ActsAs` into `ActiveRecord::Base` from an
|
|
14
|
+
# `on_load(:active_record)` hook inside a **railtie initializer**, so nothing short of booting the
|
|
15
|
+
# application runs it. Declare that one include in a shim, and the DSL types itself from the gem RBI:
|
|
16
|
+
#
|
|
17
|
+
# ~~~rbi
|
|
18
|
+
# # sorbet/rbi/shims/ruby_llm.rbi
|
|
19
|
+
# class ActiveRecord::Base
|
|
20
|
+
# include RubyLLM::ActiveRecord::ActsAs
|
|
21
|
+
# end
|
|
22
|
+
# ~~~
|
|
23
|
+
#
|
|
24
|
+
# What is left for a compiler is per model: the methods module each `acts_as_*` call mixes into the
|
|
25
|
+
# class that declares it.
|
|
26
|
+
#
|
|
27
|
+
# For example, with the following `ActiveRecord::Base` subclass:
|
|
28
|
+
#
|
|
29
|
+
# ~~~rb
|
|
30
|
+
# class Chat < ApplicationRecord
|
|
31
|
+
# acts_as_chat
|
|
32
|
+
# end
|
|
33
|
+
# ~~~
|
|
34
|
+
#
|
|
35
|
+
# This compiler will produce the RBI file `chat.rbi` with the following content:
|
|
36
|
+
#
|
|
37
|
+
# ~~~rbi
|
|
38
|
+
# # chat.rbi
|
|
39
|
+
# # typed: true
|
|
40
|
+
# class Chat
|
|
41
|
+
# include RubyLLM::ActiveRecord::ChatMethods
|
|
42
|
+
# end
|
|
43
|
+
# ~~~
|
|
44
|
+
#
|
|
45
|
+
# The module is declared rather than re-implemented, so `ask`, `with_instructions`,
|
|
46
|
+
# `with_runtime_instructions` and the rest keep the signatures they have in the gem RBI. Models that
|
|
47
|
+
# never call an `acts_as_*` method are left alone.
|
|
48
|
+
class RubyLLM < Tapioca::Dsl::Compiler
|
|
49
|
+
ConstantType = type_member { { fixed: T.class_of(::ActiveRecord::Base) } }
|
|
50
|
+
|
|
51
|
+
# One module per `acts_as_*`, under both the current and the legacy API; the names are fixed, so
|
|
52
|
+
# there is nothing to discover by walking ancestors with a name prefix.
|
|
53
|
+
MIXINS = [
|
|
54
|
+
"RubyLLM::ActiveRecord::ChatMethods",
|
|
55
|
+
"RubyLLM::ActiveRecord::MessageMethods",
|
|
56
|
+
"RubyLLM::ActiveRecord::ModelMethods",
|
|
57
|
+
"RubyLLM::ActiveRecord::ToolCallMethods",
|
|
58
|
+
"RubyLLM::ActiveRecord::ChatLegacyMethods",
|
|
59
|
+
"RubyLLM::ActiveRecord::MessageLegacyMethods",
|
|
60
|
+
] #: Array[String]
|
|
61
|
+
|
|
62
|
+
# @override
|
|
63
|
+
#: -> void
|
|
64
|
+
def decorate
|
|
65
|
+
mixins = self.class.mixins_of(constant)
|
|
66
|
+
|
|
67
|
+
root.create_path(constant) do |model|
|
|
68
|
+
mixins.each { |name| model.create_include(name) }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
class << self
|
|
73
|
+
# @override
|
|
74
|
+
#: -> Enumerable[Module[top]]
|
|
75
|
+
def gather_constants
|
|
76
|
+
descendants_of(::ActiveRecord::Base)
|
|
77
|
+
.reject(&:abstract_class?)
|
|
78
|
+
.select { |model| mixins_of(model).any? }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The DSL itself is included into `ActiveRecord::Base`, so every model carries it. What tells a
|
|
82
|
+
# model apart is the methods module an `acts_as_*` call mixed into the model itself.
|
|
83
|
+
#: (singleton(::ActiveRecord::Base) model) -> Array[String]
|
|
84
|
+
def mixins_of(model)
|
|
85
|
+
own_ancestors = model.ancestors - ::ActiveRecord::Base.ancestors
|
|
86
|
+
names = own_ancestors.filter_map(&:name)
|
|
87
|
+
|
|
88
|
+
MIXINS & names
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
metadata
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: boba
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
-
|
|
7
|
+
- Alexander Stathis
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
@@ -29,16 +29,16 @@ dependencies:
|
|
|
29
29
|
requirements:
|
|
30
30
|
- - "<="
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: 0.
|
|
32
|
+
version: 0.20.0
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - "<="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: 0.
|
|
39
|
+
version: 0.20.0
|
|
40
40
|
email:
|
|
41
|
-
-
|
|
41
|
+
- stathis.alexanderj@gmail.com
|
|
42
42
|
executables: []
|
|
43
43
|
extensions: []
|
|
44
44
|
extra_rdoc_files: []
|
|
@@ -51,26 +51,32 @@ files:
|
|
|
51
51
|
- lib/boba/options/association_type_option.rb
|
|
52
52
|
- lib/boba/relations_railtie.rb
|
|
53
53
|
- lib/boba/version.rb
|
|
54
|
+
- lib/tapioca/dsl/compilers/action_mcp_tool.rb
|
|
54
55
|
- lib/tapioca/dsl/compilers/active_record_associations_persisted.rb
|
|
55
56
|
- lib/tapioca/dsl/compilers/active_record_columns_persisted.rb
|
|
56
57
|
- lib/tapioca/dsl/compilers/active_record_relation_types.rb
|
|
58
|
+
- lib/tapioca/dsl/compilers/acts_as_taggable_on.rb
|
|
57
59
|
- lib/tapioca/dsl/compilers/attr_json.rb
|
|
58
60
|
- lib/tapioca/dsl/compilers/draper.rb
|
|
59
61
|
- lib/tapioca/dsl/compilers/flag_shih_tzu.rb
|
|
62
|
+
- lib/tapioca/dsl/compilers/friendly_id.rb
|
|
60
63
|
- lib/tapioca/dsl/compilers/kaminari.rb
|
|
61
64
|
- lib/tapioca/dsl/compilers/money_rails.rb
|
|
62
65
|
- lib/tapioca/dsl/compilers/noticed.rb
|
|
66
|
+
- lib/tapioca/dsl/compilers/paper_trail.rb
|
|
63
67
|
- lib/tapioca/dsl/compilers/paperclip.rb
|
|
68
|
+
- lib/tapioca/dsl/compilers/ransack.rb
|
|
69
|
+
- lib/tapioca/dsl/compilers/ruby_llm.rb
|
|
64
70
|
- lib/tapioca/dsl/compilers/shrine.rb
|
|
65
71
|
- lib/tapioca/dsl/compilers/state_machines_extended.rb
|
|
66
|
-
homepage: https://github.com/
|
|
72
|
+
homepage: https://github.com/stathis-alexander/boba
|
|
67
73
|
licenses:
|
|
68
74
|
- MIT
|
|
69
75
|
metadata:
|
|
70
|
-
bug_tracker_uri: https://github.com/
|
|
71
|
-
changelog_uri: https://github.com/
|
|
72
|
-
homepage_uri: https://github.com/
|
|
73
|
-
source_code_uri: https://github.com/
|
|
76
|
+
bug_tracker_uri: https://github.com/stathis-alexander/boba/issues
|
|
77
|
+
changelog_uri: https://github.com/stathis-alexander/boba/blob/0.2.0/History.md
|
|
78
|
+
homepage_uri: https://github.com/stathis-alexander/boba
|
|
79
|
+
source_code_uri: https://github.com/stathis-alexander/boba/tree/0.2.0
|
|
74
80
|
rubygems_mfa_required: 'true'
|
|
75
81
|
rdoc_options: []
|
|
76
82
|
require_paths:
|