sp-duh 2.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +661 -0
  3. data/README.md +2 -0
  4. data/Rakefile +32 -0
  5. data/config/i18n/i18n.xlsx +0 -0
  6. data/config/initializers/active_record/connection_adapters_postgre_sql_adapter.rb +165 -0
  7. data/config/initializers/active_record/migration_without_transaction.rb +4 -0
  8. data/config/initializers/active_record/migrator.rb +34 -0
  9. data/config/initializers/rails/generators.rb +13 -0
  10. data/config/jsonapi/settings.yml +14 -0
  11. data/config/locales/pt.yml +15 -0
  12. data/lib/generators/accounting_migration/accounting_migration_generator.rb +10 -0
  13. data/lib/generators/accounting_migration/templates/migration.rb +42 -0
  14. data/lib/generators/accounting_payroll_migration/accounting_payroll_migration_generator.rb +10 -0
  15. data/lib/generators/accounting_payroll_migration/templates/migration.rb +73 -0
  16. data/lib/generators/sharded_migration/sharded_migration_generator.rb +10 -0
  17. data/lib/generators/sharded_migration/templates/migration.rb +45 -0
  18. data/lib/sp-duh.rb +32 -0
  19. data/lib/sp/duh.rb +180 -0
  20. data/lib/sp/duh/adapters/pg/text_decoder/json.rb +15 -0
  21. data/lib/sp/duh/adapters/pg/text_encoder/json.rb +15 -0
  22. data/lib/sp/duh/db/transfer/backup.rb +71 -0
  23. data/lib/sp/duh/db/transfer/restore.rb +89 -0
  24. data/lib/sp/duh/engine.rb +35 -0
  25. data/lib/sp/duh/exceptions.rb +70 -0
  26. data/lib/sp/duh/i18n/excel_loader.rb +26 -0
  27. data/lib/sp/duh/jsonapi/adapters/base.rb +168 -0
  28. data/lib/sp/duh/jsonapi/adapters/db.rb +36 -0
  29. data/lib/sp/duh/jsonapi/adapters/raw_db.rb +77 -0
  30. data/lib/sp/duh/jsonapi/configuration.rb +167 -0
  31. data/lib/sp/duh/jsonapi/doc/apidoc_documentation_format_generator.rb +286 -0
  32. data/lib/sp/duh/jsonapi/doc/generator.rb +32 -0
  33. data/lib/sp/duh/jsonapi/doc/schema_catalog_helper.rb +97 -0
  34. data/lib/sp/duh/jsonapi/doc/victor_pinus_metadata_format_parser.rb +374 -0
  35. data/lib/sp/duh/jsonapi/exceptions.rb +56 -0
  36. data/lib/sp/duh/jsonapi/model/base.rb +25 -0
  37. data/lib/sp/duh/jsonapi/model/concerns/attributes.rb +94 -0
  38. data/lib/sp/duh/jsonapi/model/concerns/model.rb +42 -0
  39. data/lib/sp/duh/jsonapi/model/concerns/persistence.rb +221 -0
  40. data/lib/sp/duh/jsonapi/model/concerns/serialization.rb +59 -0
  41. data/lib/sp/duh/jsonapi/parameters.rb +44 -0
  42. data/lib/sp/duh/jsonapi/resource_publisher.rb +28 -0
  43. data/lib/sp/duh/jsonapi/service.rb +110 -0
  44. data/lib/sp/duh/migrations.rb +47 -0
  45. data/lib/sp/duh/migrations/migrator.rb +41 -0
  46. data/lib/sp/duh/repl.rb +193 -0
  47. data/lib/sp/duh/version.rb +25 -0
  48. data/lib/tasks/db_utils.rake +98 -0
  49. data/lib/tasks/doc.rake +27 -0
  50. data/lib/tasks/i18n.rake +23 -0
  51. data/lib/tasks/oauth.rake +29 -0
  52. data/lib/tasks/transfer.rake +48 -0
  53. data/lib/tasks/xls2jrxml.rake +15 -0
  54. data/test/jsonapi/server.rb +67 -0
  55. data/test/tasks/test.rake +10 -0
  56. 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