zero-rails_openapi 1.3.2 → 1.3.3
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/Gemfile +0 -2
- data/Gemfile.lock +90 -3
- data/README.md +409 -357
- data/documentation/examples/examples_controller.rb +2 -2
- data/documentation/examples/goods_doc.rb +14 -12
- data/documentation/examples/open_api.rb +14 -4
- data/documentation/examples/{example_output_doc.json → output_example.json} +0 -0
- data/documentation/parameter.md +17 -12
- data/lib/oas_objs/schema_obj.rb +1 -1
- data/lib/open_api/config.rb +18 -18
- data/lib/open_api/config_dsl.rb +21 -2
- data/lib/open_api/dsl.rb +12 -13
- data/lib/open_api/dsl/api_info_obj.rb +27 -2
- data/lib/open_api/dsl/common_dsl.rb +1 -0
- data/lib/open_api/dsl/ctrl_info_obj.rb +1 -1
- data/lib/open_api/dsl/helpers.rb +4 -4
- data/lib/open_api/generator.rb +16 -5
- data/lib/open_api/version.rb +1 -1
- data/zero-rails_openapi.gemspec +3 -9
- metadata +31 -3
@@ -23,9 +23,9 @@ class Api::V1::ExamplesController < Api::V1::BaseController
|
|
23
23
|
desc 'Optional multiline or single-line Markdown-formatted description',
|
24
24
|
id: 'user id',
|
25
25
|
email_addr: 'email_addr\'s desc'
|
26
|
-
email = '
|
26
|
+
email = 'zero@rails.org'
|
27
27
|
|
28
|
-
query! :id, Integer, enum: 0..5, length: [1, 2], pattern: /^[0-9]$/, range: {gt:0, le:5}
|
28
|
+
query! :id, Integer, enum: 0..5, length: [1, 2], pattern: /^[0-9]$/, range: { gt:0, le:5 }
|
29
29
|
query! :done, Boolean, must_be: false, default: true, desc: 'must be false'
|
30
30
|
query :email_addr, String, lth: :ge_3, default: email # is_a: :email
|
31
31
|
|
@@ -1,12 +1,13 @@
|
|
1
1
|
class V2::GoodsDoc < BaseDoc
|
2
2
|
|
3
|
-
open_api :index, '
|
4
|
-
use: [ 'Token' ] do # use parameters write in AutoGenDoc#api_dry
|
5
|
-
# skip:
|
3
|
+
open_api :index, 'GET list of Goods.', builder: :index, # jbuilder templates is set in initializers/open_api.rb
|
4
|
+
use: [ 'Token', :page, :rows ] do # use parameters write in AutoGenDoc#api_dry
|
5
|
+
# skip: [ 'Token' ] do # you can also skip parameters
|
6
6
|
desc 'listing Goods',
|
7
7
|
view!: 'search view, allows::<br/>',
|
8
8
|
search_type!: 'search field, allows:<br/>'
|
9
9
|
|
10
|
+
# Single `query`
|
10
11
|
query :view, String, enum: {
|
11
12
|
'all goods (default)': :all,
|
12
13
|
'only online': :online,
|
@@ -14,22 +15,23 @@ class V2::GoodsDoc < BaseDoc
|
|
14
15
|
'expensive goods': :expensive,
|
15
16
|
'cheap goods': :cheap,
|
16
17
|
}
|
17
|
-
# query
|
18
|
+
# Batch `query`
|
18
19
|
do_query by: {
|
19
20
|
:search_type => { type: String, enum: %w[ name creator category price ] },
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
21
|
+
:value => String,
|
22
|
+
:export => { type: Boolean, desc: 'export as Excel format', examples: {
|
23
|
+
:right_input => true,
|
24
|
+
:wrong_input => 'wrong input'
|
25
|
+
}}
|
24
26
|
}
|
25
27
|
end
|
26
28
|
|
27
29
|
|
28
|
-
open_api :create, '
|
30
|
+
open_api :create, 'POST create a Good', builder: :success_or_not, use: 'Token' do
|
29
31
|
form! 'for creating a good', data: {
|
30
32
|
:name! => { type: String, desc: 'good\'s name' },
|
31
33
|
:category_id! => { type: Integer, desc: 'sub_category\'s id', npmt: true, range: { ge: 1 }, as: :cate },
|
32
|
-
:price! => { type: Float, desc: 'good\'s price', range: { ge: 0} },
|
34
|
+
:price! => { type: Float, desc: 'good\'s price', range: { ge: 0 } },
|
33
35
|
# -- optional
|
34
36
|
:is_online => { type: Boolean, desc: 'it\'s online?' },
|
35
37
|
:remarks => { type: String, desc: 'remarks' },
|
@@ -43,8 +45,8 @@ class V2::GoodsDoc < BaseDoc
|
|
43
45
|
end
|
44
46
|
|
45
47
|
|
46
|
-
open_api :show, '
|
48
|
+
open_api :show, 'GET a Good.', builder: :show, use: [ 'Token', :id ]
|
47
49
|
|
48
50
|
|
49
|
-
open_api :destroy, '
|
51
|
+
open_api :destroy, 'DELETE a Good.', builder: :success_or_not, use: [ 'Token', :id ]
|
50
52
|
end
|
@@ -1,6 +1,16 @@
|
|
1
1
|
require 'open_api'
|
2
2
|
|
3
3
|
OpenApi::Config.tap do |c|
|
4
|
+
# Config DSL
|
5
|
+
c.instance_eval do
|
6
|
+
api :zero_rails_api, root_controller: ApiDoc
|
7
|
+
info version: '0.0.1', title: 'Zero Rails APIs', description: 'API documentation of Zero-Rails Application.'
|
8
|
+
server 'http://localhost:3000', desc: 'Main (production) server'
|
9
|
+
server 'http://localhost:3000', desc: 'Internal staging server for testing'
|
10
|
+
security ApiKeyAuth: [ ]
|
11
|
+
security_scheme :ApiKeyAuth, type: 'apiKey', name: 'server_token', in: 'query'
|
12
|
+
end
|
13
|
+
|
4
14
|
# [REQUIRED] The location where .json doc file will be output.
|
5
15
|
c.file_output_path = 'public/open_api'
|
6
16
|
|
@@ -88,7 +98,7 @@ OpenApi::Config.tap do |c|
|
|
88
98
|
c.jbuilder_templates = {
|
89
99
|
index: (
|
90
100
|
<<~FILE
|
91
|
-
# *** Generated by ZRO ***
|
101
|
+
# *** Generated by ZRO [ please make sure that you have checked this file ] ***
|
92
102
|
json.partial! 'api/base', total: @data.size
|
93
103
|
|
94
104
|
json.data do
|
@@ -103,7 +113,7 @@ OpenApi::Config.tap do |c|
|
|
103
113
|
|
104
114
|
show: (
|
105
115
|
<<~FILE
|
106
|
-
# *** Generated by ZRO ***
|
116
|
+
# *** Generated by ZRO [ please make sure that you have checked this file ] ***
|
107
117
|
json.partial! 'api/base', total: 1
|
108
118
|
|
109
119
|
json.data do
|
@@ -116,14 +126,14 @@ OpenApi::Config.tap do |c|
|
|
116
126
|
|
117
127
|
success: (
|
118
128
|
<<~FILE
|
119
|
-
# *** Generated by ZRO ***
|
129
|
+
# *** Generated by ZRO [ please make sure that you have checked this file ] ***
|
120
130
|
json.partial! 'api/success'
|
121
131
|
FILE
|
122
132
|
),
|
123
133
|
|
124
134
|
success_or_not: (
|
125
135
|
<<~FILE
|
126
|
-
# *** Generated by ZRO ***
|
136
|
+
# *** Generated by ZRO [ please make sure that you have checked this file ] ***
|
127
137
|
unless @status
|
128
138
|
# @_code, @_msg = @error_info.present? ? @error_info : ApiError.action_failed.info
|
129
139
|
end
|
File without changes
|
data/documentation/parameter.md
CHANGED
@@ -1,21 +1,26 @@
|
|
1
|
-
### More Explanation
|
1
|
+
### More Explanation for `param` and `schema_hash`
|
2
2
|
|
3
|
-
#### param_type
|
3
|
+
#### param_type (param_location)
|
4
4
|
OpenAPI 3.0 distinguishes between the following parameter types based on the parameter location:
|
5
5
|
**header, path, query, cookie**. [more](https://swagger.io/docs/specification/describing-parameters/)
|
6
6
|
|
7
|
-
#### name
|
8
|
-
|
9
|
-
|
7
|
+
#### name (param_name)
|
8
|
+
The name of parameter. It can be Symbol or String.
|
9
|
+
|
10
|
+
If param_type is :path, it must correspond to the associated path segment form
|
11
|
+
the routing path, for example: if the API path is `/good/:id`, you have to declare a path parameter with name `id` to it.
|
12
|
+
|
13
|
+
#### type (schema_type)
|
14
|
+
Parameter's (schema) type. We call it `schema_type` because it is inside SchemaObj.
|
15
|
+
|
16
|
+
Support all [data types](https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.0.md#dataTypes) defined in OAS.
|
10
17
|
|
11
|
-
#### type
|
12
|
-
parameter (schema) type. Support all [data types](https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.0.md#dataTypes) defined in OAS.
|
13
18
|
In addition, you can use `format` in schema_hash to define in fine detail the data type being used, like:
|
14
19
|
int32, float, date ...
|
15
|
-
All the types you can use
|
20
|
+
All the types you can use as following:
|
16
21
|
- **String, 'binary', 'base64'**
|
17
22
|
- **Integer, Long, 'int32', 'int64', Float, Double**
|
18
|
-
- **File** (it will be converted
|
23
|
+
- **File** (it will be converted to `{ type: 'string', format: Config.dft_file_format }`)
|
19
24
|
- **Date, DateTime**
|
20
25
|
- **Boolean**
|
21
26
|
- **Array**: `Array[String]` or `[String]`
|
@@ -24,8 +29,8 @@ All the types you can use are:
|
|
24
29
|
(`!` bang key means it is required).
|
25
30
|
- Nested Object: `{ id!: Integer, name: { first: String, last: String } }`
|
26
31
|
- Nested Array and Object: `[[{ id!: Integer, name: { first: String, last: String } }]]`
|
27
|
-
- **:ComponentKey**:
|
28
|
-
to the component correspond to ComponentKey
|
32
|
+
- **:ComponentKey**: pass **Symbol** value to type will generate a Schema Reference Object link
|
33
|
+
to the component correspond to ComponentKey, like: :IdPath, :NameQuery
|
29
34
|
|
30
35
|
You can use `Object.const_set()` to define a constant that does not exist, but note that
|
31
36
|
the value you set could not be a Symbol (it will be explained as a Ref Object), should be a String.
|
@@ -59,4 +64,4 @@ You can set the schema by following keys (all are optional), the words in parent
|
|
59
64
|
5. If type is Object, for describing each property's schema, the only way is use ref type, like: `{ id: :Id, name: :Name }`
|
60
65
|
- **pattern (regexp, pr, reg)**
|
61
66
|
- **default (dft, default_value)**
|
62
|
-
- **as** # TODO
|
67
|
+
- **as** # TODO
|
data/lib/oas_objs/schema_obj.rb
CHANGED
@@ -33,7 +33,7 @@ module OpenApi
|
|
33
33
|
processed_is_and_format(param_name),
|
34
34
|
{
|
35
35
|
pattern: _pattern&.inspect&.delete('/'),
|
36
|
-
default: '_default',
|
36
|
+
default: _default.nil? ? nil : '_default',
|
37
37
|
examples: self[:examples].present? ? ExampleObj.new(self[:examples], self[:exp_by]).process : nil,
|
38
38
|
},
|
39
39
|
{ as: _as, permit: _permit, not_permit: _npermit, req_if: _req_if, opt_if: _opt_if }
|
data/lib/open_api/config.rb
CHANGED
@@ -18,20 +18,20 @@ module OpenApi
|
|
18
18
|
# Getting started: https://swagger.io/docs/specification/basic-structure/
|
19
19
|
cattr_accessor :register_docs do
|
20
20
|
{
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
21
|
+
# # [REQUIRED] At least one doc.
|
22
|
+
# zero_rails_api: {
|
23
|
+
# # [REQUIRED] ZRO will scan all the descendants of the root_controller, and then generate their docs.
|
24
|
+
# root_controller: ApplicationController,
|
25
|
+
#
|
26
|
+
# # [REQUIRED] Info Object: The info section contains API information
|
27
|
+
# info: {
|
28
|
+
# # [REQUIRED] The title of the application.
|
29
|
+
# title: 'Zero Rails Apis',
|
30
|
+
# # [REQUIRED] The version of the OpenAPI document
|
31
|
+
# # (which is distinct from the OpenAPI Specification version or the API implementation version).
|
32
|
+
# version: '0.0.1'
|
33
|
+
# }
|
34
|
+
# }
|
35
35
|
}
|
36
36
|
end
|
37
37
|
|
@@ -55,7 +55,7 @@ module OpenApi
|
|
55
55
|
{
|
56
56
|
index: (
|
57
57
|
<<~FILE
|
58
|
-
# *** Generated by ZRO ***
|
58
|
+
# *** Generated by ZRO [ please make sure that you have checked this file ] ***
|
59
59
|
json.partial! 'api/base', total: @data.size
|
60
60
|
|
61
61
|
json.data do
|
@@ -70,7 +70,7 @@ module OpenApi
|
|
70
70
|
|
71
71
|
show: (
|
72
72
|
<<~FILE
|
73
|
-
# *** Generated by ZRO ***
|
73
|
+
# *** Generated by ZRO [ please make sure that you have checked this file ] ***
|
74
74
|
json.partial! 'api/base', total: 1
|
75
75
|
|
76
76
|
json.data do
|
@@ -83,14 +83,14 @@ module OpenApi
|
|
83
83
|
|
84
84
|
success: (
|
85
85
|
<<~FILE
|
86
|
-
# *** Generated by ZRO ***
|
86
|
+
# *** Generated by ZRO [ please make sure that you have checked this file ] ***
|
87
87
|
json.partial! 'api/success'
|
88
88
|
FILE
|
89
89
|
),
|
90
90
|
|
91
91
|
success_or_not: (
|
92
92
|
<<~FILE
|
93
|
-
# *** Generated by ZRO ***
|
93
|
+
# *** Generated by ZRO [ please make sure that you have checked this file ] ***
|
94
94
|
unless @status
|
95
95
|
# @_code, @_msg = @error_info.present? ? @error_info : ApiError.action_failed.info
|
96
96
|
end
|
data/lib/open_api/config_dsl.rb
CHANGED
@@ -4,8 +4,27 @@ module OpenApi
|
|
4
4
|
base.class_eval do
|
5
5
|
module_function
|
6
6
|
|
7
|
-
def
|
8
|
-
|
7
|
+
def api name, root_controller:
|
8
|
+
@api = name
|
9
|
+
register_docs[name] = { root_controller: root_controller }
|
10
|
+
end
|
11
|
+
|
12
|
+
def info version:, title:, **addition
|
13
|
+
register_docs[@api].merge! version: version, title: title, **addition
|
14
|
+
end
|
15
|
+
|
16
|
+
def server url, desc: ''
|
17
|
+
(register_docs[@api][:servers] ||= [ ]) << { url: url, description: desc }
|
18
|
+
end
|
19
|
+
|
20
|
+
def security requirement
|
21
|
+
(register_docs[@api][:global_security] ||= [ ]) << requirement
|
22
|
+
end
|
23
|
+
|
24
|
+
alias_method :security_require, :security
|
25
|
+
|
26
|
+
def security_scheme scheme_name, schema# = { }
|
27
|
+
(register_docs[@api][:global_security_schemes] ||= { }).merge! scheme_name => schema
|
9
28
|
end
|
10
29
|
end
|
11
30
|
end
|
data/lib/open_api/dsl.rb
CHANGED
@@ -26,28 +26,28 @@ module OpenApi
|
|
26
26
|
def components &block
|
27
27
|
apis_tag if @_ctrl_infos.nil?
|
28
28
|
current_ctrl = @_ctrl_infos[:components] = CtrlInfoObj.new
|
29
|
-
current_ctrl.instance_eval
|
29
|
+
current_ctrl.instance_eval(&block)
|
30
30
|
current_ctrl._process_objs
|
31
31
|
end
|
32
32
|
|
33
|
-
def open_api
|
33
|
+
def open_api action, summary = '', builder: nil, skip: [ ], use: [ ], &block
|
34
34
|
apis_tag if @_ctrl_infos.nil?
|
35
35
|
|
36
|
-
# select the routing info (corresponding to the current method) from
|
37
|
-
action_path = "#{@_ctrl_path ||= controller_path}##{
|
36
|
+
# select the routing info (corresponding to the current method) from routing list.
|
37
|
+
action_path = "#{@_ctrl_path ||= controller_path}##{action}"
|
38
38
|
routes_info = ctrl_routes_list&.select { |api| api[:action_path].match? /^#{action_path}$/ }&.first
|
39
|
-
pp "[ZRO Warning] Routing mapping failed: #{@_ctrl_path}##{
|
39
|
+
pp "[ZRO Warning] Routing mapping failed: #{@_ctrl_path}##{action}" and return if routes_info.nil?
|
40
40
|
Generator.generate_builder_file(action_path, builder) if builder.present?
|
41
41
|
|
42
42
|
# structural { #path: { #http_method:{ } } }, for pushing into Paths Object.
|
43
43
|
path = (@_api_infos ||= { })[routes_info[:path]] ||= { }
|
44
44
|
current_api = path[routes_info[:http_verb]] =
|
45
45
|
ApiInfoObj.new(action_path, skip: Array(skip), use: Array(use))
|
46
|
-
.merge! description: '', summary: summary, operationId:
|
46
|
+
.merge! description: '', summary: summary, operationId: action, tags: [@_apis_tag],
|
47
47
|
parameters: [ ], requestBody: '', responses: { }, security: [ ], servers: [ ]
|
48
48
|
|
49
49
|
current_api.tap do |api|
|
50
|
-
[
|
50
|
+
[action, :all].each do |key| # blocks_store_key
|
51
51
|
@_apis_blocks&.[](key)&.each { |blk| api.instance_eval(&blk) }
|
52
52
|
end
|
53
53
|
api.param_use = [ ] # skip 和 use 是对 dry 块而言的
|
@@ -58,18 +58,17 @@ module OpenApi
|
|
58
58
|
end
|
59
59
|
|
60
60
|
# method could be symbol array, like: %i[ .. ]
|
61
|
-
def api_dry
|
61
|
+
def api_dry action = :all, desc = '', &block
|
62
62
|
@_apis_blocks ||= { }
|
63
|
-
if
|
64
|
-
|
63
|
+
if action.is_a? Array
|
64
|
+
action.each { |m| (@_apis_blocks[m.to_sym] ||= [ ]) << block }
|
65
65
|
else
|
66
|
-
(@_apis_blocks[
|
66
|
+
(@_apis_blocks[action.to_sym] ||= [ ]) << block
|
67
67
|
end
|
68
68
|
end
|
69
69
|
|
70
70
|
def ctrl_routes_list
|
71
|
-
|
72
|
-
@routes_list[@_ctrl_path]
|
71
|
+
Generator.routes_list[@_ctrl_path]
|
73
72
|
end
|
74
73
|
end
|
75
74
|
end
|
@@ -6,7 +6,7 @@ module OpenApi
|
|
6
6
|
include DSL::CommonDSL
|
7
7
|
include DSL::Helpers
|
8
8
|
|
9
|
-
attr_accessor :action_path, :param_skip, :param_use, :param_descs
|
9
|
+
attr_accessor :action_path, :param_skip, :param_use, :param_descs, :param_order
|
10
10
|
|
11
11
|
def initialize(action_path, skip: [ ], use: [ ])
|
12
12
|
self.action_path = action_path
|
@@ -50,7 +50,7 @@ module OpenApi
|
|
50
50
|
%i[header header! path path! query query! cookie cookie!].each do |param_type|
|
51
51
|
define_method "do_#{param_type}" do |by:|
|
52
52
|
by.each do |key, value|
|
53
|
-
args = [ key.dup.to_s.delete('!'), value.delete(:type), value ]
|
53
|
+
args = [ key.dup.to_s.delete('!').to_sym, value.delete(:type), value ]
|
54
54
|
key.to_s['!'] ? send("#{param_type}!", *args) : send(param_type, *args)
|
55
55
|
end
|
56
56
|
end unless param_type.to_s['!']
|
@@ -113,12 +113,37 @@ module OpenApi
|
|
113
113
|
self[:servers] << { url: url, description: desc }
|
114
114
|
end
|
115
115
|
|
116
|
+
def order *param_names
|
117
|
+
self.param_order = param_names
|
118
|
+
end
|
119
|
+
|
120
|
+
def param_examples exp_by = :all, examples_hash
|
121
|
+
_process_objs
|
122
|
+
exp_by = self[:parameters].map { |p| p[:name] } if exp_by == :all
|
123
|
+
# TODO: ref obj
|
124
|
+
# exp_in_params = self[:parameters].map { |p| p[:schema][:examples] }.compact
|
125
|
+
# examples_hash.map! do |key, value|
|
126
|
+
# if value == []
|
127
|
+
# if key.in?(exp_in_params.map { |e| e.keys }.flatten.uniq)
|
128
|
+
# # TODO
|
129
|
+
# end
|
130
|
+
# end
|
131
|
+
# end
|
132
|
+
self[:examples] = ExampleObj.new(examples_hash, exp_by).process
|
133
|
+
end
|
134
|
+
alias_method :examples, :param_examples
|
135
|
+
|
116
136
|
|
117
137
|
def _process_objs
|
118
138
|
self[:parameters]&.each_with_index do |p, index|
|
119
139
|
self[:parameters][index] = p.process if p.is_a?(ParamObj)
|
120
140
|
end
|
121
141
|
|
142
|
+
# Parameters sorting
|
143
|
+
self[:parameters].clone.each do |p|
|
144
|
+
self[:parameters][param_order.index(p[:name])] = p
|
145
|
+
end if param_order.present?
|
146
|
+
|
122
147
|
self[:responses]&.each do |code, obj|
|
123
148
|
self[:responses][code] = obj.process if obj.is_a?(ResponseObj)
|
124
149
|
end
|
@@ -6,7 +6,7 @@ module OpenApi
|
|
6
6
|
include DSL::CommonDSL
|
7
7
|
include DSL::Helpers
|
8
8
|
|
9
|
-
def schema component_key, type, schema_hash = { }
|
9
|
+
def schema component_key, type, schema_hash# = { }
|
10
10
|
(self[:schemas] ||= { })[component_key] = SchemaObj.new(type, schema_hash).process
|
11
11
|
end
|
12
12
|
arrow_enable :schema
|
data/lib/open_api/dsl/helpers.rb
CHANGED
@@ -11,9 +11,9 @@ module OpenApi
|
|
11
11
|
# (2) config in model: https://github.com/zhandao/zero-rails/tree/master/app/models/good.rb
|
12
12
|
# (3) jbuilder file: https://github.com/zhandao/zero-rails/blob/mster/app/views/api/v1/goods/index.json.jbuilder
|
13
13
|
# in a word, BuilderSupport let you control the `output fields and nested association infos` very easily.
|
14
|
-
if model
|
14
|
+
if model.respond_to? :show_attrs
|
15
15
|
columns = model.columns.map(&:name).map(&:to_sym)
|
16
|
-
model
|
16
|
+
model.show_attrs.map do |attr|
|
17
17
|
if columns.include? attr
|
18
18
|
index = columns.index attr
|
19
19
|
type = model.columns[index].sql_type_metadata.type.to_s.camelize
|
@@ -26,13 +26,13 @@ module OpenApi
|
|
26
26
|
end rescue next
|
27
27
|
end
|
28
28
|
else
|
29
|
-
model
|
29
|
+
model.columns.map do |column|
|
30
30
|
name = column.name.to_sym
|
31
31
|
type = column.sql_type_metadata.type.to_s.camelize
|
32
32
|
type = 'DateTime' if type == 'Datetime'
|
33
33
|
{ name => Object.const_get(type) }
|
34
34
|
end
|
35
|
-
end
|
35
|
+
end.compact.reduce({ }, :merge) rescue ''
|
36
36
|
end
|
37
37
|
|
38
38
|
# Arrow Writing:
|