sp-duh 2.0.6
Sign up to get free protection for your applications and to get access to all the features.
- 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
|