yogurt 0.1.1
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 +7 -0
- data/.DS_Store +0 -0
- data/.github/workflows/tests.yml +117 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.rubocop.yml +113 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +0 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +82 -0
- data/LICENSE +201 -0
- data/README.md +38 -0
- data/Rakefile +9 -0
- data/bin/bundle +105 -0
- data/bin/byebug +29 -0
- data/bin/coderay +29 -0
- data/bin/console +16 -0
- data/bin/htmldiff +29 -0
- data/bin/ldiff +29 -0
- data/bin/pry +29 -0
- data/bin/rake +29 -0
- data/bin/rspec +29 -0
- data/bin/rubocop +29 -0
- data/bin/ruby-parse +29 -0
- data/bin/ruby-rewrite +29 -0
- data/bin/setup +8 -0
- data/bin/srb +29 -0
- data/bin/srb-rbi +29 -0
- data/lib/yogurt.rb +95 -0
- data/lib/yogurt/code_generator.rb +706 -0
- data/lib/yogurt/code_generator/defined_class.rb +22 -0
- data/lib/yogurt/code_generator/defined_class_sorter.rb +44 -0
- data/lib/yogurt/code_generator/defined_method.rb +24 -0
- data/lib/yogurt/code_generator/enum_class.rb +56 -0
- data/lib/yogurt/code_generator/field_access_method.rb +273 -0
- data/lib/yogurt/code_generator/field_access_path.rb +56 -0
- data/lib/yogurt/code_generator/generated_file.rb +53 -0
- data/lib/yogurt/code_generator/input_class.rb +52 -0
- data/lib/yogurt/code_generator/leaf_class.rb +68 -0
- data/lib/yogurt/code_generator/operation_declaration.rb +12 -0
- data/lib/yogurt/code_generator/root_class.rb +130 -0
- data/lib/yogurt/code_generator/type_wrapper.rb +13 -0
- data/lib/yogurt/code_generator/typed_input.rb +12 -0
- data/lib/yogurt/code_generator/typed_output.rb +16 -0
- data/lib/yogurt/code_generator/utils.rb +140 -0
- data/lib/yogurt/code_generator/variable_definition.rb +33 -0
- data/lib/yogurt/converters.rb +50 -0
- data/lib/yogurt/error_result.rb +30 -0
- data/lib/yogurt/http.rb +80 -0
- data/lib/yogurt/inspectable.rb +22 -0
- data/lib/yogurt/memoize.rb +33 -0
- data/lib/yogurt/query.rb +23 -0
- data/lib/yogurt/query_container.rb +89 -0
- data/lib/yogurt/query_container/interfaces_and_unions_have_typename.rb +107 -0
- data/lib/yogurt/query_declaration.rb +11 -0
- data/lib/yogurt/query_executor.rb +23 -0
- data/lib/yogurt/query_result.rb +25 -0
- data/lib/yogurt/scalar_converter.rb +19 -0
- data/lib/yogurt/unexpected_object_type.rb +30 -0
- data/lib/yogurt/validation_error.rb +6 -0
- data/lib/yogurt/version.rb +6 -0
- data/sorbet/config +10 -0
- data/sorbet/rbi/fake_schema.rbi +14 -0
- data/sorbet/rbi/graphql.rbi +11 -0
- data/sorbet/rbi/hidden-definitions/errors.txt +20513 -0
- data/sorbet/rbi/hidden-definitions/hidden.rbi +42882 -0
- data/sorbet/rbi/sorbet-typed/lib/graphql/all/graphql.rbi +48 -0
- data/sorbet/rbi/sorbet-typed/lib/rainbow/all/rainbow.rbi +276 -0
- data/sorbet/rbi/sorbet-typed/lib/rubocop/~>0.85/rubocop.rbi +2072 -0
- data/sorbet/rbi/todo.rbi +6 -0
- data/yogurt.gemspec +54 -0
- metadata +286 -0
@@ -0,0 +1,53 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Yogurt
|
5
|
+
class CodeGenerator
|
6
|
+
class GeneratedFile < T::Struct
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
class FileType < T::Enum
|
10
|
+
enums do
|
11
|
+
# File contains the definition of a GraphQL Enum
|
12
|
+
ENUM = new('enum')
|
13
|
+
|
14
|
+
# File contains the definition of a GraphQL operation (ie, a root query result)
|
15
|
+
OPERATION = new('operation')
|
16
|
+
|
17
|
+
# File contains the definition of a GraphQL object result (ie, a non-root query result)
|
18
|
+
OBJECT_RESULT = new('object_result')
|
19
|
+
|
20
|
+
# File contains the definition of a GraphQL input object
|
21
|
+
INPUT_OBJECT = new('input_object')
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# The name of the constant that is stored in this file
|
26
|
+
const :constant_name, String
|
27
|
+
|
28
|
+
# Type of constant that is stored in this file
|
29
|
+
const :type, FileType
|
30
|
+
|
31
|
+
# Names of the constants that this file references. If you are not using an
|
32
|
+
# autoloading tool, the files containing these constants need to be `require`'d
|
33
|
+
# at the start of the file.
|
34
|
+
const :dependencies, T::Array[String]
|
35
|
+
|
36
|
+
# The code that defines the constant that is stored in this file
|
37
|
+
const :code, String
|
38
|
+
|
39
|
+
# Full contents of the file
|
40
|
+
sig {returns(String)}
|
41
|
+
def contents
|
42
|
+
<<~STRING
|
43
|
+
# typed: strict
|
44
|
+
# frozen_string_literal: true
|
45
|
+
|
46
|
+
require 'pp'
|
47
|
+
|
48
|
+
#{code}
|
49
|
+
STRING
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Yogurt
|
5
|
+
class CodeGenerator
|
6
|
+
# Input classes are used for input objects
|
7
|
+
class InputClass < T::Struct
|
8
|
+
extend T::Sig
|
9
|
+
include Utils
|
10
|
+
include DefinedClass
|
11
|
+
|
12
|
+
const :name, String
|
13
|
+
const :arguments, T::Array[VariableDefinition]
|
14
|
+
|
15
|
+
sig {override.returns(T::Array[String])}
|
16
|
+
def dependencies
|
17
|
+
arguments.map(&:dependency).compact
|
18
|
+
end
|
19
|
+
|
20
|
+
sig {override.returns(String)}
|
21
|
+
def to_ruby
|
22
|
+
extract = []
|
23
|
+
props = []
|
24
|
+
serializers = []
|
25
|
+
arguments.sort.each do |definition|
|
26
|
+
props << "prop #{definition.name.inspect}, #{definition.signature}"
|
27
|
+
extract << "#{definition.name} = self.#{definition.name}"
|
28
|
+
serializers.push(<<~STRING.strip)
|
29
|
+
#{definition.graphql_name.inspect} => #{definition.serializer.strip},
|
30
|
+
STRING
|
31
|
+
end
|
32
|
+
|
33
|
+
<<~STRING
|
34
|
+
class #{name} < T::Struct
|
35
|
+
extend T::Sig
|
36
|
+
|
37
|
+
#{indent(props.join("\n"), 1).strip}
|
38
|
+
|
39
|
+
sig {returns(T::Hash[String, T.untyped])}
|
40
|
+
def serialize
|
41
|
+
#{indent(extract.join("\n"), 2).strip}
|
42
|
+
|
43
|
+
{
|
44
|
+
#{indent(serializers.join("\n"), 3).strip}
|
45
|
+
}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
STRING
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Yogurt
|
5
|
+
class CodeGenerator
|
6
|
+
# Leaf classes are generated for the inner types of query results.
|
7
|
+
class LeafClass < T::Struct
|
8
|
+
include DefinedClass
|
9
|
+
extend T::Sig
|
10
|
+
include Utils
|
11
|
+
|
12
|
+
const :name, String
|
13
|
+
const :schema, GRAPHQL_SCHEMA
|
14
|
+
const :graphql_type, T.untyped # rubocop:disable Sorbet/ForbidUntypedStructProps
|
15
|
+
prop :defined_methods, T::Array[DefinedMethod]
|
16
|
+
prop :dependencies, T::Array[String]
|
17
|
+
|
18
|
+
# Adds the defined methods to the ones that are already defined in the class
|
19
|
+
sig {params(extra_methods: T::Array[DefinedMethod]).void}
|
20
|
+
def merge_defined_methods(extra_methods)
|
21
|
+
own_methods = defined_methods.map {|dm| [dm.name, dm]}.to_h
|
22
|
+
extra_methods.each do |extra|
|
23
|
+
own = own_methods[extra.name]
|
24
|
+
if own.nil?
|
25
|
+
own_methods[extra.name] = extra
|
26
|
+
elsif !own.merge?(extra)
|
27
|
+
raise "Cannot merge method #{extra.inspect} into #{own.inspect}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
self.defined_methods = own_methods.values
|
32
|
+
end
|
33
|
+
|
34
|
+
sig {override.returns(String)}
|
35
|
+
def to_ruby
|
36
|
+
pretty_print = generate_pretty_print(defined_methods)
|
37
|
+
|
38
|
+
dynamic_methods = <<~STRING.strip
|
39
|
+
#{defined_methods.map(&:to_ruby).join("\n")}
|
40
|
+
#{pretty_print}
|
41
|
+
STRING
|
42
|
+
|
43
|
+
<<~STRING
|
44
|
+
class #{name}
|
45
|
+
extend T::Sig
|
46
|
+
include Yogurt::QueryResult
|
47
|
+
|
48
|
+
#{indent(possible_types_constant(schema, graphql_type), 1).strip}
|
49
|
+
|
50
|
+
sig {params(result: Yogurt::OBJECT_TYPE).void}
|
51
|
+
def initialize(result)
|
52
|
+
@result = T.let(result, Yogurt::OBJECT_TYPE)
|
53
|
+
end
|
54
|
+
|
55
|
+
sig {override.returns(Yogurt::OBJECT_TYPE)}
|
56
|
+
def raw_result
|
57
|
+
@result
|
58
|
+
end
|
59
|
+
|
60
|
+
#{indent(typename_method(schema, graphql_type), 1).strip}
|
61
|
+
|
62
|
+
#{indent(dynamic_methods, 1).strip}
|
63
|
+
end
|
64
|
+
STRING
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Yogurt
|
5
|
+
class CodeGenerator
|
6
|
+
class OperationDeclaration < T::Struct
|
7
|
+
const :declaration, QueryDeclaration
|
8
|
+
const :operation_name, String
|
9
|
+
const :variables, T::Array[GraphQL::Language::Nodes::VariableDefinition]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Yogurt
|
5
|
+
class CodeGenerator
|
6
|
+
# Root classes are generated for the root of a GraphQL query.
|
7
|
+
class RootClass < T::Struct
|
8
|
+
include DefinedClass
|
9
|
+
extend T::Sig
|
10
|
+
include Utils
|
11
|
+
|
12
|
+
const :name, String
|
13
|
+
const :schema, GRAPHQL_SCHEMA
|
14
|
+
const :operation_name, String
|
15
|
+
const :graphql_type, T.untyped # rubocop:disable Sorbet/ForbidUntypedStructProps
|
16
|
+
const :query_container, QueryContainer::CONTAINER
|
17
|
+
const :defined_methods, T::Array[DefinedMethod]
|
18
|
+
const :variables, T::Array[VariableDefinition]
|
19
|
+
const :dependencies, T::Array[String]
|
20
|
+
|
21
|
+
sig {override.returns(String)}
|
22
|
+
def to_ruby
|
23
|
+
pretty_print = generate_pretty_print(defined_methods)
|
24
|
+
declaration = query_container.fetch_query(operation_name)
|
25
|
+
original_query = declaration.query_text
|
26
|
+
parsed_query = GraphQL.parse(original_query)
|
27
|
+
reprinted_query = GraphQL::Language::Printer.new.print(parsed_query)
|
28
|
+
|
29
|
+
dynamic_methods = <<~STRING.strip
|
30
|
+
#{defined_methods.map(&:to_ruby).join("\n")}
|
31
|
+
#{pretty_print}
|
32
|
+
STRING
|
33
|
+
|
34
|
+
<<~STRING
|
35
|
+
class #{name} < Yogurt::Query
|
36
|
+
extend T::Sig
|
37
|
+
include Yogurt::QueryResult
|
38
|
+
include Yogurt::ErrorResult
|
39
|
+
|
40
|
+
SCHEMA = T.let(#{schema.name}, Yogurt::GRAPHQL_SCHEMA)
|
41
|
+
OPERATION_NAME = T.let(#{operation_name.inspect}, String)
|
42
|
+
QUERY_TEXT = T.let(<<~'GRAPHQL', String)
|
43
|
+
#{indent(reprinted_query, 2).strip}
|
44
|
+
GRAPHQL
|
45
|
+
#{indent(possible_types_constant(schema, graphql_type), 1).strip}
|
46
|
+
|
47
|
+
#{indent(execute_method, 1).strip}
|
48
|
+
|
49
|
+
sig {params(data: Yogurt::OBJECT_TYPE, errors: T.nilable(T::Array[Yogurt::OBJECT_TYPE])).void}
|
50
|
+
def initialize(data, errors)
|
51
|
+
@result = T.let(data, Yogurt::OBJECT_TYPE)
|
52
|
+
@errors = T.let(errors, T.nilable(T::Array[Yogurt::OBJECT_TYPE]))
|
53
|
+
end
|
54
|
+
|
55
|
+
sig {override.returns(Yogurt::OBJECT_TYPE)}
|
56
|
+
def raw_result
|
57
|
+
@result
|
58
|
+
end
|
59
|
+
|
60
|
+
#{indent(typename_method(schema, graphql_type), 1).strip}
|
61
|
+
|
62
|
+
sig {override.returns(T.nilable(T::Array[Yogurt::OBJECT_TYPE]))}
|
63
|
+
def errors
|
64
|
+
@errors
|
65
|
+
end
|
66
|
+
|
67
|
+
#{indent(dynamic_methods, 1).strip}
|
68
|
+
end
|
69
|
+
STRING
|
70
|
+
end
|
71
|
+
|
72
|
+
sig {returns(String)}
|
73
|
+
def execute_method
|
74
|
+
executor = Yogurt.registered_schemas.fetch(schema)
|
75
|
+
options_type = executor.options_type_alias.name
|
76
|
+
signature_params = ["options: #{options_type}"]
|
77
|
+
|
78
|
+
params = if options_type.start_with?("T.nilable", "T.untyped")
|
79
|
+
["options=nil"]
|
80
|
+
else
|
81
|
+
["options"]
|
82
|
+
end
|
83
|
+
|
84
|
+
variable_extraction = if variables.any?
|
85
|
+
serializers = []
|
86
|
+
|
87
|
+
variables.sort.each do |variable|
|
88
|
+
if variable.signature.start_with?("T.nilable")
|
89
|
+
params.push("#{variable.name}: nil")
|
90
|
+
else
|
91
|
+
params.push("#{variable.name}:")
|
92
|
+
end
|
93
|
+
|
94
|
+
signature_params.push("#{variable.name}: #{variable.signature}")
|
95
|
+
serializers.push(<<~STRING.strip)
|
96
|
+
#{variable.graphql_name.inspect} => #{variable.serializer.strip},
|
97
|
+
STRING
|
98
|
+
end
|
99
|
+
|
100
|
+
<<~STRING
|
101
|
+
{
|
102
|
+
#{indent(serializers.join("\n"), 1).strip}
|
103
|
+
}
|
104
|
+
STRING
|
105
|
+
else
|
106
|
+
"nil"
|
107
|
+
end
|
108
|
+
|
109
|
+
<<~STRING
|
110
|
+
sig do
|
111
|
+
params(
|
112
|
+
#{indent(signature_params.join(",\n"), 2).strip}
|
113
|
+
).returns(T.any(T.attached_class, Yogurt::ErrorResult::OnlyErrors))
|
114
|
+
end
|
115
|
+
def self.execute(#{params.join(', ')})
|
116
|
+
raw_result = Yogurt.execute(
|
117
|
+
query: QUERY_TEXT,
|
118
|
+
schema: SCHEMA,
|
119
|
+
operation_name: OPERATION_NAME,
|
120
|
+
options: options,
|
121
|
+
variables: #{indent(variable_extraction, 2).strip}
|
122
|
+
)
|
123
|
+
|
124
|
+
from_result(raw_result)
|
125
|
+
end
|
126
|
+
STRING
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Yogurt
|
5
|
+
class CodeGenerator
|
6
|
+
class TypedOutput < T::Struct
|
7
|
+
# The signature to put in the sorbet type
|
8
|
+
const :signature, String
|
9
|
+
|
10
|
+
# Converter function to use for the return result.
|
11
|
+
# This assumes that a local variable named `raw_value` has the
|
12
|
+
# value to be converted.
|
13
|
+
const :deserializer, String
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Yogurt
|
5
|
+
class CodeGenerator
|
6
|
+
module Utils
|
7
|
+
extend T::Sig
|
8
|
+
extend self
|
9
|
+
|
10
|
+
sig {params(schema: GRAPHQL_SCHEMA, graphql_type: T.untyped).returns(String)}
|
11
|
+
def typename_method(schema, graphql_type)
|
12
|
+
possible_types = schema.possible_types(graphql_type)
|
13
|
+
|
14
|
+
if possible_types.size == 1
|
15
|
+
<<~STRING
|
16
|
+
sig {override.returns(String)}
|
17
|
+
def __typename
|
18
|
+
#{possible_types.fetch(0).graphql_name.inspect}
|
19
|
+
end
|
20
|
+
STRING
|
21
|
+
else
|
22
|
+
<<~STRING
|
23
|
+
sig {override.returns(String)}
|
24
|
+
def __typename
|
25
|
+
raw_result["__typename"]
|
26
|
+
end
|
27
|
+
STRING
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
sig {params(schema: GRAPHQL_SCHEMA, graphql_type: T.untyped).returns(String)}
|
32
|
+
def possible_types_constant(schema, graphql_type)
|
33
|
+
possible_types = schema
|
34
|
+
.possible_types(graphql_type)
|
35
|
+
.map(&:graphql_name)
|
36
|
+
.sort
|
37
|
+
.map(&:inspect)
|
38
|
+
|
39
|
+
single_line = possible_types.join(', ')
|
40
|
+
if single_line.size <= 80
|
41
|
+
<<~STRING.strip
|
42
|
+
POSSIBLE_TYPES = T.let(
|
43
|
+
[#{single_line}],
|
44
|
+
T::Array[String]
|
45
|
+
)
|
46
|
+
STRING
|
47
|
+
else
|
48
|
+
multi_line = possible_types.join(",\n")
|
49
|
+
<<~STRING.strip
|
50
|
+
POSSIBLE_TYPES = T.let(
|
51
|
+
[
|
52
|
+
#{indent(multi_line, 2).strip}
|
53
|
+
].freeze,
|
54
|
+
T::Array[String]
|
55
|
+
)
|
56
|
+
STRING
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
sig {params(camel_cased_word: String).returns(String)}
|
61
|
+
def underscore(camel_cased_word)
|
62
|
+
return camel_cased_word unless /[A-Z-]|::/.match?(camel_cased_word)
|
63
|
+
|
64
|
+
word = camel_cased_word.to_s.gsub("::", "/")
|
65
|
+
word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
|
66
|
+
word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
67
|
+
word.tr!("-", "_")
|
68
|
+
word.downcase!
|
69
|
+
word
|
70
|
+
end
|
71
|
+
|
72
|
+
sig {params(term: String).returns(String)}
|
73
|
+
def camelize(term)
|
74
|
+
string = term.to_s
|
75
|
+
string = string.sub(/^[a-z\d]*/, &:capitalize)
|
76
|
+
string.gsub!(%r{(?:_|(/))([a-z\d]*)}i) {"#{Regexp.last_match(1)}#{T.must(Regexp.last_match(2)).capitalize}"}
|
77
|
+
string.gsub!("/", "::")
|
78
|
+
string
|
79
|
+
end
|
80
|
+
|
81
|
+
sig {params(string: String, amount: Integer).returns(String)}
|
82
|
+
def indent(string, amount)
|
83
|
+
return string if amount.zero?
|
84
|
+
|
85
|
+
padding = ' ' * amount
|
86
|
+
|
87
|
+
buffer = T.unsafe(String).new("", capacity: string.size)
|
88
|
+
string.each_line do |line|
|
89
|
+
buffer << padding if line.size > 1 || line != "\n"
|
90
|
+
buffer << line
|
91
|
+
end
|
92
|
+
|
93
|
+
buffer
|
94
|
+
end
|
95
|
+
|
96
|
+
sig {params(desired_name: String).returns(Symbol)}
|
97
|
+
def generate_method_name(desired_name)
|
98
|
+
base_desired_name = desired_name
|
99
|
+
escaping_level = 0
|
100
|
+
|
101
|
+
while PROTECTED_NAMES.include?(desired_name)
|
102
|
+
escaping_level += 1
|
103
|
+
desired_name = "#{base_desired_name}#{'_' * escaping_level}"
|
104
|
+
end
|
105
|
+
|
106
|
+
desired_name.to_sym
|
107
|
+
end
|
108
|
+
|
109
|
+
sig {params(methods: T::Array[DefinedMethod]).returns(String)}
|
110
|
+
def generate_pretty_print(methods)
|
111
|
+
inspect_lines = methods.map do |dm|
|
112
|
+
<<~STRING
|
113
|
+
p.comma_breakable
|
114
|
+
p.text(#{dm.name.to_s.inspect})
|
115
|
+
p.text(': ')
|
116
|
+
p.pp(#{dm.name})
|
117
|
+
STRING
|
118
|
+
end
|
119
|
+
|
120
|
+
object_group = <<~STRING.strip
|
121
|
+
p.breakable
|
122
|
+
p.text('__typename')
|
123
|
+
p.text(': ')
|
124
|
+
p.pp(__typename)
|
125
|
+
|
126
|
+
#{inspect_lines.join("\n\n")}
|
127
|
+
STRING
|
128
|
+
|
129
|
+
<<~STRING
|
130
|
+
sig {override.params(p: PP::PPMethods).void}
|
131
|
+
def pretty_print(p)
|
132
|
+
p.object_group(self) do
|
133
|
+
#{indent(object_group, 2).strip}
|
134
|
+
end
|
135
|
+
end
|
136
|
+
STRING
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|