Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
gastonmorixe committed Aug 27, 2023
0 parents commit 9a0c02b
Show file tree
Hide file tree
Showing 14 changed files with 501 additions and 0 deletions.
14 changes: 14 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
*.gem
coverage/*
Gemfile.lock
*~
.bundle
.rvmrc
log/*
measurement/*
pkg/*
.DS_Store
.env
spec/dummy/tmp/*
spec/dummy/log/*.log
.idea
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Rails Fields

## Summary
Enforce field types and attributes for ActiveRecord models in Ruby on Rails applications.

## Description
The `rails-fields` gem provides robust field type enforcement for ActiveRecord models in Ruby on Rails applications. It includes utility methods for type validation, logging, and field mappings between GraphQL and ActiveRecord types. Custom error classes provide clear diagnostics for field-related issues, making it easier to maintain consistent data models.

## Installation

Add this line to your application's Gemfile:

```ruby
gem 'rails-fields'
```

And then execute:

```bash
$ bundle install
```

Or install it yourself as:

```
$ gem install rails-fields
```

## Usage

(TBD: Add usage examples here)

## License

The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).

## Author

Gaston Morixe - [email protected]
2 changes: 2 additions & 0 deletions TODO.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- [ ] Generators
- [ ] Install to ApplicationRecord by default
26 changes: 26 additions & 0 deletions lib/rails_fields.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
require "rails_fields/errors/rails_fields_error"
require "rails_fields/errors/rails_fields_mismatch_error"
require "rails_fields/errors/rails_fields_unknown_type_error"
require "rails_fields/utils/logging"
require "rails_fields/utils/mappings"
require "rails_fields/utils/helpers"
require "rails_fields/class_methods"
require "rails_fields/instance_methods"

# Provides enforcement of declared field for ActiveRecord models.
module RailsFields
@processed_classes = {}

def self.processed_classes
@processed_classes
end

# @param base [ActiveRecord::Base] the model to include the module in
def self.included(base)
# base.extend(ClassMethods)
# todo: raise if class methods not found
base.after_initialize do
self.class.enforce_declared_fields
end
end
end
125 changes: 125 additions & 0 deletions lib/rails_fields/class_methods.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
module RailsFields
module ClassMethods
# TODO: Check all models at rails init app? like migrations?

def declared_fields
@declared_fields ||= []
end

def declared_fields=(value)
@declared_fields = value
end

def write_migration(index: nil)
changes = RailsFields::Utils.detect_changes(self)
RailsFields::Utils.generate_migration(self, changes, index:, write: true)
end

# Declares a field with enforcement.
#
# @!method
# @param name [Symbol] the name of the field
# @param type [Symbol] the type of the field
# @param null [Boolean] whether the field can be null (default: true)
# @param index [Boolean] whether to index the field (default: false)
# @return [void]
#
# @!macro [attach] field
# @!attribute $1
# @return [$2] the $1 property
def field(name, type, null: true, index: false)
# Check if type is a valid GraphQL type
# GraphQL::Types.const_get(type) if type.is_a?(Symbol) || type.is_a?(String)
unless Utils.valid_type?(type)
raise Errors::RailsFieldsUnknownTypeError.new("
Declared field '#{name}' in class '#{self.name}' of unknown type '#{type}'. Allowed types are: #{Utils.allowed_types.join(', ')}.
")
end

declared_fields << OpenStruct.new(name: name.to_s, type:, null:, index:)
end

def gql_type
return RailsFields.processed_classes[self] if RailsFields.processed_classes[self].present?

fields = declared_fields
owner_self = self

type = Class.new(::Types::BaseObject) do
# graphql_name "#{owner_self.name}Type"
graphql_name "#{owner_self.name}"
description "A type representing a #{owner_self.name}"

fields.each do |f|
next if f.type.nil? # TODO: ! remove references fields

# Assuming a proper mapping from your custom types to GraphQL types
# TODO: use a better method or block
field_gql_type = f.name == :id ? GraphQL::Types::ID : Utils::RAILS_TO_GQL_TYPE_MAP[f.type]
field f.name, field_gql_type
end
end

# Cache the processed class here to prevent infinite recursion
RailsFields.processed_classes[self] = type

type.instance_eval do
owner_self.reflections.each do |association_name, reflection|
if reflection.macro == :has_many
reflection_klass = if reflection.options[:through]
through_reflection_klass = reflection.through_reflection.klass
source_reflection_name = reflection.source_reflection_name.to_s
source_reflection = through_reflection_klass.reflections[source_reflection_name]
source_reflection ? source_reflection.klass : through_reflection_klass
else
reflection.klass
end
field association_name, [reflection_klass.gql_type], null: true
elsif reflection.macro == :belongs_to
field association_name, reflection.klass.gql_type, null: true
end
end

type
end
end

def enforce_declared_fields
database_columns = column_names.map(&:to_sym)
declared_fields_names = declared_fields.map(&:name).map(&:to_sym) || []
changes = RailsFields::Utils.detect_changes(self)
migration = RailsFields::Utils.generate_migration(self, changes)
instance_methods = self.instance_methods(false).select do |method|
instance_method(method).source_location.first.start_with?(Rails.root.to_s)
end
extra_methods = instance_methods - declared_fields_names.map(&:to_sym)
has_changes = !changes.nil?

unless extra_methods.empty?
# TODO: Custom error subclass
raise "You have extra methods declared in #{name}: #{extra_methods.join(', ')}. Please remove them or declare them as fields."
end

if has_changes
error_message = <<~STRING
----------------
Declared Fields:
#{declared_fields_names.join(', ')}
Database columns:
#{database_columns.join(', ')}
Changes:
#{changes.to_yaml.lines[1..-1].join}
Migration:
#{migration}
----------------
STRING
raise Errors::RailsFieldsMismatchError.new(error_message)
end
end
end
end
5 changes: 5 additions & 0 deletions lib/rails_fields/errors/rails_fields_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module RailsFields
module Errors
class RailsFieldsError < StandardError; end
end
end
18 changes: 18 additions & 0 deletions lib/rails_fields/errors/rails_fields_mismatch_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module RailsFields
module Errors
class RailsFieldsMismatchError < RailsFieldsError
include ActiveSupport::ActionableError

action "Save migrations" do
models = RailsFields::Utils.active_record_models
models.each_with_index do |m, index|
m.write_migration(index:)
end
end

# action "Run db:migrations" do
# ActiveRecord::Tasks::DatabaseTasks.migrate
# end
end
end
end
5 changes: 5 additions & 0 deletions lib/rails_fields/errors/rails_fields_unknown_type_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module RailsFields
module Errors
class RailsFieldsUnknownTypeError < RailsFieldsError; end
end
end
4 changes: 4 additions & 0 deletions lib/rails_fields/instance_methods.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module RailsFields
module InstanceMethods
end
end
Loading

0 comments on commit 9a0c02b

Please sign in to comment.