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.
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