facera 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.DS_Store +0 -0
- data/.github/workflows/gem-push.yml +42 -0
- data/.github/workflows/ruby.yml +35 -0
- data/.gitignore +39 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +137 -0
- data/Gemfile +9 -0
- data/LICENSE +21 -0
- data/README.md +309 -0
- data/Rakefile +6 -0
- data/examples/01_core_dsl.rb +132 -0
- data/examples/02_facet_system.rb +216 -0
- data/examples/03_api_generation.rb +117 -0
- data/examples/04_auto_mounting.rb +182 -0
- data/examples/05_adapters.rb +196 -0
- data/examples/README.md +184 -0
- data/examples/server/README.md +376 -0
- data/examples/server/adapters/payment_adapter.rb +139 -0
- data/examples/server/application.rb +17 -0
- data/examples/server/config/facera.rb +33 -0
- data/examples/server/config.ru +10 -0
- data/examples/server/cores/payment_core.rb +82 -0
- data/examples/server/facets/external_facet.rb +38 -0
- data/examples/server/facets/internal_facet.rb +33 -0
- data/examples/server/facets/operator_facet.rb +48 -0
- data/facera.gemspec +30 -0
- data/img/facera.png +0 -0
- data/lib/facera/adapter.rb +83 -0
- data/lib/facera/attribute.rb +95 -0
- data/lib/facera/auto_mount.rb +124 -0
- data/lib/facera/capability.rb +117 -0
- data/lib/facera/capability_access.rb +59 -0
- data/lib/facera/configuration.rb +83 -0
- data/lib/facera/context.rb +29 -0
- data/lib/facera/core.rb +65 -0
- data/lib/facera/dsl.rb +41 -0
- data/lib/facera/entity.rb +50 -0
- data/lib/facera/error_formatter.rb +100 -0
- data/lib/facera/errors.rb +40 -0
- data/lib/facera/executor.rb +265 -0
- data/lib/facera/facet.rb +103 -0
- data/lib/facera/field_visibility.rb +69 -0
- data/lib/facera/generators/core_generator.rb +23 -0
- data/lib/facera/generators/facet_generator.rb +25 -0
- data/lib/facera/generators/install_generator.rb +64 -0
- data/lib/facera/generators/templates/core.rb.tt +49 -0
- data/lib/facera/generators/templates/facet.rb.tt +23 -0
- data/lib/facera/grape/api_generator.rb +59 -0
- data/lib/facera/grape/endpoint_generator.rb +316 -0
- data/lib/facera/grape/entity_generator.rb +89 -0
- data/lib/facera/grape.rb +14 -0
- data/lib/facera/introspection.rb +111 -0
- data/lib/facera/introspection_api.rb +66 -0
- data/lib/facera/invariant.rb +26 -0
- data/lib/facera/loader.rb +153 -0
- data/lib/facera/openapi_generator.rb +338 -0
- data/lib/facera/railtie.rb +51 -0
- data/lib/facera/registry.rb +34 -0
- data/lib/facera/tasks/routes.rake +66 -0
- data/lib/facera/version.rb +3 -0
- data/lib/facera.rb +35 -0
- metadata +137 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
module Facera
|
|
2
|
+
class OpenAPIGenerator
|
|
3
|
+
attr_reader :facet_name, :facet, :core
|
|
4
|
+
|
|
5
|
+
def initialize(facet_name)
|
|
6
|
+
@facet_name = facet_name
|
|
7
|
+
@facet = Registry.facets[facet_name]
|
|
8
|
+
raise "Facet #{facet_name} not found" unless @facet
|
|
9
|
+
@core = Registry.cores[@facet.core_name]
|
|
10
|
+
raise "Core #{@facet.core_name} not found" unless @core
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def generate
|
|
14
|
+
{
|
|
15
|
+
openapi: '3.0.3',
|
|
16
|
+
info: info_section,
|
|
17
|
+
servers: servers_section,
|
|
18
|
+
paths: paths_section,
|
|
19
|
+
components: components_section
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def info_section
|
|
26
|
+
{
|
|
27
|
+
title: "#{@facet_name.to_s.capitalize} API",
|
|
28
|
+
description: @facet.description || "#{@facet_name} facet of #{@core.name} core",
|
|
29
|
+
version: Facera::VERSION,
|
|
30
|
+
contact: {
|
|
31
|
+
name: 'Facera Framework'
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def servers_section
|
|
37
|
+
config = Facera.configuration
|
|
38
|
+
base_path = config.base_path
|
|
39
|
+
facet_path = config.path_for_facet(@facet_name)
|
|
40
|
+
|
|
41
|
+
[{
|
|
42
|
+
url: "#{base_path}#{facet_path}",
|
|
43
|
+
description: "#{@facet_name.to_s.capitalize} API"
|
|
44
|
+
}]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def paths_section
|
|
48
|
+
paths = {}
|
|
49
|
+
|
|
50
|
+
# Health endpoint
|
|
51
|
+
paths['/health'] = {
|
|
52
|
+
get: {
|
|
53
|
+
summary: 'Health check',
|
|
54
|
+
tags: ['Health'],
|
|
55
|
+
responses: {
|
|
56
|
+
'200' => {
|
|
57
|
+
description: 'Service is healthy',
|
|
58
|
+
content: {
|
|
59
|
+
'application/json' => {
|
|
60
|
+
schema: {
|
|
61
|
+
type: 'object',
|
|
62
|
+
properties: {
|
|
63
|
+
status: { type: 'string', example: 'ok' },
|
|
64
|
+
facet: { type: 'string', example: @facet_name.to_s },
|
|
65
|
+
core: { type: 'string', example: @core.name.to_s },
|
|
66
|
+
timestamp: { type: 'string', format: 'date-time' }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# Entity endpoints
|
|
77
|
+
@facet.field_visibilities.each do |entity_name, visibility|
|
|
78
|
+
entity = @core.entities[entity_name]
|
|
79
|
+
|
|
80
|
+
# Collection endpoints
|
|
81
|
+
collection_path = "/#{entity_name}s"
|
|
82
|
+
paths[collection_path] = {}
|
|
83
|
+
|
|
84
|
+
# POST (create)
|
|
85
|
+
if capability_allowed?("create_#{entity_name}")
|
|
86
|
+
paths[collection_path][:post] = create_operation(entity_name, entity, visibility)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# GET (list)
|
|
90
|
+
if capability_allowed?("list_#{entity_name}s")
|
|
91
|
+
paths[collection_path][:get] = list_operation(entity_name, entity, visibility)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Single resource endpoints
|
|
95
|
+
resource_path = "/#{entity_name}s/{id}"
|
|
96
|
+
paths[resource_path] = {}
|
|
97
|
+
|
|
98
|
+
# GET (retrieve)
|
|
99
|
+
if capability_allowed?("get_#{entity_name}")
|
|
100
|
+
paths[resource_path][:get] = get_operation(entity_name, entity, visibility)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Action endpoints
|
|
104
|
+
@core.capabilities.each do |cap_name, capability|
|
|
105
|
+
next unless capability.type == :action
|
|
106
|
+
next unless capability.entity == entity_name
|
|
107
|
+
next unless capability_allowed?(cap_name)
|
|
108
|
+
|
|
109
|
+
action_name = cap_name.to_s.sub("#{entity_name}_", '')
|
|
110
|
+
action_path = "/#{entity_name}s/{id}/#{action_name}"
|
|
111
|
+
|
|
112
|
+
paths[action_path] = {
|
|
113
|
+
post: action_operation(cap_name, capability, entity, visibility)
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
paths
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def create_operation(entity_name, entity, visibility)
|
|
122
|
+
{
|
|
123
|
+
summary: "Create a new #{entity_name}",
|
|
124
|
+
tags: [entity_name.to_s.capitalize],
|
|
125
|
+
requestBody: {
|
|
126
|
+
required: true,
|
|
127
|
+
content: {
|
|
128
|
+
'application/json' => {
|
|
129
|
+
schema: request_schema(entity_name, entity, visibility)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
responses: {
|
|
134
|
+
'201' => {
|
|
135
|
+
description: "#{entity_name.to_s.capitalize} created successfully",
|
|
136
|
+
content: {
|
|
137
|
+
'application/json' => {
|
|
138
|
+
schema: response_schema(entity_name, entity, visibility)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
'400' => error_response('Validation error'),
|
|
143
|
+
'401' => error_response('Unauthorized')
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def list_operation(entity_name, entity, visibility)
|
|
149
|
+
{
|
|
150
|
+
summary: "List #{entity_name}s",
|
|
151
|
+
tags: [entity_name.to_s.capitalize],
|
|
152
|
+
parameters: [
|
|
153
|
+
{ name: 'limit', in: 'query', schema: { type: 'integer', default: 20 }, description: 'Number of results' },
|
|
154
|
+
{ name: 'offset', in: 'query', schema: { type: 'integer', default: 0 }, description: 'Pagination offset' }
|
|
155
|
+
],
|
|
156
|
+
responses: {
|
|
157
|
+
'200' => {
|
|
158
|
+
description: "List of #{entity_name}s",
|
|
159
|
+
content: {
|
|
160
|
+
'application/json' => {
|
|
161
|
+
schema: {
|
|
162
|
+
type: 'array',
|
|
163
|
+
items: response_schema(entity_name, entity, visibility)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
'401' => error_response('Unauthorized')
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def get_operation(entity_name, entity, visibility)
|
|
174
|
+
{
|
|
175
|
+
summary: "Get a #{entity_name}",
|
|
176
|
+
tags: [entity_name.to_s.capitalize],
|
|
177
|
+
parameters: [
|
|
178
|
+
{ name: 'id', in: 'path', required: true, schema: { type: 'string', format: 'uuid' } }
|
|
179
|
+
],
|
|
180
|
+
responses: {
|
|
181
|
+
'200' => {
|
|
182
|
+
description: "#{entity_name.to_s.capitalize} details",
|
|
183
|
+
content: {
|
|
184
|
+
'application/json' => {
|
|
185
|
+
schema: response_schema(entity_name, entity, visibility)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
'404' => error_response('Not found'),
|
|
190
|
+
'401' => error_response('Unauthorized')
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def action_operation(cap_name, capability, entity, visibility)
|
|
196
|
+
action_name = cap_name.to_s.sub("#{capability.entity}_", '')
|
|
197
|
+
|
|
198
|
+
{
|
|
199
|
+
summary: "#{action_name.capitalize} a #{capability.entity}",
|
|
200
|
+
tags: [capability.entity.to_s.capitalize],
|
|
201
|
+
parameters: [
|
|
202
|
+
{ name: 'id', in: 'path', required: true, schema: { type: 'string', format: 'uuid' } }
|
|
203
|
+
],
|
|
204
|
+
requestBody: capability.required_params.any? || capability.optional_params.any? ? {
|
|
205
|
+
content: {
|
|
206
|
+
'application/json' => {
|
|
207
|
+
schema: {
|
|
208
|
+
type: 'object',
|
|
209
|
+
properties: (capability.required_params + capability.optional_params)
|
|
210
|
+
.reject { |p| p == :id }
|
|
211
|
+
.map { |param| [param, { type: 'string' }] }
|
|
212
|
+
.to_h,
|
|
213
|
+
required: capability.required_params.reject { |p| p == :id }
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
} : nil,
|
|
218
|
+
responses: {
|
|
219
|
+
'200' => {
|
|
220
|
+
description: "#{capability.entity.to_s.capitalize} #{action_name}d successfully",
|
|
221
|
+
content: {
|
|
222
|
+
'application/json' => {
|
|
223
|
+
schema: response_schema(capability.entity, entity, visibility)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
'400' => error_response('Validation error'),
|
|
228
|
+
'404' => error_response('Not found'),
|
|
229
|
+
'401' => error_response('Unauthorized')
|
|
230
|
+
}
|
|
231
|
+
}.compact
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def request_schema(entity_name, entity, visibility)
|
|
235
|
+
properties = {}
|
|
236
|
+
required = []
|
|
237
|
+
|
|
238
|
+
entity.attributes.each do |attr_name, attr|
|
|
239
|
+
next if attr.immutable? && attr_name != :id
|
|
240
|
+
|
|
241
|
+
properties[attr_name] = attribute_to_schema(attr)
|
|
242
|
+
required << attr_name if attr.required?
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
{
|
|
246
|
+
type: 'object',
|
|
247
|
+
properties: properties,
|
|
248
|
+
required: required
|
|
249
|
+
}
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def response_schema(entity_name, entity, visibility)
|
|
253
|
+
properties = {}
|
|
254
|
+
visible_fields = visibility.visible_field_names(entity)
|
|
255
|
+
|
|
256
|
+
visible_fields.each do |field_name|
|
|
257
|
+
if entity.attributes[field_name]
|
|
258
|
+
properties[field_name] = attribute_to_schema(entity.attributes[field_name])
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Add computed fields
|
|
263
|
+
visibility.computed_fields.each do |computed_name, _block|
|
|
264
|
+
properties[computed_name] = { type: 'string', description: 'Computed field' }
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
{
|
|
268
|
+
type: 'object',
|
|
269
|
+
properties: properties
|
|
270
|
+
}
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def attribute_to_schema(attr)
|
|
274
|
+
schema = { type: type_to_openapi(attr.type) }
|
|
275
|
+
schema[:enum] = attr.enum_values if attr.enum_values.any?
|
|
276
|
+
schema[:format] = 'uuid' if attr.type == :uuid
|
|
277
|
+
schema[:format] = 'date-time' if attr.type == :datetime
|
|
278
|
+
schema
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def type_to_openapi(type)
|
|
282
|
+
case type
|
|
283
|
+
when :string, :text, :uuid then 'string'
|
|
284
|
+
when :integer then 'integer'
|
|
285
|
+
when :decimal, :money then 'number'
|
|
286
|
+
when :boolean then 'boolean'
|
|
287
|
+
when :datetime, :date then 'string'
|
|
288
|
+
when :json, :jsonb then 'object'
|
|
289
|
+
else 'string'
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def error_response(description)
|
|
294
|
+
{
|
|
295
|
+
description: description,
|
|
296
|
+
content: {
|
|
297
|
+
'application/json' => {
|
|
298
|
+
schema: {
|
|
299
|
+
type: 'object',
|
|
300
|
+
properties: {
|
|
301
|
+
error: { type: 'string' },
|
|
302
|
+
message: { type: 'string' }
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def capability_allowed?(cap_name)
|
|
311
|
+
@facet.capability_allowed?(cap_name.to_sym)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def components_section
|
|
315
|
+
{
|
|
316
|
+
securitySchemes: {
|
|
317
|
+
bearerAuth: {
|
|
318
|
+
type: 'http',
|
|
319
|
+
scheme: 'bearer',
|
|
320
|
+
bearerFormat: 'JWT'
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
class << self
|
|
327
|
+
def for_facet(facet_name)
|
|
328
|
+
new(facet_name).generate
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def generate_all
|
|
332
|
+
Registry.facets.keys.map do |facet_name|
|
|
333
|
+
[facet_name, for_facet(facet_name)]
|
|
334
|
+
end.to_h
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
if defined?(Rails)
|
|
2
|
+
module Facera
|
|
3
|
+
class Railtie < Rails::Railtie
|
|
4
|
+
railtie_name :facera
|
|
5
|
+
|
|
6
|
+
config.facera = Facera.configuration
|
|
7
|
+
|
|
8
|
+
# Auto-load facet and core definitions
|
|
9
|
+
initializer "facera.load_definitions", before: :load_config_initializers do
|
|
10
|
+
# Use Facera's loader for automatic discovery
|
|
11
|
+
Facera.load_all!
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Auto-mount facets after initialization
|
|
15
|
+
initializer "facera.auto_mount", after: :load_config_initializers do |app|
|
|
16
|
+
# Check if user has created a facera initializer
|
|
17
|
+
facera_initializer = Rails.root.join('config/initializers/facera.rb')
|
|
18
|
+
|
|
19
|
+
if File.exist?(facera_initializer)
|
|
20
|
+
# User has control via initializer, don't auto-mount unless they call it
|
|
21
|
+
Rails.logger.info "Facera: Configuration found at config/initializers/facera.rb"
|
|
22
|
+
else
|
|
23
|
+
# No initializer, auto-mount with defaults
|
|
24
|
+
if Registry.facets.any?
|
|
25
|
+
Rails.logger.info "Facera: Auto-mounting #{Registry.facets.count} facets..."
|
|
26
|
+
Facera.auto_mount!(app)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Reload facets in development mode
|
|
32
|
+
config.to_prepare do
|
|
33
|
+
if Rails.env.development?
|
|
34
|
+
Facera.load_all!
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Add rake tasks
|
|
39
|
+
rake_tasks do
|
|
40
|
+
load 'facera/tasks/routes.rake'
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Add generators
|
|
44
|
+
generators do
|
|
45
|
+
require 'facera/generators/install_generator'
|
|
46
|
+
require 'facera/generators/core_generator'
|
|
47
|
+
require 'facera/generators/facet_generator'
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Facera
|
|
2
|
+
module Registry
|
|
3
|
+
class << self
|
|
4
|
+
def cores
|
|
5
|
+
@cores ||= {}
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def facets
|
|
9
|
+
@facets ||= {}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def register_core(name, core)
|
|
13
|
+
cores[name.to_sym] = core
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def register_facet(name, facet)
|
|
17
|
+
facets[name.to_sym] = facet
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def find_core(name)
|
|
21
|
+
cores[name.to_sym] or raise Error, "Core '#{name}' not found"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def find_facet(name)
|
|
25
|
+
facets[name.to_sym] or raise Error, "Facet '#{name}' not found"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def reset!
|
|
29
|
+
@cores = {}
|
|
30
|
+
@facets = {}
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
namespace :facera do
|
|
2
|
+
desc "Show all Facera routes"
|
|
3
|
+
task routes: :environment do
|
|
4
|
+
puts "\n" + "=" * 80
|
|
5
|
+
puts "Facera Routes"
|
|
6
|
+
puts "=" * 80
|
|
7
|
+
|
|
8
|
+
if Facera::Registry.facets.empty?
|
|
9
|
+
puts "\nNo facets defined."
|
|
10
|
+
puts "Create facets in app/facets/ or use 'rails g facera:facet NAME --core=CORE'"
|
|
11
|
+
puts "=" * 80 + "\n"
|
|
12
|
+
next
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
Facera::Registry.facets.each do |name, facet|
|
|
16
|
+
config = Facera.configuration
|
|
17
|
+
next unless config.facet_enabled?(name)
|
|
18
|
+
|
|
19
|
+
path_prefix = "#{config.base_path}#{config.path_for_facet(name)}"
|
|
20
|
+
api = Facera::Grape::APIGenerator.for_facet(name)
|
|
21
|
+
|
|
22
|
+
puts "\n#{name.to_s.upcase} (#{facet.description})"
|
|
23
|
+
puts " Base: #{path_prefix}"
|
|
24
|
+
puts " Capabilities: #{facet.allowed_capabilities.count}/#{facet.core.capabilities.count}"
|
|
25
|
+
puts " Routes:"
|
|
26
|
+
|
|
27
|
+
# Group routes by resource
|
|
28
|
+
api.routes.group_by { |r| r.path.split('/')[1] }.each do |resource, routes|
|
|
29
|
+
routes.each do |route|
|
|
30
|
+
method = route.request_method.ljust(7)
|
|
31
|
+
path = "#{path_prefix}#{route.path.gsub('(.:format)', '')}"
|
|
32
|
+
puts " #{method} #{path}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
puts "\n" + "=" * 80 + "\n"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
desc "Show Facera configuration"
|
|
41
|
+
task config: :environment do
|
|
42
|
+
config = Facera.configuration
|
|
43
|
+
|
|
44
|
+
puts "\n" + "=" * 80
|
|
45
|
+
puts "Facera Configuration"
|
|
46
|
+
puts "=" * 80
|
|
47
|
+
puts "\nGeneral:"
|
|
48
|
+
puts " Base path: #{config.base_path}"
|
|
49
|
+
puts " Version: #{config.version}"
|
|
50
|
+
puts " Dashboard: #{config.dashboard}"
|
|
51
|
+
puts " Generate docs: #{config.generate_docs}"
|
|
52
|
+
|
|
53
|
+
puts "\nFacets:"
|
|
54
|
+
puts " Defined: #{Facera::Registry.facets.count}"
|
|
55
|
+
puts " Enabled: #{Facera::Registry.facets.count - config.disabled_facets.count}"
|
|
56
|
+
puts " Disabled: #{config.disabled_facets.join(', ')}" if config.disabled_facets.any?
|
|
57
|
+
|
|
58
|
+
puts "\nCores:"
|
|
59
|
+
puts " Defined: #{Facera::Registry.cores.count}"
|
|
60
|
+
Facera::Registry.cores.each do |name, core|
|
|
61
|
+
puts " #{name}: #{core.entities.count} entities, #{core.capabilities.count} capabilities"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
puts "\n" + "=" * 80 + "\n"
|
|
65
|
+
end
|
|
66
|
+
end
|
data/lib/facera.rb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require_relative "facera/version"
|
|
2
|
+
require_relative "facera/registry"
|
|
3
|
+
require_relative "facera/context"
|
|
4
|
+
require_relative "facera/attribute"
|
|
5
|
+
require_relative "facera/entity"
|
|
6
|
+
require_relative "facera/invariant"
|
|
7
|
+
require_relative "facera/capability"
|
|
8
|
+
require_relative "facera/core"
|
|
9
|
+
require_relative "facera/errors"
|
|
10
|
+
require_relative "facera/error_formatter"
|
|
11
|
+
require_relative "facera/field_visibility"
|
|
12
|
+
require_relative "facera/capability_access"
|
|
13
|
+
require_relative "facera/facet"
|
|
14
|
+
require_relative "facera/adapter"
|
|
15
|
+
require_relative "facera/dsl"
|
|
16
|
+
require_relative "facera/executor"
|
|
17
|
+
require_relative "facera/grape"
|
|
18
|
+
require_relative "facera/configuration"
|
|
19
|
+
require_relative "facera/loader"
|
|
20
|
+
require_relative "facera/introspection"
|
|
21
|
+
require_relative "facera/openapi_generator"
|
|
22
|
+
require_relative "facera/introspection_api"
|
|
23
|
+
require_relative "facera/auto_mount"
|
|
24
|
+
|
|
25
|
+
# Load Rails integration if Rails is present
|
|
26
|
+
require_relative "facera/railtie" if defined?(Rails)
|
|
27
|
+
|
|
28
|
+
module Facera
|
|
29
|
+
class Error < StandardError; end
|
|
30
|
+
|
|
31
|
+
# Convenience method to create APIs
|
|
32
|
+
def self.api_for(facet_name)
|
|
33
|
+
Grape::APIGenerator.for_facet(facet_name)
|
|
34
|
+
end
|
|
35
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: facera
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Juan Carlos Garcia
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-03-09 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: grape
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '2.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '2.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: grape-entity
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.0'
|
|
41
|
+
description: Facera allows you to define your system once as a semantic core and expose
|
|
42
|
+
it through multiple facets, each tailored to different consumers while remaining
|
|
43
|
+
logically consistent.
|
|
44
|
+
email:
|
|
45
|
+
- jugade92@gmail.com
|
|
46
|
+
executables: []
|
|
47
|
+
extensions: []
|
|
48
|
+
extra_rdoc_files: []
|
|
49
|
+
files:
|
|
50
|
+
- ".DS_Store"
|
|
51
|
+
- ".github/workflows/gem-push.yml"
|
|
52
|
+
- ".github/workflows/ruby.yml"
|
|
53
|
+
- ".gitignore"
|
|
54
|
+
- ".rspec"
|
|
55
|
+
- CHANGELOG.md
|
|
56
|
+
- Gemfile
|
|
57
|
+
- LICENSE
|
|
58
|
+
- README.md
|
|
59
|
+
- Rakefile
|
|
60
|
+
- examples/01_core_dsl.rb
|
|
61
|
+
- examples/02_facet_system.rb
|
|
62
|
+
- examples/03_api_generation.rb
|
|
63
|
+
- examples/04_auto_mounting.rb
|
|
64
|
+
- examples/05_adapters.rb
|
|
65
|
+
- examples/README.md
|
|
66
|
+
- examples/server/README.md
|
|
67
|
+
- examples/server/adapters/payment_adapter.rb
|
|
68
|
+
- examples/server/application.rb
|
|
69
|
+
- examples/server/config.ru
|
|
70
|
+
- examples/server/config/facera.rb
|
|
71
|
+
- examples/server/cores/payment_core.rb
|
|
72
|
+
- examples/server/facets/external_facet.rb
|
|
73
|
+
- examples/server/facets/internal_facet.rb
|
|
74
|
+
- examples/server/facets/operator_facet.rb
|
|
75
|
+
- facera.gemspec
|
|
76
|
+
- img/facera.png
|
|
77
|
+
- lib/facera.rb
|
|
78
|
+
- lib/facera/adapter.rb
|
|
79
|
+
- lib/facera/attribute.rb
|
|
80
|
+
- lib/facera/auto_mount.rb
|
|
81
|
+
- lib/facera/capability.rb
|
|
82
|
+
- lib/facera/capability_access.rb
|
|
83
|
+
- lib/facera/configuration.rb
|
|
84
|
+
- lib/facera/context.rb
|
|
85
|
+
- lib/facera/core.rb
|
|
86
|
+
- lib/facera/dsl.rb
|
|
87
|
+
- lib/facera/entity.rb
|
|
88
|
+
- lib/facera/error_formatter.rb
|
|
89
|
+
- lib/facera/errors.rb
|
|
90
|
+
- lib/facera/executor.rb
|
|
91
|
+
- lib/facera/facet.rb
|
|
92
|
+
- lib/facera/field_visibility.rb
|
|
93
|
+
- lib/facera/generators/core_generator.rb
|
|
94
|
+
- lib/facera/generators/facet_generator.rb
|
|
95
|
+
- lib/facera/generators/install_generator.rb
|
|
96
|
+
- lib/facera/generators/templates/core.rb.tt
|
|
97
|
+
- lib/facera/generators/templates/facet.rb.tt
|
|
98
|
+
- lib/facera/grape.rb
|
|
99
|
+
- lib/facera/grape/api_generator.rb
|
|
100
|
+
- lib/facera/grape/endpoint_generator.rb
|
|
101
|
+
- lib/facera/grape/entity_generator.rb
|
|
102
|
+
- lib/facera/introspection.rb
|
|
103
|
+
- lib/facera/introspection_api.rb
|
|
104
|
+
- lib/facera/invariant.rb
|
|
105
|
+
- lib/facera/loader.rb
|
|
106
|
+
- lib/facera/openapi_generator.rb
|
|
107
|
+
- lib/facera/railtie.rb
|
|
108
|
+
- lib/facera/registry.rb
|
|
109
|
+
- lib/facera/tasks/routes.rake
|
|
110
|
+
- lib/facera/version.rb
|
|
111
|
+
homepage: https://github.com/jcagarcia/facera
|
|
112
|
+
licenses:
|
|
113
|
+
- MIT
|
|
114
|
+
metadata:
|
|
115
|
+
homepage_uri: https://github.com/jcagarcia/facera
|
|
116
|
+
source_code_uri: https://github.com/jcagarcia/facera
|
|
117
|
+
changelog_uri: https://github.com/jcagarcia/facera/blob/main/CHANGELOG.md
|
|
118
|
+
post_install_message:
|
|
119
|
+
rdoc_options: []
|
|
120
|
+
require_paths:
|
|
121
|
+
- lib
|
|
122
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
123
|
+
requirements:
|
|
124
|
+
- - ">="
|
|
125
|
+
- !ruby/object:Gem::Version
|
|
126
|
+
version: 3.2.0
|
|
127
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - ">="
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: '0'
|
|
132
|
+
requirements: []
|
|
133
|
+
rubygems_version: 3.4.19
|
|
134
|
+
signing_key:
|
|
135
|
+
specification_version: 4
|
|
136
|
+
summary: A Ruby framework for building multi-facet APIs from a single semantic core
|
|
137
|
+
test_files: []
|