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.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/.DS_Store +0 -0
  3. data/.github/workflows/gem-push.yml +42 -0
  4. data/.github/workflows/ruby.yml +35 -0
  5. data/.gitignore +39 -0
  6. data/.rspec +3 -0
  7. data/CHANGELOG.md +137 -0
  8. data/Gemfile +9 -0
  9. data/LICENSE +21 -0
  10. data/README.md +309 -0
  11. data/Rakefile +6 -0
  12. data/examples/01_core_dsl.rb +132 -0
  13. data/examples/02_facet_system.rb +216 -0
  14. data/examples/03_api_generation.rb +117 -0
  15. data/examples/04_auto_mounting.rb +182 -0
  16. data/examples/05_adapters.rb +196 -0
  17. data/examples/README.md +184 -0
  18. data/examples/server/README.md +376 -0
  19. data/examples/server/adapters/payment_adapter.rb +139 -0
  20. data/examples/server/application.rb +17 -0
  21. data/examples/server/config/facera.rb +33 -0
  22. data/examples/server/config.ru +10 -0
  23. data/examples/server/cores/payment_core.rb +82 -0
  24. data/examples/server/facets/external_facet.rb +38 -0
  25. data/examples/server/facets/internal_facet.rb +33 -0
  26. data/examples/server/facets/operator_facet.rb +48 -0
  27. data/facera.gemspec +30 -0
  28. data/img/facera.png +0 -0
  29. data/lib/facera/adapter.rb +83 -0
  30. data/lib/facera/attribute.rb +95 -0
  31. data/lib/facera/auto_mount.rb +124 -0
  32. data/lib/facera/capability.rb +117 -0
  33. data/lib/facera/capability_access.rb +59 -0
  34. data/lib/facera/configuration.rb +83 -0
  35. data/lib/facera/context.rb +29 -0
  36. data/lib/facera/core.rb +65 -0
  37. data/lib/facera/dsl.rb +41 -0
  38. data/lib/facera/entity.rb +50 -0
  39. data/lib/facera/error_formatter.rb +100 -0
  40. data/lib/facera/errors.rb +40 -0
  41. data/lib/facera/executor.rb +265 -0
  42. data/lib/facera/facet.rb +103 -0
  43. data/lib/facera/field_visibility.rb +69 -0
  44. data/lib/facera/generators/core_generator.rb +23 -0
  45. data/lib/facera/generators/facet_generator.rb +25 -0
  46. data/lib/facera/generators/install_generator.rb +64 -0
  47. data/lib/facera/generators/templates/core.rb.tt +49 -0
  48. data/lib/facera/generators/templates/facet.rb.tt +23 -0
  49. data/lib/facera/grape/api_generator.rb +59 -0
  50. data/lib/facera/grape/endpoint_generator.rb +316 -0
  51. data/lib/facera/grape/entity_generator.rb +89 -0
  52. data/lib/facera/grape.rb +14 -0
  53. data/lib/facera/introspection.rb +111 -0
  54. data/lib/facera/introspection_api.rb +66 -0
  55. data/lib/facera/invariant.rb +26 -0
  56. data/lib/facera/loader.rb +153 -0
  57. data/lib/facera/openapi_generator.rb +338 -0
  58. data/lib/facera/railtie.rb +51 -0
  59. data/lib/facera/registry.rb +34 -0
  60. data/lib/facera/tasks/routes.rake +66 -0
  61. data/lib/facera/version.rb +3 -0
  62. data/lib/facera.rb +35 -0
  63. metadata +137 -0
@@ -0,0 +1,65 @@
1
+ module Facera
2
+ class Core
3
+ attr_reader :name, :entities, :capabilities, :invariants
4
+
5
+ def initialize(name)
6
+ @name = name.to_sym
7
+ @entities = {}
8
+ @capabilities = {}
9
+ @invariants = {}
10
+ @current_entity = nil
11
+ end
12
+
13
+ def entity(name, &block)
14
+ entity_obj = Entity.new(name)
15
+ @entities[name.to_sym] = entity_obj
16
+ @current_entity = entity_obj
17
+
18
+ entity_obj.instance_eval(&block) if block_given?
19
+
20
+ @current_entity = nil
21
+ entity_obj
22
+ end
23
+
24
+ def capability(name, type:, &block)
25
+ capability_obj = Capability.new(name, type: type)
26
+ @capabilities[name.to_sym] = capability_obj
27
+
28
+ capability_obj.instance_eval(&block) if block_given?
29
+
30
+ capability_obj
31
+ end
32
+
33
+ def invariant(name, description: nil, &block)
34
+ invariant_obj = Invariant.new(name, description: description, &block)
35
+ @invariants[name.to_sym] = invariant_obj
36
+ end
37
+
38
+ def find_entity(name)
39
+ @entities[name.to_sym] or raise Error, "Entity '#{name}' not found in core '#{@name}'"
40
+ end
41
+
42
+ def find_capability(name)
43
+ @capabilities[name.to_sym] or raise Error, "Capability '#{name}' not found in core '#{@name}'"
44
+ end
45
+
46
+ def find_invariant(name)
47
+ @invariants[name.to_sym]
48
+ end
49
+
50
+ def validate_invariants(context)
51
+ errors = []
52
+
53
+ @invariants.each do |name, invariant|
54
+ begin
55
+ result = invariant.check(context)
56
+ errors << "Invariant '#{name}' failed" unless result
57
+ rescue Error => e
58
+ errors << e.message
59
+ end
60
+ end
61
+
62
+ errors
63
+ end
64
+ end
65
+ end
data/lib/facera/dsl.rb ADDED
@@ -0,0 +1,41 @@
1
+ module Facera
2
+ module DSL
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ module ClassMethods
8
+ def define_core(name, &block)
9
+ core = Core.new(name)
10
+ core.instance_eval(&block) if block_given?
11
+ Registry.register_core(name, core)
12
+ core
13
+ end
14
+
15
+ def define_facet(name, core:, &block)
16
+ facet = Facet.new(name, core: core)
17
+ facet.instance_eval(&block) if block_given?
18
+ Registry.register_facet(name, facet)
19
+ facet
20
+ end
21
+
22
+ def cores
23
+ Registry.cores
24
+ end
25
+
26
+ def facets
27
+ Registry.facets
28
+ end
29
+
30
+ def find_core(name)
31
+ Registry.find_core(name)
32
+ end
33
+
34
+ def find_facet(name)
35
+ Registry.find_facet(name)
36
+ end
37
+ end
38
+ end
39
+
40
+ extend DSL::ClassMethods
41
+ end
@@ -0,0 +1,50 @@
1
+ module Facera
2
+ class Entity
3
+ attr_reader :name, :attributes
4
+
5
+ def initialize(name)
6
+ @name = name.to_sym
7
+ @attributes = {}
8
+ end
9
+
10
+ def attribute(name, type, **options)
11
+ attr = Attribute.new(name, type, **options)
12
+ @attributes[attr.name] = attr
13
+ end
14
+
15
+ def find_attribute(name)
16
+ @attributes[name.to_sym]
17
+ end
18
+
19
+ def required_attributes
20
+ @attributes.values.select(&:required?)
21
+ end
22
+
23
+ def immutable_attributes
24
+ @attributes.values.select(&:immutable?)
25
+ end
26
+
27
+ def validate_data(data)
28
+ errors = []
29
+
30
+ # Check required attributes
31
+ required_attributes.each do |attr|
32
+ if data[attr.name].nil? && data[attr.name.to_s].nil?
33
+ errors << "#{attr.name} is required"
34
+ end
35
+ end
36
+
37
+ # Validate each provided attribute
38
+ data.each do |key, value|
39
+ attr = find_attribute(key)
40
+ next unless attr
41
+
42
+ unless attr.validate_value(value)
43
+ errors << "#{key} has invalid value for type #{attr.type}"
44
+ end
45
+ end
46
+
47
+ errors
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,100 @@
1
+ module Facera
2
+ class ErrorFormatter
3
+ VERBOSITY_LEVELS = [:minimal, :detailed, :structured].freeze
4
+
5
+ def initialize(verbosity = :minimal)
6
+ @verbosity = verbosity.to_sym
7
+ validate_verbosity!
8
+ end
9
+
10
+ def format(error)
11
+ case @verbosity
12
+ when :minimal
13
+ format_minimal(error)
14
+ when :detailed
15
+ format_detailed(error)
16
+ when :structured
17
+ format_structured(error)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def validate_verbosity!
24
+ unless VERBOSITY_LEVELS.include?(@verbosity)
25
+ raise Error, "Invalid verbosity level '#{@verbosity}'. Valid levels: #{VERBOSITY_LEVELS.join(', ')}"
26
+ end
27
+ end
28
+
29
+ def format_minimal(error)
30
+ {
31
+ error: error.class.name.split('::').last.gsub('Error', '').downcase,
32
+ message: error.message
33
+ }
34
+ end
35
+
36
+ def format_detailed(error)
37
+ result = {
38
+ error: error.class.name,
39
+ message: error.message,
40
+ timestamp: Time.now.iso8601
41
+ }
42
+
43
+ case error
44
+ when ValidationError
45
+ result[:validation_errors] = error.errors
46
+ when InvariantError
47
+ result[:invariant_errors] = error.invariant_errors
48
+ end
49
+
50
+ result[:backtrace] = error.backtrace.first(10) if error.backtrace
51
+
52
+ result
53
+ end
54
+
55
+ def format_structured(error)
56
+ result = {
57
+ type: error.class.name.split('::').last,
58
+ message: error.message,
59
+ timestamp: Time.now.iso8601,
60
+ severity: severity_for(error)
61
+ }
62
+
63
+ case error
64
+ when ValidationError
65
+ result[:details] = {
66
+ validation_errors: error.errors.map { |e| { field: extract_field(e), message: e } }
67
+ }
68
+ when InvariantError
69
+ result[:details] = {
70
+ invariant_violations: error.invariant_errors
71
+ }
72
+ when NotFoundError
73
+ result[:details] = { resource: extract_resource(error.message) }
74
+ end
75
+
76
+ result
77
+ end
78
+
79
+ def severity_for(error)
80
+ case error
81
+ when ValidationError, PreconditionError
82
+ 'warning'
83
+ when UnauthorizedError
84
+ 'error'
85
+ when NotFoundError
86
+ 'info'
87
+ else
88
+ 'error'
89
+ end
90
+ end
91
+
92
+ def extract_field(error_message)
93
+ error_message.split(' ').first
94
+ end
95
+
96
+ def extract_resource(message)
97
+ message.split(' ').first
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,40 @@
1
+ module Facera
2
+ class FaceraError < StandardError; end
3
+
4
+ class ValidationError < FaceraError
5
+ attr_reader :errors
6
+
7
+ def initialize(errors)
8
+ @errors = errors.is_a?(Array) ? errors : [errors]
9
+ super(@errors.join(", "))
10
+ end
11
+ end
12
+
13
+ class UnauthorizedError < FaceraError
14
+ def initialize(message = "Unauthorized access")
15
+ super(message)
16
+ end
17
+ end
18
+
19
+ class NotFoundError < FaceraError
20
+ def initialize(resource, id = nil)
21
+ message = id ? "#{resource} with id '#{id}' not found" : "#{resource} not found"
22
+ super(message)
23
+ end
24
+ end
25
+
26
+ class PreconditionError < FaceraError
27
+ def initialize(message = "Precondition failed")
28
+ super(message)
29
+ end
30
+ end
31
+
32
+ class InvariantError < FaceraError
33
+ attr_reader :invariant_errors
34
+
35
+ def initialize(invariant_errors)
36
+ @invariant_errors = invariant_errors
37
+ super("Invariant violations: #{invariant_errors.join(', ')}")
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,265 @@
1
+ module Facera
2
+ class Executor
3
+ attr_reader :facet, :capability, :params, :context
4
+
5
+ def self.run(facet:, capability:, params: {}, context: {})
6
+ new(facet: facet, capability: capability, params: params, context: context).execute
7
+ end
8
+
9
+ def initialize(facet:, capability:, params: {}, context: {})
10
+ @facet = facet.is_a?(Symbol) ? Registry.find_facet(facet) : facet
11
+ @capability = capability.is_a?(Symbol) ? @facet.core.find_capability(capability) : capability
12
+ @params = normalize_params(params)
13
+ @context = context
14
+ end
15
+
16
+ def execute
17
+ # Check if capability is allowed in this facet
18
+ unless facet.capability_allowed?(capability.name)
19
+ raise UnauthorizedError, "Capability '#{capability.name}' is not allowed in facet '#{facet.name}'"
20
+ end
21
+
22
+ # Validate parameters
23
+ validate_params!
24
+
25
+ # Apply facet scoping if present
26
+ apply_scoping!
27
+
28
+ # Check preconditions
29
+ check_preconditions!
30
+
31
+ # Execute the capability logic
32
+ result = execute_capability
33
+
34
+ # Validate invariants if result is an entity (not a collection or metadata structure)
35
+ validate_invariants!(result) if should_validate_result?(result)
36
+
37
+ result
38
+ end
39
+
40
+ private
41
+
42
+ def normalize_params(params)
43
+ # Convert string keys to symbols
44
+ params.transform_keys(&:to_sym)
45
+ end
46
+
47
+ def validate_params!
48
+ errors = capability.validate_params(params)
49
+
50
+ raise ValidationError.new(errors) if errors.any?
51
+ end
52
+
53
+ def apply_scoping!
54
+ return unless facet.has_scope_for?(capability.name)
55
+
56
+ scope_block = facet.capability_scope(capability.name)
57
+ scope_params = context_eval(&scope_block)
58
+
59
+ # Merge scope params into params
60
+ @params.merge!(scope_params) if scope_params.is_a?(Hash)
61
+ end
62
+
63
+ def check_preconditions!
64
+ return if capability.preconditions.empty?
65
+
66
+ unless capability.check_preconditions(build_context)
67
+ raise PreconditionError, "Precondition failed for capability '#{capability.name}'"
68
+ end
69
+ end
70
+
71
+ def execute_capability
72
+ # Priority 1: Execute block defined inline
73
+ if capability.has_execute_block?
74
+ return execute_inline_block
75
+ end
76
+
77
+ # Priority 2: Call adapter if registered
78
+ adapter = AdapterRegistry.get(facet.core.name)
79
+ if adapter
80
+ return execute_adapter(adapter)
81
+ end
82
+
83
+ # Fallback: Mock implementation
84
+ execute_mock_implementation
85
+ end
86
+
87
+ def execute_inline_block
88
+ # Execute the block defined in the capability
89
+ context = build_context
90
+ result = context.instance_exec(params, &capability.execute_block)
91
+
92
+ # Apply field setters if result is a hash
93
+ if result.is_a?(Hash) && capability.field_setters.any?
94
+ capability.field_setters.each do |field, value|
95
+ result[field] = value.is_a?(Proc) ? value.call : value
96
+ end
97
+ end
98
+
99
+ result
100
+ end
101
+
102
+ def execute_adapter(adapter_class)
103
+ adapter_instance = adapter_class.new
104
+
105
+ # Call the appropriate adapter method
106
+ case capability.type
107
+ when :create
108
+ adapter_instance.send("create_#{capability.entity_name}", params)
109
+ when :get
110
+ adapter_instance.send("get_#{capability.entity_name}", params)
111
+ when :list
112
+ adapter_instance.send("list_#{capability.entity_name}s", params)
113
+ when :action
114
+ # For actions, call the method by capability name
115
+ adapter_instance.send(capability.name, params)
116
+ when :update
117
+ adapter_instance.send("update_#{capability.entity_name}", params)
118
+ when :delete
119
+ adapter_instance.send("delete_#{capability.entity_name}", params)
120
+ end
121
+ end
122
+
123
+ def execute_mock_implementation
124
+ # Fallback mock implementation (for testing/prototyping)
125
+ case capability.type
126
+ when :create
127
+ create_result
128
+ when :get
129
+ get_result
130
+ when :list
131
+ list_result
132
+ when :action
133
+ action_result
134
+ when :update
135
+ update_result
136
+ when :delete
137
+ delete_result
138
+ else
139
+ {}
140
+ end
141
+ end
142
+
143
+ def create_result
144
+ # Mock created entity
145
+ entity_attrs = params.dup
146
+ entity_attrs[:id] = generate_id
147
+ entity_attrs[:created_at] = Time.now
148
+ entity_attrs
149
+ end
150
+
151
+ def get_result
152
+ # Mock fetched entity
153
+ base_data = mock_entity_data
154
+ base_data[:id] = params[:id] # Use the requested ID
155
+ base_data
156
+ end
157
+
158
+ def list_result
159
+ # Mock collection
160
+ {
161
+ data: [mock_entity_data],
162
+ meta: {
163
+ total: 1,
164
+ limit: params[:limit] || 20,
165
+ offset: params[:offset] || 0
166
+ }
167
+ }
168
+ end
169
+
170
+ def action_result
171
+ # Mock entity after action
172
+ result = get_result
173
+
174
+ # Apply transitions
175
+ if capability.transitions.any?
176
+ result[:status] = capability.transitions.first
177
+ end
178
+
179
+ # Apply field setters
180
+ capability.field_setters.each do |field, value|
181
+ result[field] = value.is_a?(Proc) ? value.call : value
182
+ end
183
+
184
+ result
185
+ end
186
+
187
+ def update_result
188
+ get_result.merge(params.except(:id))
189
+ end
190
+
191
+ def delete_result
192
+ { success: true, id: params[:id] }
193
+ end
194
+
195
+ def should_validate_result?(result)
196
+ return false unless result.is_a?(Hash)
197
+ return false if capability.type == :list # List returns collection structure
198
+ return false if capability.type == :delete # Delete returns success message
199
+ return false unless capability.entity_name
200
+
201
+ true
202
+ end
203
+
204
+ def validate_invariants!(result)
205
+ return unless capability.entity_name
206
+
207
+ entity = facet.core.find_entity(capability.entity_name)
208
+ errors = entity.validate_data(result)
209
+
210
+ # Also check core invariants
211
+ invariant_errors = facet.core.validate_invariants(build_context(result))
212
+ errors.concat(invariant_errors)
213
+
214
+ raise InvariantError.new(invariant_errors) if invariant_errors.any?
215
+ raise ValidationError.new(errors) if errors.any?
216
+ end
217
+
218
+ def build_context(data = {})
219
+ # Create a context object with data that can be used in blocks
220
+ Context.new(data.merge(params).merge(context))
221
+ end
222
+
223
+ def context_eval(&block)
224
+ build_context.instance_eval(&block)
225
+ end
226
+
227
+ def generate_id
228
+ require 'securerandom'
229
+ SecureRandom.uuid
230
+ end
231
+
232
+ def mock_entity_data
233
+ # Generate mock data based on entity definition
234
+ return {} unless capability.entity_name
235
+
236
+ entity = facet.core.find_entity(capability.entity_name)
237
+ data = {
238
+ id: generate_id,
239
+ created_at: Time.now
240
+ }
241
+
242
+ entity.attributes.each do |name, attr|
243
+ next if [:id, :created_at, :updated_at].include?(name)
244
+ data[name] = mock_value_for_type(attr.type)
245
+ end
246
+
247
+ data
248
+ end
249
+
250
+ def mock_value_for_type(type)
251
+ case type
252
+ when :string then "sample"
253
+ when :integer then 42
254
+ when :money then 100.0
255
+ when :uuid then generate_id
256
+ when :enum then :pending
257
+ when :boolean then true
258
+ when :hash then {}
259
+ when :array then []
260
+ when :timestamp, :datetime then Time.now
261
+ else nil
262
+ end
263
+ end
264
+ end
265
+ end
@@ -0,0 +1,103 @@
1
+ module Facera
2
+ class Facet
3
+ attr_reader :name, :core_name, :description, :field_visibilities, :capability_access,
4
+ :error_verbosity, :format, :rate_limit, :audit_enabled
5
+
6
+ def initialize(name, core:)
7
+ @name = name.to_sym
8
+ @core_name = core.to_sym
9
+ @description = nil
10
+ @field_visibilities = {}
11
+ @capability_access = CapabilityAccess.new
12
+ @error_verbosity = :minimal
13
+ @format = :json
14
+ @rate_limit = nil
15
+ @audit_enabled = false
16
+ @audit_options = {}
17
+ end
18
+
19
+ def core
20
+ @core ||= Registry.find_core(@core_name)
21
+ end
22
+
23
+ def description(text = nil)
24
+ return @description if text.nil?
25
+ @description = text
26
+ end
27
+
28
+ def expose(entity_name, &block)
29
+ visibility = FieldVisibility.new(entity_name)
30
+ @field_visibilities[entity_name.to_sym] = visibility
31
+ visibility.instance_eval(&block) if block_given?
32
+ visibility
33
+ end
34
+
35
+ def allow_capabilities(*capability_names)
36
+ @capability_access.allow(*capability_names)
37
+ end
38
+
39
+ def deny_capabilities(*capability_names)
40
+ @capability_access.deny(*capability_names)
41
+ end
42
+
43
+ def scope(capability_name, &block)
44
+ @capability_access.scope(capability_name, &block)
45
+ end
46
+
47
+ def error_verbosity(level = nil)
48
+ return @error_verbosity if level.nil?
49
+ @error_verbosity = level.to_sym
50
+ end
51
+
52
+ def format(format_type = nil)
53
+ return @format if format_type.nil?
54
+ @format = format_type.to_sym
55
+ end
56
+
57
+ def rate_limit(requests: nil, per: nil)
58
+ return @rate_limit if requests.nil?
59
+ @rate_limit = { requests: requests, per: per.to_sym }
60
+ end
61
+
62
+ def audit_all_operations(**options)
63
+ @audit_enabled = true
64
+ @audit_options = options
65
+ end
66
+
67
+ def field_visibility_for(entity_name)
68
+ @field_visibilities[entity_name.to_sym]
69
+ end
70
+
71
+ def visible_fields_for(entity_name)
72
+ visibility = field_visibility_for(entity_name)
73
+ return [] unless visibility
74
+
75
+ entity = core.find_entity(entity_name)
76
+ visibility.all_visible_fields(entity)
77
+ end
78
+
79
+ def capability_allowed?(capability_name)
80
+ @capability_access.allowed?(capability_name)
81
+ end
82
+
83
+ def capability_scope(capability_name)
84
+ @capability_access.scope_for(capability_name)
85
+ end
86
+
87
+ def has_scope_for?(capability_name)
88
+ @capability_access.has_scope?(capability_name)
89
+ end
90
+
91
+ def allowed_capabilities
92
+ @capability_access.allowed_capability_names(core)
93
+ end
94
+
95
+ def error_formatter
96
+ @error_formatter ||= ErrorFormatter.new(@error_verbosity)
97
+ end
98
+
99
+ def format_error(error)
100
+ error_formatter.format(error)
101
+ end
102
+ end
103
+ end