mongify-mongoid 1.0.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.
@@ -0,0 +1,30 @@
1
+ module Mongify
2
+ module Mongoid
3
+ # Base Mongify Error
4
+ class Error < RuntimeError; end
5
+
6
+ # Not Implemented Error from Mongify
7
+ class NotImplementedError < Error; end
8
+
9
+ # Would overwrite existing file
10
+ class OverwritingFolder < Error; end
11
+
12
+ # File Not Found Exception
13
+ class FileNotFound < Error; end
14
+
15
+ # Raised when Translation file is missing
16
+ class TranslationFileNotFound < FileNotFound; end
17
+
18
+ # Raised when an invalid relation is created
19
+ class InvalidRelation < Error; end
20
+
21
+ # Raised when an invalid field is created
22
+ class InvalidField < Error; end
23
+
24
+ # Raised when application has no root folder set
25
+ class RootMissing < Error; end
26
+
27
+ # Raised when an invalid option is passed via CLI
28
+ class InvalidOption < Error; end
29
+ end
30
+ end
@@ -0,0 +1,117 @@
1
+ module Mongify
2
+ module Mongoid
3
+ #
4
+ # Generator - Processes translation file and generates output based on models
5
+ #
6
+ class Generator
7
+ attr_reader :models
8
+ def initialize(translation_file, output_dir)
9
+ @translation_file = translation_file
10
+ @output_dir = output_dir
11
+ @models = {}
12
+ end
13
+
14
+ # Process translation file and generate output files
15
+ # @return [nil]
16
+ def process
17
+ unless File.exists?(@translation_file)
18
+ raise Mongify::Mongoid::TranslationFileNotFound, "Unable to find Translation File at #{@translation_file}"
19
+ end
20
+
21
+ generate_models
22
+ process_fields
23
+ generate_embedded_relations
24
+ write_models_to_file
25
+ nil
26
+ end
27
+
28
+ # Generate models based on traslation tables
29
+ # @return [Array] List of tables that models were generated from
30
+ def generate_models
31
+ (translation.tables + translation.polymorphic_tables).each do |table|
32
+ build_model(table)
33
+ end
34
+ end
35
+
36
+ # Goes through all the models and adds fields based on columns in the given table
37
+ # @return [Array] List of models that fields were added to
38
+ def process_fields
39
+ models.each do |key, model|
40
+ table = translation.find(model.table_name)
41
+ model = generate_fields_for model, table if table
42
+ end
43
+ end
44
+
45
+ # Goes through embedded relationships
46
+ # @return [Array] List of tables that were used to extra embedded relations
47
+ def generate_embedded_relations
48
+ translation.embed_tables.each do |table|
49
+ extract_embedded_relations(table)
50
+ end
51
+ end
52
+
53
+ # Writes models to files
54
+ # @return [Printer] Printer class used to write output
55
+ def write_models_to_file
56
+ Printer.new(models, @output_dir).tap do |p|
57
+ p.write
58
+ end
59
+ end
60
+
61
+ # Returns model based on table name
62
+ # @param [String] name Name of table
63
+ # @return [Model, nil] Found model or nil
64
+ def find_model(name)
65
+ @models[name.to_s.downcase.to_sym]
66
+ end
67
+
68
+ # Returns Mongify translation class for given translation file
69
+ # @return [Mongify::Translation] Translation file
70
+ def translation
71
+ @translation ||= Mongify::Translation.parse(@translation_file)
72
+ end
73
+
74
+ #######
75
+ private
76
+ #######
77
+
78
+ # Generates fields for given model and it's table
79
+ # @param [Model] model
80
+ # @param [Mongify::Database::Table] table
81
+ # @return [Array] List of columns from the table
82
+ def generate_fields_for(model, table)
83
+ table.columns.each do |column|
84
+ if column.options['references'] && parent_model = find_model(column.options['references'])
85
+ model.add_relation(Model::Relation::BELONGS_TO, parent_model.class_name.downcase)
86
+ #TODO: Look into if there is there a way to figure out a has_one relationship?
87
+ parent_model.add_relation(Model::Relation::HAS_MANY, model.table_name)
88
+ else
89
+ model.add_field(column.name, column.type.to_s.classify, column.options)
90
+ end
91
+ end
92
+ end
93
+
94
+ # Extracts embedded relationships for given table
95
+ # @param [Mongify::Database::Table] table
96
+ # @return [Model] Model for the given table
97
+ def extract_embedded_relations(table)
98
+ model = find_model(table.name)
99
+ parent_model = find_model(table.embed_in)
100
+
101
+ model.add_relation(Model::Relation::EMBEDDED_IN, table.embed_in)
102
+ parent_model.add_relation((table.embedded_as_object? ? Model::Relation::EMBEDS_ONE : Model::Relation::EMBEDS_MANY), model.table_name)
103
+ model
104
+ end
105
+
106
+ # Returns build model based on a table
107
+ # @param [Mongify::Database::Table] table
108
+ def build_model(table)
109
+ model = Mongify::Mongoid::Model.new(class_name: table.name.classify, table_name: table.name)
110
+ model.polymorphic_as = table.polymorphic_as if table.polymorphic?
111
+ #TODO: Might need to check that model doesn't already exist in @models
112
+ @models[table.name.downcase.to_sym] = model
113
+ model
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,137 @@
1
+ require "mongify/mongoid/model/field"
2
+ require "mongify/mongoid/model/relation"
3
+
4
+ module Mongify
5
+ module Mongoid
6
+ #
7
+ # Class that will be used to define a mongoid model
8
+ #
9
+ class Model
10
+ #default created at field name
11
+ CREATED_AT_FIELD = 'created_at'
12
+ #default update at field name
13
+ UPDATED_AT_FIELD = 'updated_at'
14
+
15
+ #List of fields to exclude from the model
16
+ EXCLUDED_FIELDS = [
17
+ 'id',
18
+ CREATED_AT_FIELD,
19
+ UPDATED_AT_FIELD
20
+ ]
21
+ attr_accessor :class_name, :table_name, :fields, :relations, :polymorphic_as
22
+
23
+ #Returns true if it has any timestamps
24
+ def has_timestamps?
25
+ has_created_at_timestamp? || has_updated_at_timestamp?
26
+ end
27
+
28
+ #Returns true both timestamps are present (created_at and updated_at)
29
+ def has_both_timestamps?
30
+ has_created_at_timestamp? && has_updated_at_timestamp?
31
+ end
32
+
33
+ #Returns true created_at timestamp exists
34
+ def has_created_at_timestamp?
35
+ !!@created_at
36
+ end
37
+
38
+ #Returns true updated_at timestamp exists
39
+ def has_updated_at_timestamp?
40
+ !!@updated_at
41
+ end
42
+
43
+ #Returns true it is a polymorphic model
44
+ def polymorphic?
45
+ !!@polymorphic_as
46
+ end
47
+
48
+ def initialize(options = {})
49
+ @class_name = options[:class_name].to_s
50
+ @table_name = options[:table_name].to_s
51
+ self.polymorphic_as = options[:polymorphic_as]
52
+
53
+ @fields = {}
54
+ @relations = []
55
+ end
56
+ alias :name :class_name
57
+
58
+ # Adds a field definition to the class,
59
+ # e.g:
60
+ # add_field("field_name", "String", {rename_to: "name"})
61
+ def add_field(name, type, options={})
62
+ options.stringify_keys!
63
+ return if options['ignore'] || options['references'] || polymorphic_field?(name)
64
+ check_for_timestamp(name)
65
+ return if EXCLUDED_FIELDS.include?(name.to_s.downcase)
66
+ name = options['rename_to'] if options['rename_to'].present?
67
+ begin
68
+ @fields[name.to_sym] = Field.new(name, type, options)
69
+ rescue InvalidField => e
70
+ raise InvalidField, "Unkown field type #{type} for #{name} in #{class_name}"
71
+ end
72
+ end
73
+
74
+ # Adds a relationship definition to the class,
75
+ # e.g:
76
+ # add_relation("embedded_in", "users")
77
+ #
78
+ # @note Embedded relations will overpower related relations
79
+ # @return [Relation] New generated relation
80
+ def add_relation(relation_name, association, options={})
81
+ if existing = find_relation_by(association)
82
+ if relation_name =~ /^embed/
83
+ delete_relation_for association
84
+ else
85
+ return
86
+ end
87
+ end
88
+ Relation.new(relation_name.to_s, association, options).tap do |r|
89
+ @relations << r
90
+ end
91
+ end
92
+
93
+ # Returns binding for ERB template
94
+ # @return [Binding]
95
+ def get_binding
96
+ return binding()
97
+ end
98
+
99
+ # Returns an improved inspection output including fields and relations
100
+ # @return [String] String representation of the model
101
+ def to_s
102
+ "#<Mongify::Mongoid::Model::#{name} fields=#{@fields.keys} relations=#{@relations.map{|r| "#{r.name} :#{r.association}"}}>"
103
+ end
104
+
105
+ #######
106
+ private
107
+ #######
108
+
109
+ # Checks if given field name is a known timestamp field
110
+ # @return [nil]
111
+ def check_for_timestamp name
112
+ @created_at = true if name == CREATED_AT_FIELD
113
+ @updated_at = true if name == UPDATED_AT_FIELD
114
+ nil
115
+ end
116
+
117
+ # Returns true if given field name follows polymorphic rules
118
+ # @return [Boolean]
119
+ def polymorphic_field? name
120
+ return unless polymorphic?
121
+ name == "#{polymorphic_as}_type" || name == "#{polymorphic_as}_id"
122
+ end
123
+
124
+ # Finds a relation by association
125
+ # @return [Relation, nil] The found relation or nil
126
+ def find_relation_by association
127
+ @relations.find{|r| r.association == association || r.association == association.singularize}
128
+ end
129
+
130
+ # Deletes given relations based on association name
131
+ # @return [nil]
132
+ def delete_relation_for association
133
+ @relations.reject!{ |r| r.association == association || r.association == association.singularize}
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,79 @@
1
+ module Mongify
2
+ module Mongoid
3
+ class Model
4
+ #
5
+ # Field for a Mongoid Model
6
+ #
7
+ class Field
8
+ #List of accepted types
9
+ ACCEPTED_TYPES = [
10
+ "Array",
11
+ "BigDecimal",
12
+ "Boolean",
13
+ "Date",
14
+ "DateTime",
15
+ "Float",
16
+ "Hash",
17
+ "Integer",
18
+ "Moped::BSON::ObjectId",
19
+ "Moped::BSON::Binary",
20
+ "Range",
21
+ "Regexp",
22
+ "String",
23
+ "Symbol",
24
+ "Time",
25
+ "TimeWithZone"
26
+ ]
27
+
28
+ #Hash of translations for different types
29
+ TRANSLATION_TYPES = {
30
+ array: "Array",
31
+ bigdecimal: "BigDecimal",
32
+ boolean: "Boolean",
33
+ date: "Date",
34
+ datetime: "DateTime",
35
+ float: "Float",
36
+ hash: "Hash",
37
+ integer: "Integer",
38
+ objectid: "Moped::BSON::ObjectId",
39
+ binary: "Moped::BSON::Binary",
40
+ range: "Range",
41
+ regexp: "Regexp",
42
+ string: "String",
43
+ symbol: "Symbol",
44
+ time: "Time",
45
+ text: "String",
46
+ timewithzone: "TimeWithZone"
47
+ }
48
+
49
+ attr_accessor :name, :type, :options
50
+
51
+ def initialize(name, type, options={})
52
+ type = translate_type(type)
53
+ check_field_type(type)
54
+ @name = name
55
+ @type = type
56
+ @options = options
57
+ end
58
+
59
+ #######
60
+ private
61
+ #######
62
+
63
+ # Tries to find a translation for a SQL type to a Mongoid Type
64
+ # @param [String] name Name of type
65
+ # @return [String] Translated field name or the same name if no translation is found
66
+ def translate_type(name)
67
+ TRANSLATION_TYPES[name.to_s.downcase.to_sym] || name
68
+ end
69
+
70
+ # Raises InvalidField if field type is unknown
71
+ # @param [String] name Name of type
72
+ # @raise InvalidField if name is not an accepted type
73
+ def check_field_type (name)
74
+ raise InvalidField, "Unknown field type #{name}" unless ACCEPTED_TYPES.map(&:downcase).include? name.to_s.downcase
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,60 @@
1
+ module Mongify
2
+ module Mongoid
3
+ class Model
4
+ #
5
+ # This class defines a relation for an association on a mongoid model
6
+ #
7
+ class Relation
8
+ # Embeds one relation name
9
+ EMBEDS_ONE = "embeds_one"
10
+ # Embeds many relation name
11
+ EMBEDS_MANY = "embeds_many"
12
+ # Embedded in relation name
13
+ EMBEDDED_IN = "embedded_in"
14
+ # Has one relation name
15
+ HAS_ONE = "has_one"
16
+ # Has many relation name
17
+ HAS_MANY = "has_many"
18
+ # Has and belongs to many relation name
19
+ HAS_AND_BELONGS_TO_MANY = "has_and_belongs_to_many"
20
+ # Belongs to relation name
21
+ BELONGS_TO = "belongs_to"
22
+
23
+ # Holds a list of all allowed relations
24
+ VALID_RELATIONS = [
25
+ EMBEDS_ONE,
26
+ EMBEDS_MANY,
27
+ EMBEDDED_IN,
28
+ HAS_ONE,
29
+ HAS_MANY,
30
+ HAS_AND_BELONGS_TO_MANY,
31
+ BELONGS_TO
32
+ ]
33
+
34
+ # List of fields that need to be singularized
35
+ SINGULARIZE_RELATIONS = [
36
+ BELONGS_TO,
37
+ EMBEDDED_IN,
38
+ EMBEDS_ONE
39
+ ]
40
+
41
+ # Valid Option key values
42
+ # currently not used
43
+ OPTION_KEYS = %w(class_name inverse_of)
44
+
45
+ attr_accessor :name, :association, :options
46
+
47
+ def initialize(name, association, options = {})
48
+ @name, @association, @options = name.to_s, association.to_s, options
49
+ unless VALID_RELATIONS.include?(@name)
50
+ raise Mongify::Mongoid::InvalidRelation, "Mongoid does not support the relation #{name} for model associations"
51
+ end
52
+
53
+ #Singularize association if belongs_to or embedded_in
54
+ self.association = association.singularize if SINGULARIZE_RELATIONS.include? name
55
+
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,58 @@
1
+ require 'erb'
2
+
3
+ module Mongify
4
+ module Mongoid
5
+ #
6
+ # Class that writes the models to individule mongoid model files
7
+ #
8
+ class Printer
9
+ #Template file location
10
+ TEMPLATE_FILE = File.expand_path('templates/mongoid.rb.erb', File.dirname(__FILE__))
11
+ attr_accessor :models, :output_directory
12
+ def initialize(models, output_directory)
13
+ @models = models
14
+ @output_directory = output_directory
15
+ end
16
+
17
+ # Writes models to given output directory
18
+ # @return [nil]
19
+ def write
20
+ models.each do |key, model|
21
+ write_file(model)
22
+ end
23
+ nil
24
+ end
25
+
26
+ #######
27
+ private
28
+ #######
29
+
30
+ # Returns ERB template file
31
+ # @return [String] Template file content
32
+ def template
33
+ @template ||= File.read(TEMPLATE_FILE)
34
+ end
35
+
36
+ # Writes given model to output file
37
+ # @param [Model] model Model
38
+ # @return [String] The written output
39
+ def write_file model
40
+ output = render_file model
41
+ save_file output, model.name
42
+ output
43
+ end
44
+
45
+ # Renders ERB template for given model
46
+ # @return [String] rendered template string
47
+ def render_file model
48
+ ERB.new(template, nil, '-').result(model.get_binding)
49
+ end
50
+
51
+ # Saveds text output into a file (output_directory/model_name.rb)
52
+ # @return [File] saved file
53
+ def save_file output, file_name
54
+ File.open(File.join(output_directory, "#{file_name.downcase}.rb"), 'w') {|f| f.write(output) }
55
+ end
56
+ end
57
+ end
58
+ end