fragment-dev 1.4.6 → 2.1.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.
@@ -0,0 +1,169 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ return unless defined?(Tapioca::Dsl::Compilers)
5
+
6
+ require 'fragment_client'
7
+ require 'tapioca/dsl/helpers/graphql_sorbet_types'
8
+
9
+ module Tapioca
10
+ module Dsl
11
+ module Compilers
12
+ # Declares the typed payload classes {FragmentClient::TypedEntries.load}
13
+ # builds at load time, giving each a real `initialize` signature and typed
14
+ # readers.
15
+ #
16
+ # `bundle exec tapioca dsl` writes them to
17
+ # `sorbet/rbi/dsl/fragment_client/entries/`. The classes are only visible if
18
+ # whatever calls `TypedEntries.load` runs when Tapioca loads the application;
19
+ # it needs no credentials, so an initializer works.
20
+ class FragmentTypedEntries < Compiler
21
+ extend T::Sig
22
+
23
+ ConstantType = type_member { { fixed: T.class_of(FragmentClient::TypedLedgerEntry) } }
24
+
25
+ # Sorbet types for the scalars a Fragment Schema binds parameters to.
26
+ # Anything absent -- enums, input objects, newer scalars -- falls through to
27
+ # `T.untyped` rather than a guess that could reject a valid call.
28
+ GRAPHQL_SCALARS = T.let(
29
+ {
30
+ 'String' => '::String',
31
+ 'ID' => '::String',
32
+ 'SafeString' => '::String',
33
+ 'ParameterizedString' => '::String',
34
+ # ISO 8601 strings, not Ruby date objects.
35
+ 'Date' => '::String',
36
+ 'DateTime' => '::String',
37
+ 'FirstMoment' => '::String',
38
+ 'LastMoment' => '::String',
39
+ 'Period' => '::String',
40
+ 'PeriodFilter' => '::String',
41
+ 'UTCOffset' => '::String',
42
+ # Fragment's big integers are strings on the wire.
43
+ 'Int96' => '::String',
44
+ 'Int' => '::Integer',
45
+ 'Float' => '::Float',
46
+ 'Boolean' => 'T::Boolean',
47
+ 'JSON' => 'T.untyped',
48
+ 'Parameters' => 'T.untyped'
49
+ }.freeze,
50
+ T::Hash[String, String]
51
+ )
52
+
53
+ # The optional common fields, in the order the base class declares them.
54
+ COMMON_OPTIONAL_FIELDS = T.let(
55
+ {
56
+ posted: '::String',
57
+ description: '::String',
58
+ tags: 'T::Array[T.untyped]',
59
+ groups: 'T::Array[T.untyped]',
60
+ conditions: 'T::Array[T.untyped]'
61
+ }.freeze,
62
+ T::Hash[Symbol, String]
63
+ )
64
+
65
+ class << self
66
+ extend T::Sig
67
+
68
+ sig { override.returns(T::Enumerable[Module]) }
69
+ def gather_constants
70
+ # `name_of` is nil for a payload in an anonymous namespace, whose name
71
+ # (`#<Module:0x...>::AuthCaptureV1`) Sorbet cannot resolve anyway.
72
+ descendants_of(FragmentClient::TypedLedgerEntry).select { |klass| name_of(klass) }
73
+ end
74
+ end
75
+
76
+ sig { override.void }
77
+ def decorate
78
+ spec = constant.spec
79
+
80
+ # `create_path` would omit the superclass, leaving Sorbet unaware that a
81
+ # payload inherits `to_entry_input`, `set?` and the common-field readers.
82
+ root.create_class(constant.to_s,
83
+ superclass_name: '::FragmentClient::TypedLedgerEntry') do |payload|
84
+ payload.create_method('initialize', parameters: initialize_parameters(spec),
85
+ return_type: 'void')
86
+
87
+ spec.parameters.each do |parameter|
88
+ payload.create_method(parameter.name.to_s, return_type: reader_type(parameter),
89
+ comments: parameter_comments(parameter))
90
+ end
91
+
92
+ payload.create_method('entry_type', return_type: '::String', class_method: true,
93
+ comments: [comment(spec.entry_type.inspect)])
94
+ payload.create_method('type_version', return_type: '::Integer', class_method: true,
95
+ comments: [comment(spec.type_version.to_s)])
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ sig do
102
+ params(spec: FragmentClient::TypedEntries::EntrySpec)
103
+ .returns(T::Array[RBI::TypedParam])
104
+ end
105
+ def initialize_parameters(spec)
106
+ required, optional = spec.parameters.partition(&:required)
107
+
108
+ # Required before optional: Sorbet rejects a `sig` that interleaves them,
109
+ # though Ruby permits it for keyword arguments. Source order survives
110
+ # within each group and on the wire, which is where spec 2.4 applies.
111
+ parameters = [
112
+ create_kw_param('ik', type: '::String'),
113
+ create_kw_param('ledger_ik', type: '::String')
114
+ ]
115
+ required.each do |parameter|
116
+ parameters << create_kw_param(parameter.name.to_s, type: sorbet_type(parameter))
117
+ end
118
+ optional.each do |parameter|
119
+ parameters << create_kw_opt_param(parameter.name.to_s, type: optional_type(parameter),
120
+ default: 'T.unsafe(nil)')
121
+ end
122
+ COMMON_OPTIONAL_FIELDS.each do |name, type|
123
+ parameters << create_kw_opt_param(name.to_s, type: nilable(type),
124
+ default: 'T.unsafe(nil)')
125
+ end
126
+
127
+ parameters
128
+ end
129
+
130
+ sig { params(parameter: FragmentClient::TypedEntries::Parameter).returns(String) }
131
+ def reader_type(parameter)
132
+ parameter.required ? sorbet_type(parameter) : optional_type(parameter)
133
+ end
134
+
135
+ sig { params(parameter: FragmentClient::TypedEntries::Parameter).returns(String) }
136
+ def optional_type(parameter)
137
+ nilable(sorbet_type(parameter))
138
+ end
139
+
140
+ # Optional fields are plainly nilable: the unset sentinel is private to the
141
+ # constructor and never reaches a caller.
142
+ sig { params(type: String).returns(String) }
143
+ def nilable(type)
144
+ type == 'T.untyped' ? 'T.untyped' : "T.nilable(#{type})"
145
+ end
146
+
147
+ sig { params(parameter: FragmentClient::TypedEntries::Parameter).returns(String) }
148
+ def sorbet_type(parameter)
149
+ Helpers::GraphqlSorbetTypes.translate(parameter.graphql_type, scalars: GRAPHQL_SCALARS)
150
+ end
151
+
152
+ sig { params(parameter: FragmentClient::TypedEntries::Parameter).returns(T::Array[RBI::Comment]) }
153
+ def parameter_comments(parameter)
154
+ text = "Schema parameter `#{parameter.wire_name}` (`#{parameter.graphql_type}`)."
155
+ if parameter.escaped?
156
+ text += " Exposed as `#{parameter.name}` because `#{parameter.wire_name}` is " \
157
+ 'already taken; the wire name is unchanged.'
158
+ end
159
+ [comment(text)]
160
+ end
161
+
162
+ sig { params(text: String).returns(RBI::Comment) }
163
+ def comment(text)
164
+ RBI::Comment.new(text)
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,53 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ module Dsl
6
+ module Helpers
7
+ # Translates a GraphQL type expression, as written in a document, into a
8
+ # Sorbet type string.
9
+ #
10
+ # Nothing here knows about Fragment: the scalar table is supplied by the
11
+ # caller, and anything absent from it becomes `T.untyped`. Kept separate so
12
+ # it can move to a gem of its own if a second consumer appears -- no gem
13
+ # does this today, and Tapioca ships no GraphQL compiler.
14
+ #
15
+ # translate('[SafeString!]!', scalars: { 'SafeString' => '::String' })
16
+ # #=> "T::Array[::String]"
17
+ # translate('[String]', scalars: { 'String' => '::String' })
18
+ # #=> "T::Array[T.nilable(::String)]"
19
+ #
20
+ # Top-level nullability is the caller's to apply, since only the caller knows
21
+ # whether an optional field is nilable, defaulted, or something else.
22
+ # Nullability inside a list is applied here, having nowhere else to go.
23
+ module GraphqlSorbetTypes
24
+ extend T::Sig
25
+
26
+ UNTYPED = 'T.untyped'
27
+
28
+ class << self
29
+ extend T::Sig
30
+
31
+ sig { params(graphql_type: String, scalars: T::Hash[String, String]).returns(String) }
32
+ def translate(graphql_type, scalars:)
33
+ type = graphql_type.delete_suffix('!')
34
+ return scalars.fetch(type, UNTYPED) unless type.start_with?('[')
35
+
36
+ element = type.delete_prefix('[').delete_suffix(']')
37
+ "T::Array[#{list_element(element, scalars: scalars)}]"
38
+ end
39
+
40
+ private
41
+
42
+ sig { params(element: String, scalars: T::Hash[String, String]).returns(String) }
43
+ def list_element(element, scalars:)
44
+ type = translate(element, scalars: scalars)
45
+ return type if element.end_with?('!') || type == UNTYPED
46
+
47
+ "T.nilable(#{type})"
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fragment-dev
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.6
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - fragment
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-03 00:00:00.000000000 Z
11
+ date: 2026-08-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -67,8 +67,15 @@ files:
67
67
  - lib/fragment.schema.json
68
68
  - lib/fragment_client.rb
69
69
  - lib/fragment_client.rbi
70
+ - lib/fragment_client/graphql_ast.rb
71
+ - lib/fragment_client/typed_entries.rb
72
+ - lib/fragment_client/typed_ledger_entry.rb
70
73
  - lib/fragment_client/version.rb
71
74
  - lib/queries.graphql
75
+ - lib/tapioca/dsl/compilers/fragment_query_methods.rb
76
+ - lib/tapioca/dsl/compilers/fragment_response_types.rb
77
+ - lib/tapioca/dsl/compilers/fragment_typed_entries.rb
78
+ - lib/tapioca/dsl/helpers/graphql_sorbet_types.rb
72
79
  homepage: https://fragment.dev
73
80
  licenses:
74
81
  - Apache-2.0