sp-duh 2.0.6
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/LICENSE +661 -0
- data/README.md +2 -0
- data/Rakefile +32 -0
- data/config/i18n/i18n.xlsx +0 -0
- data/config/initializers/active_record/connection_adapters_postgre_sql_adapter.rb +165 -0
- data/config/initializers/active_record/migration_without_transaction.rb +4 -0
- data/config/initializers/active_record/migrator.rb +34 -0
- data/config/initializers/rails/generators.rb +13 -0
- data/config/jsonapi/settings.yml +14 -0
- data/config/locales/pt.yml +15 -0
- data/lib/generators/accounting_migration/accounting_migration_generator.rb +10 -0
- data/lib/generators/accounting_migration/templates/migration.rb +42 -0
- data/lib/generators/accounting_payroll_migration/accounting_payroll_migration_generator.rb +10 -0
- data/lib/generators/accounting_payroll_migration/templates/migration.rb +73 -0
- data/lib/generators/sharded_migration/sharded_migration_generator.rb +10 -0
- data/lib/generators/sharded_migration/templates/migration.rb +45 -0
- data/lib/sp-duh.rb +32 -0
- data/lib/sp/duh.rb +180 -0
- data/lib/sp/duh/adapters/pg/text_decoder/json.rb +15 -0
- data/lib/sp/duh/adapters/pg/text_encoder/json.rb +15 -0
- data/lib/sp/duh/db/transfer/backup.rb +71 -0
- data/lib/sp/duh/db/transfer/restore.rb +89 -0
- data/lib/sp/duh/engine.rb +35 -0
- data/lib/sp/duh/exceptions.rb +70 -0
- data/lib/sp/duh/i18n/excel_loader.rb +26 -0
- data/lib/sp/duh/jsonapi/adapters/base.rb +168 -0
- data/lib/sp/duh/jsonapi/adapters/db.rb +36 -0
- data/lib/sp/duh/jsonapi/adapters/raw_db.rb +77 -0
- data/lib/sp/duh/jsonapi/configuration.rb +167 -0
- data/lib/sp/duh/jsonapi/doc/apidoc_documentation_format_generator.rb +286 -0
- data/lib/sp/duh/jsonapi/doc/generator.rb +32 -0
- data/lib/sp/duh/jsonapi/doc/schema_catalog_helper.rb +97 -0
- data/lib/sp/duh/jsonapi/doc/victor_pinus_metadata_format_parser.rb +374 -0
- data/lib/sp/duh/jsonapi/exceptions.rb +56 -0
- data/lib/sp/duh/jsonapi/model/base.rb +25 -0
- data/lib/sp/duh/jsonapi/model/concerns/attributes.rb +94 -0
- data/lib/sp/duh/jsonapi/model/concerns/model.rb +42 -0
- data/lib/sp/duh/jsonapi/model/concerns/persistence.rb +221 -0
- data/lib/sp/duh/jsonapi/model/concerns/serialization.rb +59 -0
- data/lib/sp/duh/jsonapi/parameters.rb +44 -0
- data/lib/sp/duh/jsonapi/resource_publisher.rb +28 -0
- data/lib/sp/duh/jsonapi/service.rb +110 -0
- data/lib/sp/duh/migrations.rb +47 -0
- data/lib/sp/duh/migrations/migrator.rb +41 -0
- data/lib/sp/duh/repl.rb +193 -0
- data/lib/sp/duh/version.rb +25 -0
- data/lib/tasks/db_utils.rake +98 -0
- data/lib/tasks/doc.rake +27 -0
- data/lib/tasks/i18n.rake +23 -0
- data/lib/tasks/oauth.rake +29 -0
- data/lib/tasks/transfer.rake +48 -0
- data/lib/tasks/xls2jrxml.rake +15 -0
- data/test/jsonapi/server.rb +67 -0
- data/test/tasks/test.rake +10 -0
- metadata +170 -0
@@ -0,0 +1,286 @@
|
|
1
|
+
module SP
|
2
|
+
module Duh
|
3
|
+
module JSONAPI
|
4
|
+
module Doc
|
5
|
+
|
6
|
+
class ApidocDocumentationFormatGenerator
|
7
|
+
|
8
|
+
def generate(resource_parser, version, doc_folder_path)
|
9
|
+
@version = version
|
10
|
+
# First create an index of all resources (for relationships linking)
|
11
|
+
@resources = {}
|
12
|
+
resource_parser.each do |resource|
|
13
|
+
next if get_resource_data(resource, :scope) != :public
|
14
|
+
@resources[get_resource_name(resource)] = get_resource_link(resource)
|
15
|
+
end
|
16
|
+
_log "Generating Apidoc documentation for version #{version} in folder #{doc_folder_path}", "JSONAPI::Doc::Generator"
|
17
|
+
resource_parser.each do |resource|
|
18
|
+
next if get_resource_data(resource, :scope) != :public
|
19
|
+
generate_documentation(resource, doc_folder_path)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def generate_documentation(resource, folder_path)
|
26
|
+
readonly = (get_resource_data(resource, :readonly) == true)
|
27
|
+
_log " Generating documentation for resource #{resource}", "JSONAPI::Doc::Generator"
|
28
|
+
File.open(File.join(folder_path, "#{get_resource_name(resource)}.js"), File::CREAT | File::TRUNC | File::RDWR) do |f|
|
29
|
+
wrap_in_comments(f) { append_lines(f, get_post_documentation(resource)) } if !readonly
|
30
|
+
wrap_in_comments(f) { append_lines(f, get_get_documentation(resource)) }
|
31
|
+
wrap_in_comments(f) { append_lines(f, get_patch_documentation(resource)) } if !readonly
|
32
|
+
wrap_in_comments(f) { append_lines(f, get_delete_documentation(resource)) } if !readonly
|
33
|
+
wrap_in_comments(f) { append_lines(f, get_get_documentation(resource, false)) }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def get_resource_link(resource)
|
38
|
+
group = get_resource_data(resource, :group).gsub(' ', '_')
|
39
|
+
get_action_name = "#{('Get_' + get_resource_name(resource)).camelcase}"
|
40
|
+
link = '#api-' + "#{group}-#{get_action_name}"
|
41
|
+
link
|
42
|
+
end
|
43
|
+
|
44
|
+
def wrap_in_comments(f) ; write_header(f) ; yield ; write_footer(f) ; end
|
45
|
+
|
46
|
+
def get_get_documentation(resource, single = true)
|
47
|
+
documentation = get_api_method_header(resource, :get, single) + get_api_method_params(resource, :get, single) + get_attribute_list(resource, single) + get_api_method_success_example(resource, :get, single)
|
48
|
+
documentation.compact!
|
49
|
+
documentation
|
50
|
+
end
|
51
|
+
|
52
|
+
def get_post_documentation(resource)
|
53
|
+
documentation = get_api_method_header(resource, :post) + get_api_method_params(resource, :post) + get_attribute_list(resource) + get_api_method_success_example(resource, :post)
|
54
|
+
documentation.compact!
|
55
|
+
documentation
|
56
|
+
end
|
57
|
+
|
58
|
+
def get_patch_documentation(resource)
|
59
|
+
documentation = get_api_method_header(resource, :patch) + get_api_method_params(resource, :patch) + get_attribute_list(resource) + get_api_method_success_example(resource, :patch)
|
60
|
+
documentation.compact!
|
61
|
+
documentation
|
62
|
+
end
|
63
|
+
|
64
|
+
def get_delete_documentation(resource)
|
65
|
+
documentation = get_api_method_header(resource, :delete) + get_api_method_params(resource, :delete) + get_api_method_success_example(resource, :delete)
|
66
|
+
documentation.compact!
|
67
|
+
documentation
|
68
|
+
end
|
69
|
+
|
70
|
+
def get_api_method_header(resource, method, single = true)
|
71
|
+
data = []
|
72
|
+
resource_name = get_resource_name(resource)
|
73
|
+
resource_description = get_resource_data(resource, :description)
|
74
|
+
resource_description = [ resource_name.titlecase ] if resource_description.blank?
|
75
|
+
method_title = "#{get_human_method(method).capitalize} #{uncapitalize(resource_description.first)} #{(single ? '' : 'list')}"
|
76
|
+
case
|
77
|
+
when method.to_sym.in?([ :patch, :delete ]) || (method.to_sym == :get && single)
|
78
|
+
url = "/#{resource_name}/:id"
|
79
|
+
else
|
80
|
+
url = "/#{resource_name}"
|
81
|
+
end
|
82
|
+
data << "@api {#{method}} #{url} #{method_title}"
|
83
|
+
data << "@apiVersion #{@version}"
|
84
|
+
data << "@apiName #{(method.to_s + '_' + resource_name + (single ? '' : '_list')).camelcase}"
|
85
|
+
data << "@apiGroup #{get_resource_data(resource, :group)}"
|
86
|
+
data << "@apiDescription #{method_title}"
|
87
|
+
resource_description.each_with_index do |d, i|
|
88
|
+
next if i == 0
|
89
|
+
data << d
|
90
|
+
end
|
91
|
+
data
|
92
|
+
end
|
93
|
+
|
94
|
+
def get_attribute_type_name_and_description(a)
|
95
|
+
data = ""
|
96
|
+
type = nil
|
97
|
+
if !a[:catalog].nil?
|
98
|
+
type = get_type(a[:catalog])
|
99
|
+
data += "{#{type}} "
|
100
|
+
end
|
101
|
+
description = (a[:description] || []).join('. ')
|
102
|
+
if a[:association]
|
103
|
+
if type && @resources[type.gsub('[]','')]
|
104
|
+
description = '<a href="' + @resources[type.gsub('[]','')] + '">' + description + '</a>'
|
105
|
+
end
|
106
|
+
end
|
107
|
+
data += "#{a[:name]} #{description}"
|
108
|
+
data
|
109
|
+
end
|
110
|
+
|
111
|
+
def get_api_method_params(resource, method, single = true)
|
112
|
+
a = get_resource_data(resource, :id)
|
113
|
+
id_param = (a.nil? ? -1 : a.to_i)
|
114
|
+
params = []
|
115
|
+
case
|
116
|
+
when method.to_sym.in?([ :post, :patch, :delete ]) || (method.to_sym == :get && single)
|
117
|
+
if resource[:attributes]
|
118
|
+
resource[:attributes].each_with_index do |a, i|
|
119
|
+
next if a[:readonly] == true
|
120
|
+
next if i == id_param && method.to_sym == :post
|
121
|
+
data = "@apiParam "
|
122
|
+
params << "@apiParam " + get_attribute_type_name_and_description(a)
|
123
|
+
break if i == id_param && method.to_sym.in?([ :get, :delete ])
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
params = params + get_api_method_param_example(resource, method) if method.to_sym.in?([ :post, :patch ])
|
128
|
+
params
|
129
|
+
end
|
130
|
+
|
131
|
+
def get_attribute_list(resource, single = true)
|
132
|
+
if resource[:attributes]
|
133
|
+
resource[:attributes].map do |a|
|
134
|
+
"@apiSuccess " + get_attribute_type_name_and_description(a)
|
135
|
+
end
|
136
|
+
else
|
137
|
+
[]
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def get_api_method_param_example(resource, method)
|
142
|
+
data = [ "@apiParamExample {json} Request body example", "" ]
|
143
|
+
data = data + get_api_method_json(resource, method)
|
144
|
+
data
|
145
|
+
end
|
146
|
+
|
147
|
+
def get_api_method_success_example(resource, method, single = true)
|
148
|
+
data = [ "@apiSuccessExample {json} Success response example", "HTTP/1.1 200 OK", "" ]
|
149
|
+
case
|
150
|
+
when method.to_sym.in?([ :get, :patch, :post ])
|
151
|
+
data = data + get_api_method_json(resource, :get, single)
|
152
|
+
end
|
153
|
+
data
|
154
|
+
end
|
155
|
+
|
156
|
+
def uncapitalize(text)
|
157
|
+
words = text.split(' ')
|
158
|
+
words[0] = words[0].downcase
|
159
|
+
words.join(' ')
|
160
|
+
end
|
161
|
+
|
162
|
+
def get_human_method(m) ; m.to_sym == :post ? :create : (m.to_sym == :patch ? :update : m.to_sym) ; end
|
163
|
+
def get_resource_name(r) ; get_resource_data(r, :name) ; end
|
164
|
+
def get_resource_data(r, name) ; r[:resource][name] ; end
|
165
|
+
def get_attribute(r, i) ; r[:attributes][i] if r[:attributes] ; end
|
166
|
+
def get_attribute_data(r, i, name) ; get_attribute(r, i)[name] ; end
|
167
|
+
def get_type(r) ; r['format_type'].gsub(/character varying\(\d+\)/, 'text').gsub(/character/, 'text').gsub(' without time zone', '').gsub(' with time zone', '') ; end
|
168
|
+
|
169
|
+
def get_api_method_json(resource, method, single = true)
|
170
|
+
json = [ '{' ]
|
171
|
+
case
|
172
|
+
when method.to_sym.in?([ :post, :patch ]) || (method.to_sym == :get && single)
|
173
|
+
json << ' "data": {'
|
174
|
+
json = json + get_resource_json(resource, method.to_sym != :post, !method.to_sym.in?([ :post, :patch ]))
|
175
|
+
json << ' }'
|
176
|
+
when method.to_sym == :get && !single
|
177
|
+
json << ' "data": [{'
|
178
|
+
json = json + get_resource_json(resource)
|
179
|
+
json << ' }]'
|
180
|
+
end
|
181
|
+
json << '}'
|
182
|
+
json
|
183
|
+
end
|
184
|
+
|
185
|
+
def get_resource_json(resource, include_id = true, include_readonly = true)
|
186
|
+
json = []
|
187
|
+
json << ' "type": "' + get_resource_name(resource) + '",'
|
188
|
+
id_index = get_resource_data(resource, :id).to_i
|
189
|
+
json << ' "id": "' + get_default_value_for_attribute(resource, id_index) + '",' if include_id
|
190
|
+
json << ' "attributes": {'
|
191
|
+
resource[:attributes].each_with_index do |a, i|
|
192
|
+
next if i == id_index
|
193
|
+
next if a[:readonly] == true && !include_readonly
|
194
|
+
next if a[:association]
|
195
|
+
json << ' "' + a[:name].to_s + '": ' + get_default_value_for_attribute(resource, i) + ','
|
196
|
+
end
|
197
|
+
delete_comma(json)
|
198
|
+
json << ' }'
|
199
|
+
has_associations = false
|
200
|
+
resource[:attributes].each_with_index do |a, i|
|
201
|
+
next if !a[:association]
|
202
|
+
next if a[:readonly] == true && !include_readonly
|
203
|
+
if !has_associations
|
204
|
+
add_comma(json)
|
205
|
+
json << ' "relationships": {'
|
206
|
+
has_associations = true
|
207
|
+
end
|
208
|
+
json = json + get_association_json(resource, i, a)
|
209
|
+
end
|
210
|
+
if has_associations
|
211
|
+
delete_comma(json)
|
212
|
+
json << ' }'
|
213
|
+
end
|
214
|
+
json
|
215
|
+
end
|
216
|
+
|
217
|
+
def add_comma(lines) ; lines[lines.length-1] = lines[lines.length-1] + ',' ; end
|
218
|
+
def delete_comma(lines) ; lines[lines.length-1] = lines[lines.length-1][0..lines[lines.length-1].length-2] ; end
|
219
|
+
|
220
|
+
def get_association_json(resource, i, attribute)
|
221
|
+
json = []
|
222
|
+
json << ' "' + attribute[:name] + '": {'
|
223
|
+
if attribute[:association] == :'to-many'
|
224
|
+
json << ' "data": ['
|
225
|
+
json << ' {'
|
226
|
+
json << ' "type": "' + get_type(attribute[:catalog]).sub('[]', '') + '",'
|
227
|
+
json << ' "id": ' + get_default_value_for_attribute(resource, i)
|
228
|
+
json << ' },'
|
229
|
+
json << ' {'
|
230
|
+
json << ' "type": "' + get_type(attribute[:catalog]).sub('[]', '') + '",'
|
231
|
+
json << ' "id": ' + (get_default_value_for_attribute(resource, i).to_i + 1).to_s
|
232
|
+
json << ' }'
|
233
|
+
json << ' ],'
|
234
|
+
else
|
235
|
+
json << ' "data": {'
|
236
|
+
json << ' "type": "' + get_type(attribute[:catalog]) + '",'
|
237
|
+
json << ' "id": ' + get_default_value_for_attribute(resource, i)
|
238
|
+
json << ' },'
|
239
|
+
end
|
240
|
+
delete_comma(json)
|
241
|
+
json << ' },'
|
242
|
+
json
|
243
|
+
end
|
244
|
+
|
245
|
+
def get_default_value_for_attribute(r, i)
|
246
|
+
a = get_attribute(r, i)
|
247
|
+
if a.nil?
|
248
|
+
'null'
|
249
|
+
else
|
250
|
+
if a[:name].to_sym == :id || a[:association]
|
251
|
+
return "1"
|
252
|
+
else
|
253
|
+
default = a[:example]
|
254
|
+
if !default.nil? && !a[:catalog].nil?
|
255
|
+
default = default.gsub('"','').gsub("'", '')
|
256
|
+
type = get_type(a[:catalog])
|
257
|
+
case type
|
258
|
+
when 'boolean'
|
259
|
+
default = default
|
260
|
+
when 'integer', 'bigint'
|
261
|
+
default = default
|
262
|
+
when 'numeric', 'decimal', 'float'
|
263
|
+
default = default
|
264
|
+
else
|
265
|
+
default = '"' + default + '"'
|
266
|
+
end
|
267
|
+
end
|
268
|
+
if default.nil?
|
269
|
+
default = 'null'
|
270
|
+
end
|
271
|
+
default
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
def write_header(f) ; f.puts '/**' ; end
|
277
|
+
def append_lines(f, lines) ; lines.each { |l| append_line(f, l) } ; end
|
278
|
+
def append_line(f, line = nil) ; f.puts ' * ' + line.to_s ; end
|
279
|
+
def write_footer(f) ; f.puts '*/' ; end
|
280
|
+
|
281
|
+
end
|
282
|
+
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'sp/duh/jsonapi/doc/victor_pinus_metadata_format_parser'
|
2
|
+
require 'sp/duh/jsonapi/doc/apidoc_documentation_format_generator'
|
3
|
+
|
4
|
+
module SP
|
5
|
+
module Duh
|
6
|
+
module JSONAPI
|
7
|
+
module Doc
|
8
|
+
|
9
|
+
class Generator
|
10
|
+
|
11
|
+
def initialize(pg_connection)
|
12
|
+
@pg_connection = pg_connection
|
13
|
+
end
|
14
|
+
|
15
|
+
def generate(resource_publisher, version, doc_folder_path = File.join(Dir.pwd, 'apidoc'))
|
16
|
+
# Load the JSONAPI resources from the given publishers
|
17
|
+
@parser = SP::Duh::JSONAPI::Doc::VictorPinusMetadataFormatParser.new(@pg_connection)
|
18
|
+
@parser.parse(resource_publisher)
|
19
|
+
# Generate the resources documentation
|
20
|
+
@generator = SP::Duh::JSONAPI::Doc::ApidocDocumentationFormatGenerator.new
|
21
|
+
@generator.generate(@parser, version, doc_folder_path)
|
22
|
+
# Regenerate the documentation site
|
23
|
+
_log "Generating the documentation site in #{doc_folder_path}", "JSONAPI::Doc::Generator"
|
24
|
+
`(cd #{doc_folder_path} && apidoc)`
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module SP
|
2
|
+
module Duh
|
3
|
+
module JSONAPI
|
4
|
+
module Doc
|
5
|
+
|
6
|
+
class SchemaCatalogHelper
|
7
|
+
|
8
|
+
def initialize(pg_connection)
|
9
|
+
@pg_connection = pg_connection
|
10
|
+
@default_schema = get_db_first_sharded_schema()
|
11
|
+
clear_settings
|
12
|
+
end
|
13
|
+
|
14
|
+
def clear_settings ; @settings = {} ; @attrs = nil ; end
|
15
|
+
def add_setting(setting, value) ; @settings[setting.to_sym] = value ; end
|
16
|
+
def get_settings
|
17
|
+
@settings.merge({
|
18
|
+
schema: (@settings[:use_schema] ? @default_schema : 'public')
|
19
|
+
})
|
20
|
+
end
|
21
|
+
|
22
|
+
def get_attribute(name)
|
23
|
+
if @attrs.nil?
|
24
|
+
if get_settings[:table_name].nil?
|
25
|
+
@attrs = get_db_function_attribute_definitions(get_settings[:schema], get_settings[:function_name])
|
26
|
+
else
|
27
|
+
@attrs = get_db_table_attribute_definitions(get_settings[:schema], get_settings[:table_name])
|
28
|
+
end
|
29
|
+
end
|
30
|
+
@attrs[name.to_s]
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def get_db_first_sharded_schema
|
36
|
+
result = @pg_connection.exec %q[
|
37
|
+
SELECT
|
38
|
+
c.schema_name
|
39
|
+
FROM
|
40
|
+
public.companies c
|
41
|
+
WHERE
|
42
|
+
c.use_sharded_company = true
|
43
|
+
LIMIT 1
|
44
|
+
]
|
45
|
+
result[0]['schema_name']
|
46
|
+
end
|
47
|
+
|
48
|
+
def get_db_table_attribute_definitions(schema, table_name)
|
49
|
+
return {} if schema.nil? || table_name.nil?
|
50
|
+
result = @pg_connection.exec %Q[
|
51
|
+
SELECT
|
52
|
+
t.tablename::TEXT AS object_name,
|
53
|
+
a.attname,
|
54
|
+
pg_catalog.format_type(a.atttypid, a.atttypmod),
|
55
|
+
(SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid) for 128) FROM pg_catalog.pg_attrdef d WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef) AS default_value,
|
56
|
+
a.attnotnull
|
57
|
+
FROM pg_catalog.pg_attribute a
|
58
|
+
JOIN pg_catalog.pg_class c ON a.attrelid = c.oid
|
59
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
60
|
+
JOIN pg_catalog.pg_tables t ON c.oid = (t.schemaname || '.' || t.tablename)::regclass::oid
|
61
|
+
WHERE a.attnum > 0
|
62
|
+
AND NOT a.attisdropped
|
63
|
+
AND n.nspname = '#{schema}'
|
64
|
+
AND t.tablename = '#{table_name}'
|
65
|
+
]
|
66
|
+
definitions = {}
|
67
|
+
result.each { |a| definitions.merge!({ a['attname'] => a }) }
|
68
|
+
definitions
|
69
|
+
end
|
70
|
+
|
71
|
+
def get_db_function_attribute_definitions(schema, function_name)
|
72
|
+
return {} if schema.nil? || function_name.nil?
|
73
|
+
result = @pg_connection.exec %Q[
|
74
|
+
SELECT
|
75
|
+
trim(split_part(regexp_replace(p.argument, E'^OUT ', ''), ' ', 1)) AS attname,
|
76
|
+
trim(split_part(regexp_replace(p.argument, E'^OUT ', ''), ' ', 2)) AS format_type
|
77
|
+
FROM (
|
78
|
+
SELECT
|
79
|
+
trim(unnest(regexp_split_to_array(pg_catalog.pg_get_function_arguments(p.oid), E','))) as argument
|
80
|
+
FROM pg_catalog.pg_proc p
|
81
|
+
LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
|
82
|
+
WHERE p.proname ~ '^(#{function_name})$'
|
83
|
+
AND n.nspname ~ '^(#{schema})$'
|
84
|
+
) p
|
85
|
+
WHERE
|
86
|
+
CASE WHEN p.argument ~ '^OUT' THEN true ELSE false END = true
|
87
|
+
]
|
88
|
+
definitions = {}
|
89
|
+
result.each { |a| definitions.merge!({ a['attname'] => a }) }
|
90
|
+
definitions
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,374 @@
|
|
1
|
+
require 'sp/duh/jsonapi/doc/schema_catalog_helper'
|
2
|
+
|
3
|
+
module SP
|
4
|
+
module Duh
|
5
|
+
module JSONAPI
|
6
|
+
module Doc
|
7
|
+
|
8
|
+
class VictorPinusMetadataFormatParser
|
9
|
+
include Enumerable
|
10
|
+
|
11
|
+
def resources ; @resources || [] ; end
|
12
|
+
def resource_names ; resources.map { |r| r.keys.first } ; end
|
13
|
+
|
14
|
+
def initialize(pg_connection)
|
15
|
+
@pg_connection = pg_connection
|
16
|
+
@schema_helper = SchemaCatalogHelper.new(pg_connection)
|
17
|
+
end
|
18
|
+
|
19
|
+
def parse(publisher, public_only = true)
|
20
|
+
@public_only = public_only
|
21
|
+
begin
|
22
|
+
publisher = publisher.constantize if publisher.is_a?(String)
|
23
|
+
raise Exceptions::InvalidResourcePublisherError.new(publisher: publisher.name) if !publisher.include?(ResourcePublisher)
|
24
|
+
@publisher = publisher
|
25
|
+
rescue StandardError => e
|
26
|
+
raise Exceptions::InvalidResourcePublisherError.new(publisher: publisher.is_a?(String) ? publisher : publisher.name)
|
27
|
+
end
|
28
|
+
@resources = []
|
29
|
+
add_resources_from_folder(publisher.jsonapi_resources_root)
|
30
|
+
@resources
|
31
|
+
end
|
32
|
+
|
33
|
+
def each(&block)
|
34
|
+
@resources.each do |resource|
|
35
|
+
parsed_resource = parse_resource(resource)
|
36
|
+
block.call(parsed_resource) if !parsed_resource.nil?
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def add_resources_from_folder(folder_name)
|
43
|
+
@resources ||= []
|
44
|
+
# First load resources at the root folder
|
45
|
+
Dir.glob(File.join(folder_name, '*.yml')) do |configuration_file|
|
46
|
+
add_resources_from_file(configuration_file)
|
47
|
+
end
|
48
|
+
# Then load resources at the inner folders
|
49
|
+
Dir.glob(File.join(folder_name, '*', '*.yml')) do |configuration_file|
|
50
|
+
add_resources_from_file(configuration_file)
|
51
|
+
end
|
52
|
+
@resources
|
53
|
+
end
|
54
|
+
|
55
|
+
def add_resources_from_file(configuration_file)
|
56
|
+
_log "Loading resources from file #{configuration_file}", "JSONAPI::Doc::Parser"
|
57
|
+
configuration = YAML.load_file(configuration_file)
|
58
|
+
if configuration.is_a? Hash
|
59
|
+
add_resource(configuration, configuration_file)
|
60
|
+
else
|
61
|
+
if configuration.is_a? Array
|
62
|
+
configuration.each { |resource| add_resource(resource, configuration_file) }
|
63
|
+
else
|
64
|
+
raise Exceptions::InvalidResourceConfigurationError.new(file: configuration_file)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def add_resource(resource, configuration_file)
|
70
|
+
raise Exceptions::InvalidResourceConfigurationError.new(file: configuration_file) if (resource.keys.count != 1)
|
71
|
+
resource_name = resource.keys[0]
|
72
|
+
_log " Loading resource #{resource_name}", "JSONAPI::Doc::Parser"
|
73
|
+
processed = false
|
74
|
+
@resources.each_with_index do |r, i|
|
75
|
+
if r.keys.include?(resource_name)
|
76
|
+
@resources[i] = get_resource_index(resource_name, configuration_file)
|
77
|
+
processed = true
|
78
|
+
break
|
79
|
+
end
|
80
|
+
end
|
81
|
+
@resources << get_resource_index(resource_name, configuration_file) if !processed
|
82
|
+
end
|
83
|
+
|
84
|
+
def get_resource_index(resource_name, configuration_file)
|
85
|
+
{
|
86
|
+
resource_name.to_sym => {
|
87
|
+
group: @publisher.name,
|
88
|
+
file: configuration_file
|
89
|
+
}
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
def parse_resource(resource)
|
94
|
+
resource_name = resource.keys[0].to_s
|
95
|
+
resource_file = resource.values[0][:file]
|
96
|
+
_log " Processing resource #{resource_name} in file #{resource_file}", "JSONAPI::Doc::Parser"
|
97
|
+
metadata = parse_file(resource_name, resource_file)
|
98
|
+
if !metadata.nil?
|
99
|
+
metadata[:resource] = {} if !metadata.has_key?(:resource)
|
100
|
+
metadata[:resource] = metadata[:resource].merge({
|
101
|
+
name: resource_name
|
102
|
+
})
|
103
|
+
if metadata[:resource][:group].blank?
|
104
|
+
# group = /^(?<group>\w+?)_/.match(resource_name)
|
105
|
+
# resource_group = (group ? group[:group].capitalize : resource.values[0][:group])
|
106
|
+
resource_group = resource_name.capitalize
|
107
|
+
metadata[:resource][:group] = resource_group
|
108
|
+
end
|
109
|
+
else
|
110
|
+
_log " Ignoring PRIVATE resource #{resource_name} in file #{resource_file}", "JSONAPI::Doc::Parser"
|
111
|
+
end
|
112
|
+
metadata
|
113
|
+
end
|
114
|
+
|
115
|
+
def parse_file(resource, resource_file)
|
116
|
+
|
117
|
+
context = nil
|
118
|
+
metadata = {}
|
119
|
+
indent = nil
|
120
|
+
|
121
|
+
lines = File.readlines(resource_file)
|
122
|
+
|
123
|
+
table_name = function_name = data_schema = use_schema = nil
|
124
|
+
lines.each_with_index do |line, i|
|
125
|
+
|
126
|
+
# Ignore empty lines
|
127
|
+
next if line.strip.blank?
|
128
|
+
|
129
|
+
# Process resource definition beginning
|
130
|
+
|
131
|
+
r = get_resource(line, indent)
|
132
|
+
# First get the starting line of the resource...
|
133
|
+
next if !metadata.has_key?(:resource) && (r.first.nil? || r.first.strip != resource)
|
134
|
+
# ... but exit if we reached another resource
|
135
|
+
break if metadata.has_key?(:resource) && !r.first.nil? && r.first.strip != resource
|
136
|
+
if !r.first.nil? && !metadata.has_key?(:resource)
|
137
|
+
context = :resource
|
138
|
+
indent = r.last.length
|
139
|
+
# Get the resource metadata:
|
140
|
+
m, example, tags = get_metadata_for(lines, i, r)
|
141
|
+
scope = :private
|
142
|
+
scope = :public if 'public'.in?(tags)
|
143
|
+
metadata[:resource] = {
|
144
|
+
description: m,
|
145
|
+
scope: scope
|
146
|
+
}
|
147
|
+
tags.each do |t|
|
148
|
+
case
|
149
|
+
when t.start_with?('group')
|
150
|
+
v = t.split('=')
|
151
|
+
metadata[:resource][:group] = v.last.strip
|
152
|
+
when t == 'readonly'
|
153
|
+
metadata[:resource][:readonly] = true
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Return now if resource is private
|
159
|
+
return nil if scope == :private && @public_only
|
160
|
+
|
161
|
+
if context == :resource
|
162
|
+
|
163
|
+
# Process data structure
|
164
|
+
table_name = get_value_of('pg-table', line) if table_name.nil?
|
165
|
+
function_name = get_value_of('pg-function', line) if function_name.nil?
|
166
|
+
data_schema = get_value_of('pg-schema', line) if data_schema.nil?
|
167
|
+
|
168
|
+
use_schema = get_value_of('request-schema', line) if use_schema.nil?
|
169
|
+
use_schema = get_value_of('request-sharded-schema', line) if use_schema.nil?
|
170
|
+
|
171
|
+
# Process resource attributes
|
172
|
+
|
173
|
+
if is_beginning_of_attribute_section?(line)
|
174
|
+
context = :attributes
|
175
|
+
metadata[:resource] = {} if !metadata.has_key?(:resource)
|
176
|
+
@schema_helper.clear_settings
|
177
|
+
@schema_helper.add_setting(:schema, data_schema) if !data_schema.nil?
|
178
|
+
@schema_helper.add_setting(:table_name, table_name) if !table_name.nil?
|
179
|
+
@schema_helper.add_setting(:function_name, function_name) if !function_name.nil?
|
180
|
+
@schema_helper.add_setting(:use_schema, use_schema) if !use_schema.nil?
|
181
|
+
|
182
|
+
metadata[:resource][:catalog] = {
|
183
|
+
sharded_schema: use_schema == 'true'
|
184
|
+
}
|
185
|
+
metadata[:resource][:catalog][:schema] = data_schema if !data_schema.nil?
|
186
|
+
metadata[:resource][:catalog][:table_name] = table_name if !table_name.nil?
|
187
|
+
metadata[:resource][:catalog][:function_name] = function_name if !function_name.nil?
|
188
|
+
metadata[:attributes] = []
|
189
|
+
next
|
190
|
+
end
|
191
|
+
|
192
|
+
end
|
193
|
+
|
194
|
+
readonly = false
|
195
|
+
|
196
|
+
if context == :attributes
|
197
|
+
|
198
|
+
readonly = false
|
199
|
+
a = get_attribute(line)
|
200
|
+
if !a.first.nil?
|
201
|
+
metadata[:resource][:id] = metadata[:attributes].length if a.first.strip.to_sym == :id
|
202
|
+
# Get the attribute metadata
|
203
|
+
description, example, tags = get_metadata_for(lines, i, a)
|
204
|
+
readonly = true if 'readonly'.in?(tags)
|
205
|
+
metadata[:attributes] << {
|
206
|
+
name: a.first.strip,
|
207
|
+
catalog: @schema_helper.get_attribute(a.first),
|
208
|
+
description: description,
|
209
|
+
example: example,
|
210
|
+
readonly: readonly
|
211
|
+
}
|
212
|
+
else
|
213
|
+
a = get_jsonapi_attribute(line)
|
214
|
+
if !a.first.nil?
|
215
|
+
case
|
216
|
+
when a.first.in?([ 'to-one', 'to-many' ])
|
217
|
+
context = a.first.to_sym
|
218
|
+
readonly = false
|
219
|
+
next
|
220
|
+
end
|
221
|
+
else
|
222
|
+
column = get_value_of('pg-column', line)
|
223
|
+
metadata[:attributes].last.merge!({
|
224
|
+
catalog: @schema_helper.get_attribute(column)
|
225
|
+
}) if column
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
end
|
230
|
+
|
231
|
+
if context == :"to-one" || context == :"to-many"
|
232
|
+
a = get_jsonapi_attribute(line)
|
233
|
+
if a.first.nil?
|
234
|
+
type = get_value_of('resource', line)
|
235
|
+
table_name = get_value_of('pg-table', line)
|
236
|
+
if !type.nil? || !table_name.nil?
|
237
|
+
type ||= table_name
|
238
|
+
metadata[:attributes].last.merge!({
|
239
|
+
catalog: {
|
240
|
+
'format_type' => (context == :'to-one' ? type : "#{type}[]")
|
241
|
+
}
|
242
|
+
})
|
243
|
+
end
|
244
|
+
else
|
245
|
+
case
|
246
|
+
when a.first.in?([ 'to-one', 'to-many' ])
|
247
|
+
context = a.first.to_sym
|
248
|
+
next
|
249
|
+
else
|
250
|
+
description, example, tags = get_metadata_for(lines, i, a)
|
251
|
+
readonly = true if 'readonly'.in?(tags)
|
252
|
+
metadata[:attributes] << {
|
253
|
+
name: a.first.strip,
|
254
|
+
association: context,
|
255
|
+
description: description,
|
256
|
+
example: example,
|
257
|
+
readonly: readonly
|
258
|
+
}
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
end
|
264
|
+
_log " > #{resource} metadata", "JSONAPI::Doc::Parser"
|
265
|
+
_log metadata, "JSONAPI::Doc::Parser"
|
266
|
+
metadata
|
267
|
+
end
|
268
|
+
|
269
|
+
def get_resource(line, indent = nil)
|
270
|
+
if indent.nil?
|
271
|
+
resource = /^((?<indent>\s*))?(?<name>[a-z]+[0-9|a-z|_]*?):\s*(#(?<meta>.*?))*$/.match(line)
|
272
|
+
else
|
273
|
+
resource = /^((?<indent>\s{#{indent}}))?(?<name>[a-z]+[0-9|a-z|_]*?):\s*(#(?<meta>.*?))*$/.match(line)
|
274
|
+
end
|
275
|
+
if resource
|
276
|
+
[ resource[:name], resource[:meta], resource[:indent] ]
|
277
|
+
else
|
278
|
+
[ nil, nil, nil ]
|
279
|
+
end
|
280
|
+
end
|
281
|
+
def get_jsonapi_attribute(line)
|
282
|
+
resource = /^(?<name>[a-z]+[0-9|a-z|_|-]*?):\s*(#(?<meta>.*?))*$/.match(line.strip)
|
283
|
+
if resource
|
284
|
+
[ resource[:name], resource[:meta] ]
|
285
|
+
else
|
286
|
+
[ nil, nil ]
|
287
|
+
end
|
288
|
+
end
|
289
|
+
def is_beginning_of_attribute_section?(line)
|
290
|
+
line = line.strip
|
291
|
+
line = line[1..line.length-1] if line.start_with?('#')
|
292
|
+
name, meta = get_jsonapi_attribute(line)
|
293
|
+
return true if name && name.to_sym == :attributes
|
294
|
+
end
|
295
|
+
def get_attribute(line)
|
296
|
+
attribute = /^(#\s*)?-\s*(?<name>[a-z]+[0-9|a-z|_]*?)(\s*:\s*)?(#(?<meta>.*?))*$/.match(line.strip)
|
297
|
+
if attribute
|
298
|
+
[ attribute[:name], attribute[:meta] ]
|
299
|
+
else
|
300
|
+
[ nil, nil ]
|
301
|
+
end
|
302
|
+
end
|
303
|
+
def get_metadata(line)
|
304
|
+
line = line.strip
|
305
|
+
attribute = get_attribute(line)
|
306
|
+
metadata = line.start_with?('#') ? line[1..line.length-1].strip : nil
|
307
|
+
tag = /^#\s*\[(?<tag>(\w|=|\s)+?)\]\s*$/.match(line)
|
308
|
+
if tag.nil?
|
309
|
+
example = /\((ex|Ex|default|Default):\s*(?<eg>.+?)\)/.match(line)
|
310
|
+
if metadata && attribute.first.nil? && metadata != 'attributes:'
|
311
|
+
[ metadata, example ? example[:eg] : nil, nil ]
|
312
|
+
else
|
313
|
+
[ nil, nil, nil ]
|
314
|
+
end
|
315
|
+
else
|
316
|
+
[ nil, nil, tag[:tag] ]
|
317
|
+
end
|
318
|
+
end
|
319
|
+
def get_value_of(attribute, line)
|
320
|
+
# First try to match a string value, between double quotes
|
321
|
+
data = /^#{attribute}:\s*(\"){1}(?<value>.*?)(\"){1}/.match(line.strip)
|
322
|
+
# Then try to match a non-string value
|
323
|
+
data = /^#{attribute}:\s*(?<value>.*?)(#(?<meta>.*?))*$/.match(line.strip) if data.nil?
|
324
|
+
data ? data[:value] : nil
|
325
|
+
end
|
326
|
+
|
327
|
+
def get_metadata_for(enumerable, index, object)
|
328
|
+
return if object.first.nil?
|
329
|
+
name = object.first.strip
|
330
|
+
data = []
|
331
|
+
example = nil
|
332
|
+
tags = []
|
333
|
+
if object[1].nil?
|
334
|
+
each_backwards(enumerable, index) do |line|
|
335
|
+
m = get_metadata(line)
|
336
|
+
if m.first
|
337
|
+
data << m.first
|
338
|
+
example = m[1] if m[1]
|
339
|
+
else
|
340
|
+
if m[2]
|
341
|
+
tags << m[2]
|
342
|
+
else
|
343
|
+
break
|
344
|
+
end
|
345
|
+
end
|
346
|
+
end
|
347
|
+
else
|
348
|
+
data << object[1].strip
|
349
|
+
end
|
350
|
+
if data.any?
|
351
|
+
data.reverse!
|
352
|
+
_log " > #{name} metadata", "JSONAPI::Doc::Parser"
|
353
|
+
_log data, "JSONAPI::Doc::Parser"
|
354
|
+
else
|
355
|
+
data = nil
|
356
|
+
_log " > #{name} has no metadata", "JSONAPI::Doc::Parser"
|
357
|
+
end
|
358
|
+
[ data, example, tags ]
|
359
|
+
end
|
360
|
+
|
361
|
+
def each_backwards(enumerable, index, &block)
|
362
|
+
while index > 0 do
|
363
|
+
index = index - 1
|
364
|
+
next if enumerable[index].strip.blank?
|
365
|
+
block.call(enumerable[index])
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
end
|
370
|
+
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|
374
|
+
end
|