├── .gitignore ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── bin └── meta ├── images ├── MetaModel.png ├── banner.png ├── import-metamodel-module.png ├── integrate-metamodel-framework.png └── logo.png ├── lib ├── metamodel.rb └── metamodel │ ├── command.rb │ ├── command │ ├── clean.rb │ ├── generate.rb │ ├── init.rb │ └── install.rb │ ├── config.rb │ ├── erbal_template.rb │ ├── installer.rb │ ├── installer │ ├── renderer.rb │ └── validator.rb │ ├── metafile.rb │ ├── metafile │ └── dsl.rb │ ├── record │ ├── association.rb │ ├── model.rb │ └── property.rb │ ├── template │ ├── association │ │ ├── belongs_to_association.swift │ │ └── has_many_association.swift │ ├── json.swift │ ├── metamodel.swift │ ├── model │ │ ├── file_header.swift │ │ ├── foreign_key.swift │ │ ├── helper.swift │ │ ├── model_delete.swift │ │ ├── model_initialize.swift │ │ ├── model_query.swift │ │ ├── model_update.swift │ │ ├── static_methods.swift │ │ └── table_initialize.swift │ ├── packing.swift │ └── triggers.swift │ ├── user_interface.rb │ └── version.rb ├── metamodel.gemspec └── scaffold └── user.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /spec/examples.txt 9 | /test/tmp/ 10 | /test/version_tmp/ 11 | /tmp/ 12 | 13 | # Used by dotenv library to load environment variables. 14 | # .env 15 | 16 | ## Specific to RubyMotion: 17 | .dat* 18 | .repl_history 19 | *.bridgesupport 20 | build-iPhoneOS/ 21 | build-iPhoneSimulator/ 22 | 23 | ## Specific to RubyMotion (use of CocoaPods): 24 | # 25 | # We recommend against adding the Pods directory to your .gitignore. However 26 | # you should judge for yourself, the pros and cons are mentioned at: 27 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 28 | # 29 | # vendor/Pods/ 30 | 31 | ## Documentation cache and generated files: 32 | /.yardoc/ 33 | /_yardoc/ 34 | /doc/ 35 | /rdoc/ 36 | 37 | ## Environment normalization: 38 | /.bundle/ 39 | /vendor/bundle 40 | /lib/bundler/man/ 41 | 42 | # for a library or gem, you might want to ignore these files since the code is 43 | # intended to run in multiple environments; otherwise, check them in: 44 | # Gemfile.lock 45 | # .ruby-version 46 | # .ruby-gemset 47 | 48 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 49 | .rvmrc 50 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | group :development do 4 | gem 'claide' 5 | gem 'xcodeproj' 6 | gem 'activesupport' 7 | end 8 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activesupport (5.2.4.3) 5 | concurrent-ruby (~> 1.0, >= 1.0.2) 6 | i18n (>= 0.7, < 2) 7 | minitest (~> 5.1) 8 | tzinfo (~> 1.1) 9 | claide (1.0.0) 10 | colored (1.2) 11 | concurrent-ruby (1.1.6) 12 | i18n (1.8.2) 13 | concurrent-ruby (~> 1.0) 14 | minitest (5.14.1) 15 | thread_safe (0.3.6) 16 | tzinfo (1.2.10) 17 | thread_safe (~> 0.1) 18 | xcodeproj (1.2.0) 19 | activesupport (>= 3) 20 | claide (>= 1.0.0, < 2.0) 21 | colored (~> 1.2) 22 | 23 | PLATFORMS 24 | ruby 25 | 26 | DEPENDENCIES 27 | activesupport 28 | claide 29 | xcodeproj 30 | 31 | BUNDLED WITH 32 | 1.12.5 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Draveness 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![MetaModel-banner](./images/banner.png) 2 | 3 | # MetaModel [![Join the chat at https://gitter.im/MModel/MetaModel](https://badges.gitter.im/MModel/MetaModel.svg)](https://gitter.im/MModel/MetaModel?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | [![License](https://img.shields.io/badge/license-MIT-green.svg?style=flat)](https://github.com/draveness/metamodel/blob/master/LICENSE) 6 | [![Gem](https://img.shields.io/gem/v/metamodel.svg?style=flat)](http://rubygems.org/gems/metamodel) 7 | [![Swift](https://img.shields.io/badge/swift-3.0-yellow.svg)](https://img.shields.io/badge/Swift-%203.0%20-yellow.svg) 8 | 9 | MetaModel is an iOS framework designed to help developer to deal with data persistent, JSON parsing and offer a bunch of APIs which provides an approach of handling client side database very easily. 10 | 11 | > MetaModel is under-development, API may constantly change before gets to 1.0.0. 12 | 13 | + [x] Dealing with database without writing SQL 14 | + [x] Most concise API to retrieve data from persistent level 15 | + [ ] Fastest JSON to model API 16 | 17 | MetaModel provides convenience chainable APIs to manipulate models like ActiveRecord. 18 | 19 | ```swift 20 | let json = ["id": 1, "name": "Buzz", "email": "i@metamodel.info"] 21 | let person = Person.parse(json) 22 | 23 | // INSERT INTO "people" (id, name, email) VALUES (1, "Buzz", "i@metamodel.info") 24 | person.save 25 | 26 | // SELECT "people".* FROM "people" WHERE "people"."id" = 1 LIMIT 1 27 | if var person = Person.find(1) { 28 | // UPDATE "people" SET "people"."name") = "Buzz" WHERE "people"."id" = 1 29 | person.update(name: "Draven") 30 | } 31 | 32 | // SELECT "people".* FROM "people" 33 | print(Person.all) 34 | ``` 35 | 36 | ## Usage 37 | 38 | Use Metafile in project folder to define your model: 39 | 40 | ```ruby 41 | metamodel_version '0.1.0' 42 | 43 | define :Article do 44 | attr :title 45 | attr :content 46 | 47 | has_many :comments 48 | end 49 | 50 | define :Comment do 51 | attr :content 52 | 53 | belongs_to :article 54 | end 55 | ``` 56 | 57 | And then run `meta build` will automatically generate all the code you need. 58 | 59 | ## Installation 60 | 61 | ``` 62 | sudo gem install metamodel --verbose 63 | ``` 64 | 65 | ## Quick Start 66 | 67 | After installation , run `meta init` in your iOS project root folder which will make a `meta` directory in current folder. 68 | 69 | ```shell 70 | $ cd /path/to/project 71 | $ meta init 72 | 73 | Initialing MetaModel project 74 | 75 | Creating `Metafile` for MetaModel 76 | ``` 77 | 78 | Generate your model meta file with `meta generate`. 79 | 80 | ```shell 81 | $ meta generate Article 82 | 83 | Generating model meta file 84 | 85 | -> Adding `Article` model to Metafile 86 | 87 | [!] Adding `Article` model scaffold to Metafile, use the command below to edit it. 88 | 89 | vim Metafile 90 | ``` 91 | 92 | Edit meta file using vim, Emacs or other editor and run `meta build`. 93 | 94 | ```shell 95 | $ meta build 96 | 97 | Building MetaModel.framework in project 98 | Existing project `./metamodel/MetaModel.xcodeproj` 99 | 100 | Analyzing Metafile 101 | -> Resolving `Article` 102 | -> Resolving `Comment` 103 | 104 | Generating model files 105 | -> Using Article.swift file 106 | -> Using Comment.swift file 107 | 108 | [!] Please drag MetaModel.framework into Embedded Binaries phrase. 109 | ``` 110 | 111 | This command build a `MetaModel.framework` in project root folder, you need to add this framework to **Embedded Binaries** phrase which located in `General` tab. 112 | 113 | ![integrate-metamodel-framework](images/integrate-metamodel-framework.png) 114 | 115 | Add this line of code in your project. 116 | 117 | ```swift 118 | import MetaModel 119 | ``` 120 | 121 | ![import-metamodel-module](images/import-metamodel-module.png) 122 | 123 | ## License 124 | 125 | This project is licensed under the terms of the MIT license. See the [LICENSE](./LICENSE) file. 126 | 127 | The MIT License (MIT) 128 | 129 | Copyright (c) 2016 Draveness 130 | 131 | Permission is hereby granted, free of charge, to any person obtaining a copy 132 | of this software and associated documentation files (the "Software"), to deal 133 | in the Software without restriction, including without limitation the rights 134 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 135 | copies of the Software, and to permit persons to whom the Software is 136 | furnished to do so, subject to the following conditions: 137 | 138 | The above copyright notice and this permission notice shall be included in all 139 | copies or substantial portions of the Software. 140 | 141 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 142 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 143 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 144 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 145 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 146 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 147 | SOFTWARE. 148 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require_relative 'lib/metamodel/version' 2 | 3 | require "pathname" 4 | 5 | task :default => [:build, :install, :clean] 6 | 7 | task :release => [:build, :push, :clean] 8 | 9 | task :push do 10 | system %(gem push #{build_product_file}) 11 | end 12 | 13 | task :build do 14 | system %(gem build metamodel.gemspec) 15 | end 16 | 17 | task :install do 18 | system %(gem install #{build_product_file}) 19 | end 20 | 21 | task :clean do 22 | system %(rm *.gem) 23 | end 24 | 25 | def build_product_file 26 | "metamodel-#{MetaModel::VERSION}.gem" 27 | end 28 | -------------------------------------------------------------------------------- /bin/meta: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'metamodel' 4 | 5 | MetaModel::Command.run(ARGV) 6 | -------------------------------------------------------------------------------- /images/MetaModel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Primix/MetaModel/709df74260aa4420e65dbcc7149d3e4c2d67f0ce/images/MetaModel.png -------------------------------------------------------------------------------- /images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Primix/MetaModel/709df74260aa4420e65dbcc7149d3e4c2d67f0ce/images/banner.png -------------------------------------------------------------------------------- /images/import-metamodel-module.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Primix/MetaModel/709df74260aa4420e65dbcc7149d3e4c2d67f0ce/images/import-metamodel-module.png -------------------------------------------------------------------------------- /images/integrate-metamodel-framework.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Primix/MetaModel/709df74260aa4420e65dbcc7149d3e4c2d67f0ce/images/integrate-metamodel-framework.png -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Primix/MetaModel/709df74260aa4420e65dbcc7149d3e4c2d67f0ce/images/logo.png -------------------------------------------------------------------------------- /lib/metamodel.rb: -------------------------------------------------------------------------------- 1 | module MetaModel 2 | 3 | class PlainInformative < StandardError; end 4 | 5 | # Indicates an user error. This is defined in cocoapods-core. 6 | # 7 | class Informative < PlainInformative 8 | def message 9 | "[!] #{super}".red 10 | end 11 | end 12 | 13 | require 'pathname' 14 | require 'active_support/inflector' 15 | require 'active_support/core_ext/string' 16 | 17 | require 'metamodel/version' 18 | require 'metamodel/config' 19 | require 'metamodel/erbal_template' 20 | 21 | # Loaded immediately after dependencies to ensure proper override of their 22 | # UI methods. 23 | # 24 | require 'metamodel/user_interface' 25 | 26 | autoload :Command, 'metamodel/command' 27 | autoload :Parser, 'metamodel/parser' 28 | autoload :Installer, 'metamodel/installer' 29 | autoload :Metafile, 'metamodel/metafile' 30 | end 31 | -------------------------------------------------------------------------------- /lib/metamodel/command.rb: -------------------------------------------------------------------------------- 1 | require 'colored' 2 | require 'claide' 3 | 4 | module MetaModel 5 | class Command < CLAide::Command 6 | require 'metamodel/command/init' 7 | require 'metamodel/command/generate' 8 | require 'metamodel/command/install' 9 | require 'metamodel/command/clean' 10 | 11 | include Config::Mixin 12 | 13 | self.abstract_command = true 14 | self.command = 'meta' 15 | self.version = VERSION 16 | self.description = 'MetaModel, the Model generator.' 17 | self.plugin_prefixes = %w(claide meta) 18 | 19 | METAMODEL_COMMAND_ALIAS = { 20 | "g" => "generate", 21 | "i" => "install", 22 | "b" => "build", 23 | "c" => "clean" 24 | } 25 | 26 | METAMODEL_OPTION_ALIAS = { 27 | "-s" => "--skip-build" 28 | } 29 | 30 | def self.run(argv) 31 | if METAMODEL_COMMAND_ALIAS[argv.first] 32 | super([METAMODEL_COMMAND_ALIAS[argv.first]] + argv[1..-1]) 33 | else 34 | super(argv) 35 | end 36 | end 37 | 38 | def self.options 39 | [ 40 | ['--skip-build', 'Skip building MetaModel framework process'] 41 | ].concat(super) 42 | end 43 | 44 | def initialize(argv) 45 | config.skip_build = argv.flag?("skip-build", false) 46 | # config.verbose = self.verbose? 47 | config.verbose = true 48 | super 49 | end 50 | 51 | def installer_for_config 52 | Installer.new(config.metafile) 53 | end 54 | 55 | #-------------------------------------------------------------------------# 56 | 57 | private 58 | 59 | # Checks that meta folder exists 60 | # 61 | # @return [void] 62 | def verify_meta_exists! 63 | unless config.metefile_exist? 64 | raise Informative, "No `meta' folder found in the project directory." 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/metamodel/command/clean.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | 3 | module MetaModel 4 | class Command 5 | class Clean < Command 6 | 7 | self.summary = "Clean MetaModel project from current folder." 8 | self.description = <<-DESC 9 | Remove MetaModel folder which contains MetaModel.xcodeproj and model 10 | files from current path. 11 | DESC 12 | 13 | def initialize(argv) 14 | super 15 | end 16 | 17 | def run 18 | UI.section "Removing MetaModel project" do 19 | FileUtils.rm_rf 'MetaModel' 20 | FileUtils.rm_rf 'MetaModel.framework' 21 | UI.message "Already clean up the whole MetaModel project from current folder" 22 | end 23 | end 24 | 25 | private 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/metamodel/command/generate.rb: -------------------------------------------------------------------------------- 1 | module MetaModel 2 | class Command 3 | class Generate < Command 4 | 5 | self.summary = "Generate a class skeleton for a model." 6 | self.description = <<-DESC 7 | Generate a skeleton for a Objective-C/Swift class and create 8 | this file as model.rb in MetaModel folder. 9 | DESC 10 | 11 | def initialize(argv) 12 | @model_name = argv.shift_argument 13 | @metafile_path = config.metafile_path 14 | super 15 | end 16 | 17 | def run 18 | verify_meta_exists! 19 | UI.section "Generating model scaffold" do 20 | title_options = { :verbose_prefix => '-> '.green } 21 | UI.titled_section "Adding `#{@model_name.camelize} model to Metafile", title_options do 22 | @metafile_path.open('a') do |source| 23 | source.puts model_template(@model_name) 24 | end 25 | end 26 | UI.notice "Adding `#{@model_name.camelize}` model scaffold to Metafile, use the command below to edit it.\n" 27 | UI.message "vim Metafile" 28 | end 29 | end 30 | 31 | private 32 | 33 | def model_template(model) 34 | <<-TEMPLATE.strip_heredoc 35 | 36 | define :#{model} do 37 | # define #{model} model like this 38 | # attr nickname, :string 39 | end 40 | TEMPLATE 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/metamodel/command/init.rb: -------------------------------------------------------------------------------- 1 | module MetaModel 2 | class Command 3 | 4 | class Init < Command 5 | self.summary = "Generate a meta folder for the current directory." 6 | self.description = <<-DESC 7 | Creates a meta folder for the current directory if none exits. Call 8 | this command before all other metamodel command. 9 | DESC 10 | 11 | def initialize(argv) 12 | @metafile_path = Pathname.pwd + 'Metafile' 13 | @project_path = argv.shift_argument 14 | super 15 | end 16 | 17 | def validate! 18 | super 19 | raise Informative, 'Existing Metafile in directory' unless config.metafile_in_dir(Pathname.pwd).nil? 20 | end 21 | 22 | def run 23 | UI.section "Initialing MetaModel project" do 24 | UI.section "Creating `Metafile` for MetaModel" do 25 | FileUtils.touch(@metafile_path) 26 | @metafile_path.open('w') do |source| 27 | source.puts "metamodel_version '#{VERSION}'\n\n" 28 | end 29 | end 30 | end 31 | end 32 | 33 | private 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/metamodel/command/install.rb: -------------------------------------------------------------------------------- 1 | require 'git' 2 | 3 | module MetaModel 4 | class Command 5 | class Install < Command 6 | include Config::Mixin 7 | self.summary = "Build a MetaModel.framework from Metafile" 8 | self.description = <<-DESC 9 | Clone a metamodel project template from GitHub, parsing Metafile, validating models, 10 | generate model swift file and build MetaModel.framework. 11 | DESC 12 | 13 | attr_accessor :models 14 | 15 | def initialize(argv) 16 | validate! 17 | super 18 | end 19 | 20 | def run 21 | UI.section "Building MetaModel.framework in project" do 22 | prepare 23 | installer = installer_for_config 24 | installer.install! 25 | end 26 | UI.section "Copying MetaModel.framework into Embedded Binaries phrase." do 27 | integrate_to_project 28 | end 29 | # UI.notice "Please drag MetaModel.framework into Embedded Binaries phrase.\n" 30 | end 31 | 32 | def prepare 33 | clone_project 34 | end 35 | 36 | def clone_project 37 | if File.exist? config.metamodel_xcode_project 38 | UI.message "Existing project `#{config.metamodel_xcode_project}`" 39 | else 40 | UI.section "Cloning MetaModel project into `./metamodel` folder" do 41 | Git.clone(config.metamodel_template_uri, 'metamodel', :depth => 1) 42 | UI.message "Using `#{config.metamodel_xcode_project}` to build module" 43 | end 44 | end 45 | end 46 | 47 | def integrate_to_project 48 | xcodeprojs = Dir.glob("#{config.installation_root}/*.xcodeproj") 49 | project = Xcodeproj::Project.open(xcodeprojs.first) 50 | target = project.targets.first 51 | return if target.build_phases.find { |build_phase| build_phase.to_s == "[MetaModel] Embedded Frameworks"} 52 | 53 | # Get useful variables 54 | frameworks_group = project.main_group.find_subpath('MetaModel', true) 55 | frameworks_group.clear 56 | frameworks_group.set_source_tree('SOURCE_ROOT') 57 | frameworks_build_phase = target.build_phases.find { |build_phase| build_phase.to_s == 'FrameworksBuildPhase' } 58 | 59 | # Add new "Embed Frameworks" build phase to target 60 | embedded_frameworks_build_phase = project.new(Xcodeproj::Project::Object::PBXCopyFilesBuildPhase) 61 | embedded_frameworks_build_phase.name = '[MetaModel] Embedded Frameworks' 62 | embedded_frameworks_build_phase.symbol_dst_subfolder_spec = :frameworks 63 | target.build_phases << embedded_frameworks_build_phase 64 | 65 | # Add framework to target as "Embedded Frameworks" 66 | framework_ref = frameworks_group.new_file("./MetaModel.framework") 67 | build_file = embedded_frameworks_build_phase.add_file_reference(framework_ref) 68 | frameworks_build_phase.add_file_reference(framework_ref) 69 | build_file.settings = { 'ATTRIBUTES' => ['CodeSignOnCopy', 'RemoveHeadersOnCopy'] } 70 | project.save 71 | end 72 | 73 | def validate! 74 | # super 75 | raise Informative, 'No Metafile in current directory' unless config.metafile_in_dir(Pathname.pwd) 76 | end 77 | 78 | private 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/metamodel/config.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/multibyte/unicode' 2 | 3 | module MetaModel 4 | # Stores the global configuration of MetaModel. 5 | # 6 | class Config 7 | 8 | DEFAULTS = { 9 | :verbose => true, 10 | :silent => false, 11 | :skip_build => false, 12 | } 13 | 14 | public 15 | 16 | attr_accessor :skip_build 17 | alias_method :skip_build?, :skip_build 18 | 19 | 20 | #-------------------------------------------------------------------------# 21 | 22 | # @!group UI 23 | 24 | # @return [Bool] Whether CocoaPods should provide detailed output about the 25 | # performed actions. 26 | # 27 | attr_accessor :verbose 28 | alias_method :verbose?, :verbose 29 | 30 | public 31 | 32 | #-------------------------------------------------------------------------# 33 | 34 | # @!group Initialization 35 | 36 | def verbose 37 | @verbose && !silent 38 | end 39 | 40 | public 41 | 42 | #-------------------------------------------------------------------------# 43 | 44 | # @!group Paths 45 | 46 | # @return [Pathname] the root of the MetaModel installation where the 47 | # meta folder is located. 48 | # 49 | def installation_root 50 | current_dir = ActiveSupport::Multibyte::Unicode.normalize(Dir.pwd) 51 | current_path = Pathname.new(current_dir) 52 | unless @installation_root 53 | until current_path.root? 54 | if metafile_in_dir(current_path) 55 | @installation_root = current_path 56 | break 57 | else 58 | current_path = current_path.parent 59 | end 60 | end 61 | @installation_root ||= Pathname.pwd 62 | end 63 | @installation_root 64 | end 65 | 66 | attr_writer :installation_root 67 | alias_method :project_root, :installation_root 68 | 69 | # Returns the path of the metamodel template uri. 70 | # 71 | # @return [String] 72 | # 73 | def metamodel_template_uri 74 | "git@github.com:MModel/MetaModel-Template.git" 75 | end 76 | 77 | # Returns the path of the MetaModel.xcodeproj. 78 | # 79 | # @return [String] 80 | # 81 | def metamodel_xcode_project 82 | "./metamodel/MetaModel.xcodeproj" 83 | end 84 | 85 | # Returns whether or not metafile is in current project. 86 | # 87 | # @return [Bool] 88 | # 89 | def metefile_exist? 90 | Pathname.new(metafile_path).exist? 91 | end 92 | 93 | # Returns the path of the Metafile. 94 | # 95 | # @return [Pathname] 96 | # @return [Nil] 97 | # 98 | def metafile_path 99 | @metafile_in_dir ||= installation_root + 'Metafile' 100 | end 101 | 102 | # Returns the path of the Metafile in the given dir if any exists. 103 | # 104 | # @param [Pathname] dir 105 | # The directory where to look for the meta. 106 | # 107 | # @return [Pathname] The path of the metafile. 108 | # @return [Nil] If not meta was found in the given dir 109 | # 110 | def metafile_in_dir(dir) 111 | candidate = dir + 'Metafile' 112 | if candidate.exist? 113 | return candidate 114 | end 115 | nil 116 | end 117 | 118 | def metafile 119 | @metafile ||= Metafile.from_file(metafile_path) if metafile_path 120 | end 121 | 122 | public 123 | 124 | #-------------------------------------------------------------------------# 125 | 126 | # @!group Singleton 127 | 128 | # @return [Config] the current config instance creating one if needed. 129 | # 130 | def self.instance 131 | @instance ||= new 132 | end 133 | 134 | # Sets the current config instance. If set to nil the config will be 135 | # recreated when needed. 136 | # 137 | # @param [Config, Nil] the instance. 138 | # 139 | # @return [void] 140 | # 141 | class << self 142 | attr_writer :instance 143 | end 144 | 145 | # Provides support for accessing the configuration instance in other 146 | # scopes. 147 | # 148 | module Mixin 149 | def config 150 | Config.instance 151 | end 152 | end 153 | end 154 | end 155 | -------------------------------------------------------------------------------- /lib/metamodel/erbal_template.rb: -------------------------------------------------------------------------------- 1 | require 'erb' 2 | require 'ostruct' 3 | 4 | module MetaModel 5 | class ErbalTemplate < OpenStruct 6 | def self.render_from_hash(t, h) 7 | ErbalTemplate.new(h).render(t) 8 | end 9 | 10 | def render(template) 11 | ERB.new(template).result(binding) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/metamodel/installer.rb: -------------------------------------------------------------------------------- 1 | module MetaModel 2 | class Installer 3 | require 'metamodel/installer/renderer' 4 | require 'metamodel/installer/validator' 5 | 6 | include Config::Mixin 7 | 8 | attr_reader :metafile 9 | 10 | attr_accessor :models 11 | attr_accessor :associations 12 | 13 | attr_accessor :current_model 14 | 15 | def initialize(metafile) 16 | @metafile = metafile 17 | end 18 | 19 | def install! 20 | @models = metafile.models 21 | @associations = metafile.associations 22 | Renderer.new(@models, @associations).tap do |renderer| 23 | renderer.render! 24 | end 25 | 26 | update_initialize_method 27 | update_packing_file 28 | build_metamodel_framework unless config.skip_build? 29 | end 30 | 31 | def update_initialize_method 32 | template = File.read File.expand_path(File.join(File.dirname(__FILE__), "./template/metamodel.swift")) 33 | result = ErbalTemplate::render_from_hash(template, { :models => @models, :associations => @associations }) 34 | model_path = Pathname.new("./metamodel/MetaModel/MetaModel.swift") 35 | File.write model_path, result 36 | end 37 | 38 | def update_packing_file 39 | template = File.read File.expand_path(File.join(File.dirname(__FILE__), "./template/packing.swift")) 40 | result = ErbalTemplate::render_from_hash(template, { :models => @models, :associations => @associations }) 41 | model_path = Pathname.new("./metamodel/MetaModel/Packing.swift") 42 | File.write model_path, result 43 | end 44 | 45 | def build_metamodel_framework 46 | UI.section "Generating MetaModel.framework" do 47 | build_framework_on_iphoneos 48 | build_framework_on_iphonesimulator 49 | copy_framework_swiftmodule_files 50 | lipo_frameworks_on_different_archs 51 | end 52 | UI.message "-> ".green + "MetaModel.framework located in current folder" 53 | end 54 | 55 | def build_framework_on_iphoneos 56 | build_iphoneos = "xcodebuild -scheme MetaModel \ 57 | -project MetaModel/MetaModel.xcodeproj \ 58 | -configuration Release -sdk iphoneos \ 59 | -derivedDataPath './metamodel' \ 60 | BITCODE_GENERATION_MODE=bitcode \ 61 | ONLY_ACTIVE_ARCH=NO \ 62 | CODE_SIGNING_REQUIRED=NO \ 63 | CODE_SIGN_IDENTITY= \ 64 | clean build" 65 | result = system "#{build_iphoneos} > /dev/null" 66 | raise Informative, 'Building framework on iphoneos failed.' unless result 67 | end 68 | 69 | def build_framework_on_iphonesimulator 70 | build_iphonesimulator = "xcodebuild -scheme MetaModel \ 71 | -project MetaModel/MetaModel.xcodeproj \ 72 | -configuration Release -sdk iphonesimulator \ 73 | -derivedDataPath './metamodel' \ 74 | ONLY_ACTIVE_ARCH=NO \ 75 | CODE_SIGNING_REQUIRED=NO \ 76 | CODE_SIGN_IDENTITY= \ 77 | clean build" 78 | result = system "#{build_iphonesimulator} > /dev/null" 79 | raise Informative, 'Building framework on iphonesimulator failed.' unless result 80 | end 81 | 82 | BUILD_PRODUCTS_FOLDER = "./metamodel/Build/Products" 83 | 84 | def copy_framework_swiftmodule_files 85 | copy_command = "cp -rf #{BUILD_PRODUCTS_FOLDER}/Release-iphoneos/MetaModel.framework . && \ 86 | cp -rf #{BUILD_PRODUCTS_FOLDER}/Release-iphonesimulator/MetaModel.framework/Modules/MetaModel.swiftmodule/* \ 87 | MetaModel.framework/Modules/MetaModel.swiftmodule/" 88 | system copy_command 89 | end 90 | 91 | def lipo_frameworks_on_different_archs 92 | lipo_command = "lipo -create -output MetaModel.framework/MetaModel \ 93 | #{BUILD_PRODUCTS_FOLDER}/Release-iphonesimulator/MetaModel.framework/MetaModel \ 94 | #{BUILD_PRODUCTS_FOLDER}/Release-iphoneos/MetaModel.framework/MetaModel" 95 | result = system "#{lipo_command}" 96 | raise Informative, 'Copy framework to current folder failed.' unless result 97 | end 98 | 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/metamodel/installer/renderer.rb: -------------------------------------------------------------------------------- 1 | require 'xcodeproj' 2 | 3 | module MetaModel 4 | class Installer 5 | class Renderer 6 | include Config::Mixin 7 | 8 | attr_reader :project 9 | 10 | attr_reader :models 11 | attr_reader :associations 12 | 13 | def initialize(models, associations) 14 | @models = models 15 | @associations = associations 16 | @project = Xcodeproj::Project.open(Config.instance.metamodel_xcode_project) 17 | end 18 | 19 | def render! 20 | remove_previous_files_refereneces 21 | UI.section "Generating model files" do 22 | render_model_files 23 | end 24 | UI.section "Generating association files" do 25 | render_association_files 26 | end 27 | @project.save 28 | end 29 | 30 | def remove_previous_files_refereneces 31 | target = @project.targets.first 32 | 33 | @models.each do |model| 34 | target.source_build_phase.files_references.each do |file_ref| 35 | target.source_build_phase.remove_file_reference(file_ref) if file_ref && "#{model.name}.swift" == file_ref.name 36 | end 37 | end 38 | 39 | @associations.each do |association| 40 | target.source_build_phase.files_references.each do |file_ref| 41 | target.source_build_phase.remove_file_reference(file_ref) if file_ref && "#{association.class_name}.swift" == file_ref.name 42 | end 43 | end 44 | end 45 | 46 | def render_model_files 47 | target = @project.targets.first 48 | 49 | models_group = @project.main_group.find_subpath('MetaModel/Models', true) 50 | models_group.clear 51 | models_group.set_source_tree('SOURCE_ROOT') 52 | 53 | file_refs = [] 54 | @models.each do |model| 55 | result = model_swift_templates.map { |template| 56 | ErbalTemplate::render_from_hash(template, { :model => model }) 57 | }.join("\n") 58 | model_path = Pathname.new("./metamodel/MetaModel/#{model.name}.swift") 59 | File.write model_path, result 60 | 61 | file_refs << models_group.new_reference(Pathname.new("MetaModel/#{model.name}.swift")) 62 | 63 | UI.message '-> '.green + "Using #{model.name}.swift file" 64 | end 65 | target.add_file_references file_refs 66 | end 67 | 68 | def render_association_files 69 | target = @project.targets.first 70 | 71 | association_group = @project.main_group.find_subpath('MetaModel/Associations', true) 72 | association_group.clear 73 | association_group.set_source_tree('SOURCE_ROOT') 74 | 75 | file_refs = [] 76 | @associations.each do |association| 77 | template = association.relation == :has_many ? has_many_association_template : belongs_to_association_template 78 | result = ErbalTemplate::render_from_hash(template, { :association => association }) 79 | file_name = "#{association.class_name}.swift" 80 | File.write Pathname.new("./metamodel/MetaModel/#{file_name}"), result 81 | 82 | file_refs << association_group.new_reference(Pathname.new("MetaModel/#{file_name}")) 83 | 84 | UI.message '-> '.green + "Using #{file_name} file" 85 | end 86 | target.add_file_references file_refs 87 | end 88 | 89 | private 90 | 91 | SWIFT_TEMPLATES_FILES = %w( 92 | file_header 93 | table_initialize 94 | model_initialize 95 | model_update 96 | model_query 97 | model_delete 98 | static_methods 99 | helper 100 | ) 101 | 102 | def model_swift_templates 103 | [].tap do |templates| 104 | SWIFT_TEMPLATES_FILES.each do |file_path| 105 | template = File.read File.expand_path(File.join(File.dirname(__FILE__), "../template/model/#{file_path}.swift")) 106 | templates << template 107 | end 108 | end 109 | end 110 | 111 | def has_many_association_template 112 | File.read File.expand_path(File.join(File.dirname(__FILE__), "../template/association/has_many_association.swift")) 113 | end 114 | 115 | def belongs_to_association_template 116 | File.read File.expand_path(File.join(File.dirname(__FILE__), "../template/association/belongs_to_association.swift")) 117 | end 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /lib/metamodel/installer/validator.rb: -------------------------------------------------------------------------------- 1 | module MetaModel 2 | class Command 3 | class Install 4 | class Validator 5 | require 'metamodel/record/model' 6 | require 'metamodel/record/property' 7 | require 'metamodel/record/association' 8 | 9 | attr_reader :models 10 | attr_reader :associations 11 | 12 | def initialize(models, associations) 13 | @models = models 14 | @associations = associations 15 | end 16 | 17 | def translate 18 | name_model_hash = Hash[@models.collect { |model| [model.name, model] }] 19 | @associations.map! do |association| 20 | major_model = name_model_hash[association.major_model] 21 | major_model.associations << association 22 | association.major_model = major_model 23 | association.secondary_model = name_model_hash[association.secondary_model] 24 | raise Informative, "Associations not satisfied in `Metafile`" \ 25 | unless [association.major_model, association.secondary_model].compact.size == 2 26 | association 27 | end 28 | 29 | satisfy_constraint = @associations.reduce([]) do |remain, association| 30 | expect = remain.select { |assoc| assoc.expect_constraint? association } 31 | if expect.empty? 32 | remain << association 33 | else 34 | remain.delete expect.first 35 | end 36 | remain 37 | end 38 | raise Informative, "Unsatisfied constraints in #{satisfy_constraint.map \ 39 | { |x| x.debug_description }}" \ 40 | if satisfy_constraint.size > 0 41 | 42 | @models.each do |model| 43 | model.properties.uniq! { |prop| [prop.name] } 44 | end 45 | return @models, @associations 46 | end 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/metamodel/metafile.rb: -------------------------------------------------------------------------------- 1 | require "metamodel/metafile/dsl" 2 | 3 | module MetaModel 4 | class Metafile 5 | include MetaModel::Metafile::DSL 6 | 7 | attr_accessor :defined_in_file 8 | attr_accessor :current_model 9 | 10 | attr_accessor :models 11 | attr_accessor :associations 12 | 13 | def initialize(defined_in_file = nil, internal_hash = {}) 14 | @defined_in_file = defined_in_file 15 | @models = [] 16 | @associations = [] 17 | 18 | evaluate_model_definition(defined_in_file) 19 | amend_association 20 | end 21 | 22 | def evaluate_model_definition(path) 23 | UI.section "Analyzing Metafile" do 24 | contents ||= File.open(path, 'r:utf-8', &:read) 25 | 26 | if contents.respond_to?(:encoding) && contents.encoding.name != 'UTF-8' 27 | contents.encode!('UTF-8') 28 | end 29 | 30 | eval(contents, nil, path.to_s) 31 | end 32 | end 33 | 34 | def self.from_file(path) 35 | path = Pathname.new(path) 36 | unless path.exist? 37 | raise Informative, "No Metafile exists at path `#{path}`." 38 | end 39 | 40 | case path.extname 41 | when '', '.metafile' 42 | Metafile.new(path) 43 | else 44 | raise Informative, "Unsupported Metafile format `#{path}`." 45 | end 46 | end 47 | 48 | def amend_association 49 | name_model_hash = Hash[@models.collect { |model| [model.name, model] }] 50 | @associations.map! do |association| 51 | major_model = name_model_hash[association.major_model] 52 | major_model.associations << association 53 | association.major_model = major_model 54 | association.secondary_model = name_model_hash[association.secondary_model] 55 | raise Informative, "Associations not satisfied in `Metafile`" unless [association.major_model, association.secondary_model].compact.size == 2 56 | association 57 | end 58 | self 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/metamodel/metafile/dsl.rb: -------------------------------------------------------------------------------- 1 | module MetaModel 2 | class Metafile 3 | module DSL 4 | require 'metamodel/record/model' 5 | require 'metamodel/record/property' 6 | require 'metamodel/record/association' 7 | 8 | def metamodel_version(version) 9 | raise Informative, 10 | "Meta file #{version} not matched with current metamodel version #{VERSION}" if version != VERSION 11 | end 12 | 13 | def define(model_name) 14 | UI.message '-> '.green + "Resolving `#{model_name.to_s.camelize}`" 15 | @current_model = Record::Model.new(model_name) 16 | yield if block_given? 17 | @models << @current_model 18 | end 19 | 20 | def attr(key, type = :string, **args) 21 | @current_model.properties << Record::Property.new(key, type, args) 22 | end 23 | 24 | def has_one(name, model_name = nil, **args) 25 | model_name = name.to_s.singularize.camelize if model_name.nil? 26 | association = Record::Association.new(name, current_model.name, model_name, :has_one, args) 27 | @associations << association 28 | end 29 | 30 | def has_many(name, model_name = nil, **args) 31 | model_name = name.to_s.singularize.camelize if model_name.nil? 32 | raise Informative, "has_many relation can't be created with optional model name" if model_name.end_with? "?" 33 | association = Record::Association.new(name, current_model.name, model_name, :has_many, args) 34 | @associations << association 35 | end 36 | 37 | def belongs_to(name, model_name = nil, **args) 38 | model_name = name.to_s.singularize.camelize if model_name.nil? 39 | association = Record::Association.new(name, current_model.name, model_name, :belongs_to, args) 40 | @associations << association 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/metamodel/record/association.rb: -------------------------------------------------------------------------------- 1 | module MetaModel 2 | module Record 3 | class Association 4 | attr_reader :name 5 | attr_reader :type 6 | attr_reader :relation 7 | attr_reader :dependent 8 | attr_accessor :major_model 9 | attr_accessor :secondary_model 10 | 11 | def initialize(name, major_model, secondary_model, relation, args) 12 | dependent = args[:dependent] || :nullify 13 | 14 | @name = name.to_s.camelize :lower 15 | @relation = relation 16 | @dependent = dependent 17 | @major_model = major_model 18 | @secondary_model = secondary_model 19 | 20 | validate_association 21 | end 22 | 23 | def class_name 24 | "#{major_model.name}#{secondary_model.name}Association".camelize 25 | end 26 | 27 | def reverse_class_name 28 | "#{secondary_model.name}#{major_model.name}Association".camelize 29 | end 30 | 31 | def major_model_id 32 | major_model.foreign_id 33 | end 34 | 35 | def secondary_model_id 36 | secondary_model.foreign_id 37 | end 38 | 39 | def hash_value 40 | self.hash.to_s(16) 41 | end 42 | 43 | def expect_constraint?(constraint) 44 | result = true 45 | result &= self.major_model == constraint.secondary_model 46 | result &= self.secondary_model == constraint.major_model 47 | 48 | result &= case [self.relation, constraint.relation] 49 | when [:has_one, :belongs_to], [:belongs_to, :has_one] then true 50 | when [:belongs_to, :has_many] then 51 | return false if self.dependent == :destroy 52 | return true 53 | when [:has_many, :belongs_to] then 54 | return false if constraint.dependent == :destroy 55 | return true 56 | when [:has_many, :has_many] then 57 | return true 58 | else false 59 | end 60 | result 61 | end 62 | 63 | #-------------------------------------------------------------------------# 64 | 65 | # @!group Validation 66 | 67 | def validate_association 68 | validate_dependent(@dependent) 69 | end 70 | 71 | def validate_dependent(dependent) 72 | supported_dependent_options = [:nullify, :destroy] 73 | raise Informative, "Unknown dependent option #{dependent}, \ 74 | MetaModel only supports #{supported_dependent_options} now" \ 75 | unless supported_dependent_options.include? dependent 76 | end 77 | 78 | #-------------------------------------------------------------------------# 79 | 80 | # @!group Relation 81 | 82 | def has_one? 83 | @relation == :has_one 84 | end 85 | 86 | def has_many? 87 | @relation == :has_many 88 | end 89 | 90 | def belongs_to? 91 | @relation == :belongs_to 92 | end 93 | 94 | def is_active? 95 | has_one? || has_many? 96 | end 97 | 98 | def type 99 | case @relation 100 | when :has_one, :has_many, :belongs_to then secondary_model.name 101 | end 102 | end 103 | 104 | def debug_description 105 | "#{major_model.name}.#{relation}.#{secondary_model.name}.#{dependent}" 106 | end 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /lib/metamodel/record/model.rb: -------------------------------------------------------------------------------- 1 | module MetaModel 2 | module Record 3 | class Model 4 | attr_reader :name 5 | attr_reader :properties 6 | attr_reader :associations 7 | 8 | def initialize(name) 9 | @name = name.to_s.camelize 10 | @properties = [] 11 | @associations = [] 12 | end 13 | 14 | def contains?(property) 15 | @properties.select { |prop| prop.name == property }.size > 0 16 | end 17 | 18 | def properties_exclude_id 19 | properties_exclude_property "id" 20 | end 21 | 22 | def properties_exclude_property(property) 23 | @properties.select { |element| element.name != property } 24 | end 25 | 26 | def foreign_id 27 | "#{name}Id".camelize(:lower) 28 | end 29 | 30 | def table_name 31 | name.tableize 32 | end 33 | 34 | def relation_name 35 | "#{name}Relation" 36 | end 37 | 38 | def hash_value 39 | self.hash.to_s(16) 40 | end 41 | 42 | def property_key_value_pairs(cast = false) 43 | key_value_pairs_with_property @properties, cast 44 | end 45 | 46 | def property_key_value_pairs_without_property(property) 47 | key_value_pairs_with_property @properties.select { |element| element.name != property } 48 | end 49 | 50 | def property_exclude_id_key_value_pairs(cast = false) 51 | key_value_pairs_with_property properties_exclude_id, cast 52 | end 53 | 54 | def property_key_type_pairs(use_default_value = false) 55 | key_type_pairs_with_property @properties, use_default_value 56 | end 57 | 58 | def property_exclude_id_key_type_pairs(use_default_value = false) 59 | key_type_pairs_with_property properties_exclude_id, use_default_value 60 | end 61 | 62 | def property_key_type_pairs_without_property(property) 63 | key_type_pairs_with_property @properties.select { |element| element.name != property } 64 | end 65 | 66 | def build_table 67 | table = "CREATE TABLE #{table_name}" 68 | main_sql = @properties.map do |property| 69 | result = "#{property.name.underscore} #{property.database_type}" 70 | result << " PRIMARY KEY" if property.is_primary? 71 | result << " UNIQUE" if property.is_unique? 72 | result << " DEFAULT #{property.default_value}" if property.has_default_value? 73 | result 74 | end 75 | # foreign_sql = @properties.map do |property| 76 | # next unless property.is_foreign? 77 | # reference_table_name = property.type.tableize 78 | # "FOREIGN KEY(#{property.name}) REFERENCES #{reference_table_name}(privateId)" 79 | # end 80 | 81 | table + "(private_id INTEGER PRIMARY KEY, #{(main_sql).compact.join(", ")});" 82 | end 83 | 84 | private 85 | 86 | def key_value_pairs_with_property(properties, cast = false) 87 | properties.map do |property| 88 | if cast 89 | "#{property.name}: #{property.type_without_optional}(#{property.name})" 90 | else 91 | "#{property.name}: #{property.name}" 92 | end 93 | end.join(", ") 94 | end 95 | 96 | def key_type_pairs_with_property(properties, use_default_value = false) 97 | properties.enum_for(:each_with_index).map do |property, index| 98 | has_default_value = property.has_default_value? 99 | default_value = property.type_without_optional == "String" ? "\"#{property.default_value}\"" : property.default_value 100 | 101 | result = "#{property.name}: #{property.type.to_s}#{if has_default_value then " = " + "#{default_value}" end}" 102 | result = "#{property.name}: #{property.type.to_s} = #{property.type_without_optional}DefaultValue" if use_default_value 103 | result 104 | end.join(", ") 105 | end 106 | 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /lib/metamodel/record/property.rb: -------------------------------------------------------------------------------- 1 | module MetaModel 2 | module Record 3 | class Property 4 | attr_accessor :name 5 | attr_reader :type 6 | attr_reader :modifiers 7 | 8 | def initialize(json_key, type = :string, *modifiers) 9 | @name = json_key.to_s.camelize(:lower) 10 | @type = convert_symbol_to_type type 11 | 12 | @modifiers = {} 13 | @modifiers.default = false 14 | 15 | modifiers.flatten.map do |modifier| 16 | @modifiers[modifier] = true if modifier.is_a? Symbol 17 | @modifiers[:default] = modifier[:default] if modifier.is_a? Hash and modifier[:default] 18 | end 19 | end 20 | 21 | class << self 22 | def primary_id 23 | property = Property.new(:privateId, :int, :primary) 24 | property.name = :privateId 25 | property 26 | end 27 | end 28 | 29 | def type_without_optional 30 | return type.to_s[0..-2] if type.to_s.end_with? "?" 31 | type 32 | end 33 | 34 | def database_type 35 | case type_without_optional 36 | when "String" then "TEXT" 37 | when "Int", "Bool" then "INTEGER" 38 | when "Double", "Date", "Float" then "REAL" 39 | else raise Informative, "Unsupported type #{self.type}" 40 | end 41 | end 42 | 43 | def real_type 44 | case type_without_optional 45 | when "String" then "String" 46 | when "Int", "Bool" then "Int64" 47 | when "Double", "Date", "Float" then "Double" 48 | else raise Informative, "Unsupported type #{self.type}" 49 | end 50 | end 51 | 52 | def convert_symbol_to_type(symbol) 53 | case symbol 54 | when :int then "Int" 55 | when :double then "Double" 56 | when :bool then "Bool" 57 | when :string then "String" 58 | when :date then "Date" 59 | else symbol.to_s.camelize 60 | end 61 | end 62 | 63 | 64 | def is_array? 65 | @type.pluralize == str 66 | end 67 | 68 | def is_unique? 69 | @modifiers.include? :unique 70 | end 71 | 72 | def is_primary? 73 | @modifiers.include? :primary 74 | end 75 | 76 | def is_optional? 77 | @type.to_s.end_with? "?" 78 | end 79 | 80 | def has_default_value? 81 | !!@modifiers[:default] 82 | end 83 | 84 | def default_value 85 | has_default_value? ? modifiers[:default] : "" 86 | end 87 | 88 | private 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/metamodel/template/association/belongs_to_association.swift: -------------------------------------------------------------------------------- 1 | // 2 | // <%= association.class_name %>.swift 3 | // MetaModel 4 | // 5 | // Created by MetaModel. 6 | // Copyright © 2016 metamodel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension <%= association.class_name %> { 12 | @discardableResult static func create(<%= association.major_model_id %>: Int, <%= association.secondary_model_id %>: Int) { 13 | executeSQL("INSERT INTO \(<%= association.class_name %>.tableName) (<%= association.major_model_id.underscore %>, <%= association.secondary_model_id.underscore %>) VALUES (\(<%= association.major_model_id %>), \(<%= association.secondary_model_id %>))") 14 | } 15 | } 16 | 17 | public extension <%= association.major_model.name %> { 18 | var <%= association.name %>: <%= association.secondary_model.name %>? { 19 | get { 20 | guard let id = <%= association.class_name %>.findBy(<%= association.major_model.foreign_id %>: privateId).first?.<%= association.secondary_model.foreign_id %> else { return nil } 21 | return <%= association.secondary_model.name %>.find(id) 22 | } 23 | set { 24 | guard let newValue = newValue else { return } 25 | <%= association.class_name %>.findBy(<%= association.major_model_id %>: privateId).forEach { $0.delete } 26 | <%= association.class_name %>.create(<%= association.major_model_id %>: newValue.privateId, <%= association.secondary_model_id %>: privateId) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/metamodel/template/association/has_many_association.swift: -------------------------------------------------------------------------------- 1 | // 2 | // <%= association.class_name %>.swift 3 | // MetaModel 4 | // 5 | // Created by MetaModel. 6 | // Copyright © 2016 metamodel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | typealias <%= association.reverse_class_name %> = <%= association.class_name %> 12 | 13 | struct <%= association.class_name %> { 14 | var privateId: Int = 0 15 | var <%= association.major_model_id %>: Int = 0 16 | var <%= association.secondary_model_id %>: Int = 0 17 | 18 | enum Association: String, CustomStringConvertible { 19 | case privateId = "private_id" 20 | case <%= association.major_model_id %> = "<%= association.major_model_id.underscore %>" 21 | case <%= association.secondary_model_id %> = "<%= association.secondary_model_id.underscore %>" 22 | var description: String { get { return self.rawValue } } 23 | } 24 | <% [association.major_model, association.secondary_model].zip([association.secondary_model, association.major_model]).each do |first, second| %> 25 | static func fetch<%= first.table_name.camelize %>(<%= second.foreign_id %>: Int, first: Bool = false) -> [<%= first.name %>] { 26 | var query = "SELECT * FROM <%= first.table_name %> WHERE <%= first.table_name %>.private_id IN (" + 27 | "SELECT private_id " + 28 | "FROM \(tableName) " + 29 | "WHERE \(Association.<%= second.foreign_id %>) = \(<%= second.foreign_id %>)" + 30 | ")" 31 | if first { query += "LIMIT 1" } 32 | return MetaModels.fromQuery(query) 33 | } 34 | <% end %><% [association.major_model, association.secondary_model].each do |model| %> 35 | static func findBy(<%= model.foreign_id %>: Int) -> [<%= association.class_name %>] { 36 | let query = "SELECT * FROM \(tableName) WHERE <%= model.foreign_id.underscore %> = \(<%= model.foreign_id %>)" 37 | return MetaModels.fromQuery(query) 38 | } 39 | <% end %> 40 | var delete: Void { 41 | get { 42 | executeSQL("DELETE * FROM \(<%= association.class_name %>.tableName) WHERE private_id = \(privateId)") 43 | } 44 | } 45 | } 46 | 47 | extension <%= association.class_name %> { 48 | static func create(<%= association.major_model_id %>: Int, <%= association.secondary_model_id %>: Int) { 49 | executeSQL("INSERT INTO \(<%= association.class_name %>.tableName) (<%= association.major_model_id.underscore %>, <%= association.secondary_model_id.underscore %>) VALUES (\(<%= association.major_model_id %>), \(<%= association.secondary_model_id %>))") 50 | } 51 | } 52 | 53 | extension <%= association.class_name %> { 54 | static let tableName = "<%= association.class_name.underscore %>" 55 | static func initialize() { 56 | let initializeTableSQL = "CREATE TABLE \(tableName)(" + 57 | "private_id INTEGER PRIMARY KEY, " + 58 | "<%= association.major_model_id.underscore %> INTEGER NOT NULL, " + 59 | "<%= association.secondary_model_id.underscore %> INTEGER NOT NULL, " + 60 | "FOREIGN KEY(<%= association.major_model_id.underscore %>) REFERENCES <%= association.major_model.table_name %>(private_id)," + 61 | "FOREIGN KEY(<%= association.secondary_model_id.underscore %>) REFERENCES <%= association.secondary_model.table_name %>(private_id)" + 62 | ");" 63 | 64 | executeSQL(initializeTableSQL) 65 | initializeTrigger() 66 | } 67 | 68 | static func deinitialize() { 69 | let dropTableSQL = "DROP TABLE \(tableName)" 70 | executeSQL(dropTableSQL) 71 | deinitializeTrigger() 72 | } 73 | 74 | static func initializeTrigger() { 75 | let majorDeleteTrigger = "CREATE TRIGGER <%= association.major_model.name.underscore %>_delete_trigger " + 76 | "AFTER DELETE ON <%= association.major_model.table_name %> " + 77 | "FOR EACH ROW BEGIN " + 78 | "DELETE FROM \(tableName) WHERE private_id = OLD.private_id; " + 79 | "END;"; 80 | 81 | let secondaryDeleteTrigger = "CREATE TRIGGER <%= association.secondary_model.name.underscore %>_delete_trigger " + 82 | "AFTER DELETE ON <%= association.secondary_model.table_name %> " + 83 | "FOR EACH ROW BEGIN " + 84 | "DELETE FROM \(tableName) WHERE private_id = OLD.private_id; " + 85 | "END;"; 86 | 87 | executeSQL(majorDeleteTrigger) 88 | executeSQL(secondaryDeleteTrigger) 89 | } 90 | 91 | static func deinitializeTrigger() { 92 | let dropMajorTrigger = "DROP TRIGGER IF EXISTS <%= association.major_model.name.underscore %>_delete_trigger;" 93 | executeSQL(dropMajorTrigger) 94 | 95 | let dropSecondaryTrigger = "DROP TRIGGER IF EXISTS <%= association.secondary_model.name.underscore %>_delete_trigger;" 96 | executeSQL(dropSecondaryTrigger) 97 | } 98 | } 99 | 100 | public extension <%= association.major_model.name %> { 101 | var <%= association.name %>: [<%= association.secondary_model.name %>] { 102 | get { 103 | return <%= association.class_name %>.fetch<%= association.secondary_model.name.tableize.camelize %>(<%= association.major_model.foreign_id %>: privateId) 104 | } 105 | set { 106 | <%= association.class_name %>.findBy(<%= association.major_model_id %>: privateId).forEach { $0.delete } 107 | newValue.forEach { <%= association.class_name %>.create(<%= association.major_model_id %>: privateId, <%= association.secondary_model_id %>: $0.privateId) } 108 | } 109 | } 110 | 111 | @discardableResult func create<%= association.secondary_model.name %>(<%= association.secondary_model.property_key_type_pairs %>) -> <%= association.secondary_model.name %>? { 112 | guard let result = <%= association.secondary_model.name %>.create(<%= association.secondary_model.property_key_value_pairs %>) else { return nil } 113 | <%= association.class_name %>.create(<%= association.major_model_id %>: privateId, <%= association.secondary_model_id %>: result.privateId) 114 | return result 115 | } 116 | 117 | @discardableResult func append<%= association.secondary_model.name %>(<%= association.secondary_model.property_key_type_pairs %>) -> <%= association.secondary_model.name %>? { 118 | return create<%= association.secondary_model.name %>(<%= association.secondary_model.property_key_value_pairs %>) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /lib/metamodel/template/json.swift: -------------------------------------------------------------------------------- 1 | extension <%= model.name %> { 2 | public static func parse(json: [String: AnyObject]) -> <%= model.name %> { 3 | let id: Int = json["id"] as! Int 4 | <% model.properties_exclude_id.each do |property| %> 5 | <%= """let #{property.name}: #{property.type} = json[\"#{property.name}\"] as! #{property.type}""" %> 6 | <% end %> 7 | return <%= model.name %>(<%= model.property_key_value_pairs %>) 8 | } 9 | 10 | public static func parse(jsons: [[String: AnyObject]]) -> [<%= model.name %>] { 11 | return jsons.map(<%= model.name %>.parse) 12 | } 13 | 14 | public static func parse(data: NSData) throws -> <%= model.name %> { 15 | let json = try NSJSONSerialization.JSONObjectWithData(data, options: .AllowFragments) as! [String: AnyObject] 16 | return <%= model.name %>.parse(json) 17 | } 18 | 19 | public static func parses(data: NSData) throws -> [<%= model.name %>] { 20 | let json = try NSJSONSerialization.JSONObjectWithData(data, options: .AllowFragments) as! [[String: AnyObject]] 21 | return <%= model.name %>.parse(json) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /lib/metamodel/template/metamodel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MetaModel.swift 3 | // MetaModel 4 | // 5 | // Created by MetaModel. 6 | // Copyright © 2016 MetaModel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class MetaModel { 12 | public static func initialize() { 13 | validateMetaModelTables() 14 | } 15 | static func validateMetaModelTables() { 16 | createMetaModelTable() 17 | let infos = retrieveMetaModelTableInfos() 18 | <% models.each do |model| %><%= """if infos[#{model.name}.tableName] != \"#{model.hash_value}\" { 19 | updateMetaModelTableInfos(#{model.name}.tableName, hashValue: \"#{model.hash_value}\") 20 | #{model.name}.deinitialize() 21 | #{model.name}.initialize() 22 | }""" %> 23 | <% end %> 24 | 25 | <% associations.each do |association| %><% if association.is_active? %><%= """if infos[#{association.class_name}.tableName] != \"#{association.hash_value}\" { 26 | updateMetaModelTableInfos(#{association.class_name}.tableName, hashValue: \"#{association.hash_value}\") 27 | #{association.class_name}.deinitialize() 28 | #{association.class_name}.initialize() 29 | }""" %> 30 | <% end %><% end %> 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/metamodel/template/model/file_header.swift: -------------------------------------------------------------------------------- 1 | // 2 | // <%= model.name %>.swift 3 | // MetaModel 4 | // 5 | // Created by MetaModel. 6 | // Copyright © 2016 MetaModel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | -------------------------------------------------------------------------------- /lib/metamodel/template/model/foreign_key.swift: -------------------------------------------------------------------------------- 1 | // MARK: - Association 2 | <% model.associations.each do |association| %><% if association.has_many? %> 3 | <%= """public extension #{model.name} { 4 | var #{association.name}: #{association.secondary_model.relation_name} { 5 | get { 6 | var result = #{association.type}.filter(#{model.foreign_id}: privateId) 7 | result.#{model.foreign_id} = privateId 8 | return result 9 | } 10 | set { 11 | #{association.name}.forEach { (element) in 12 | var element = element 13 | element.update(#{model.foreign_id}: 0) 14 | } 15 | newValue.forEach { (element) in 16 | var element = element 17 | element.update(#{model.foreign_id}: privateId) 18 | } 19 | #{association.name}.#{model.foreign_id} = privateId 20 | } 21 | } 22 | }""" %><% elsif association.belongs_to? %> 23 | <%= """public extension #{model.name} { 24 | var #{association.name}: #{association.type}? { 25 | get { 26 | return #{association.secondary_model_instance} 27 | } 28 | set { 29 | guard let newValue = newValue else { return } 30 | update(#{association.secondary_model.foreign_id}: newValue.privateId) 31 | } 32 | } 33 | 34 | }""" %><% elsif association.has_one? %> 35 | <%= """public extension #{model.name} { 36 | var #{association.name}: #{association.type}? { 37 | get { 38 | return #{association.secondary_model_instance}.first 39 | } 40 | set { 41 | #{association.type}.findBy(#{model.foreign_id}: privateId).deleteAll 42 | guard var newValue = newValue else { return } 43 | newValue.update(#{model.foreign_id}: privateId) 44 | } 45 | } 46 | }"""%><% end %><% end %> 47 | -------------------------------------------------------------------------------- /lib/metamodel/template/model/helper.swift: -------------------------------------------------------------------------------- 1 | // MAKR: - Helper 2 | 3 | open class <%= model.relation_name %>: Relation<<%= model.name %>> { 4 | override init() { 5 | super.init() 6 | self.select = "SELECT \(<%= model.name %>.tableName).* FROM \(<%= model.name %>.tableName)" 7 | } 8 | 9 | override var result: [<%= model.name %>] { 10 | get { 11 | return MetaModels.fromQuery(query) 12 | } 13 | } 14 | 15 | func expandColumn(_ column: <%= model.name %>.Column) -> String { 16 | return "\(<%= model.name %>.tableName).\(column)" 17 | } 18 | } 19 | 20 | extension <%= model.name %> { 21 | var itself: String { get { return "WHERE \(<%= model.name %>.tableName).private_id = \(privateId)" } } 22 | } 23 | 24 | extension <%= model.relation_name %> { 25 | func find(_ privateId: Int) -> Self { 26 | return filter(privateId) 27 | } 28 | 29 | func find(_ privateIds: [Int]) -> Self { 30 | return filter(conditions: [.privateId: privateIds]) 31 | } 32 | 33 | func filter(_ privateId: Int) -> Self { 34 | self.filter.append("private_id = \(privateId)") 35 | return self 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/metamodel/template/model/model_delete.swift: -------------------------------------------------------------------------------- 1 | // MARK: - Delete 2 | 3 | public extension <%= model.name %> { 4 | var delete: Void { 5 | get { 6 | let deleteSQL = "DELETE FROM \(<%= model.name %>.tableName) \(itself)" 7 | executeSQL(deleteSQL) 8 | } 9 | } 10 | static var deleteAll: Void { get { return <%= model.relation_name %>().deleteAll } } 11 | } 12 | 13 | public extension <%= model.relation_name %> { 14 | var delete: Void { get { return deleteAll } } 15 | 16 | var deleteAll: Void { 17 | get { 18 | self.result.forEach { $0.delete } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/metamodel/template/model/model_initialize.swift: -------------------------------------------------------------------------------- 1 | public struct <%= model.name %> { 2 | var privateId: Int = 0 3 | <% model.properties.each do |property| %><%= """public var #{property.name}: #{property.type}""" %> 4 | <% end %> 5 | static let tableName = "<%= model.table_name %>" 6 | 7 | public enum Column: String, CustomStringConvertible { 8 | <% model.properties.each do |property| %><%= """case #{property.name} = \"#{property.name.underscore}\"""" %> 9 | <% end %> 10 | case privateId = "private_id" 11 | 12 | public var description: String { get { return self.rawValue } } 13 | } 14 | 15 | public init(<%= model.property_key_type_pairs %>) { 16 | <% model.properties.each do |property| %><%= """self.#{property.name} = #{property.name}" %> 17 | <% end %> 18 | } 19 | 20 | @discardableResult static public func new(<%= model.property_key_type_pairs %>) -> <%= model.name %> { 21 | return <%= model.name %>(<%= model.property_key_value_pairs %>) 22 | } 23 | 24 | @discardableResult static public func create(<%= model.property_key_type_pairs %>) -> <%= model.name %>? { 25 | //if <%= model.properties.select { |p| p.name.downcase.end_with? "id" }.map { |p| "#{p.name} == 0" }.push("false == true").join(" || ") %> { return nil } 26 | 27 | var columnsSQL: [<%= model.name %>.Column] = [] 28 | var valuesSQL: [Unwrapped] = [] 29 | 30 | <% model.properties.each do |property| %><% if property.is_optional? %> 31 | <%= """if let #{property.name} = #{property.name} { 32 | columnsSQL.append(.#{property.name}) 33 | valuesSQL.append(#{property.name}) 34 | }""" %><% else %> 35 | <%= """columnsSQL.append(.#{property.name}) 36 | valuesSQL.append(#{property.name}) 37 | """ %><% end %><% end %> 38 | let insertSQL = "INSERT INTO \(tableName) (\(columnsSQL.map { $0.rawValue }.joined(separator: ", "))) VALUES (\(valuesSQL.map { $0.unwrapped }.joined(separator: ", ")))" 39 | guard let _ = executeSQL(insertSQL), 40 | let lastInsertRowId = executeScalarSQL("SELECT last_insert_rowid();") as? Int64 else { return nil } 41 | var result = <%= model.name %>(<%= model.property_key_value_pairs %>) 42 | result.privateId = Int(lastInsertRowId) 43 | return result 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/metamodel/template/model/model_query.swift: -------------------------------------------------------------------------------- 1 | // MARK: - Query 2 | 3 | public extension <%= model.name %> { 4 | static var all: <%= model.relation_name %> { 5 | get { return <%= model.relation_name %>() } 6 | } 7 | 8 | static var first: <%= model.name %>? { 9 | get { 10 | return <%= model.relation_name %>().orderBy(column: .privateId, asc: true).first 11 | } 12 | } 13 | 14 | static var last: <%= model.name %>? { 15 | get { 16 | return <%= model.relation_name %>().orderBy(column: .privateId, asc: false).first 17 | } 18 | } 19 | 20 | static func first(length: UInt) -> <%= model.relation_name %> { 21 | return <%= model.relation_name %>().orderBy(column: .privateId, asc: true).limit(length) 22 | } 23 | 24 | static func last(length: UInt) -> <%= model.relation_name %> { 25 | return <%= model.relation_name %>().orderBy(column: .privateId, asc: false).limit(length) 26 | } 27 | 28 | internal static func find(_ privateId: Int) -> <%= model.name %>? { 29 | return <%= model.relation_name %>().find(privateId).first 30 | } 31 | 32 | internal static func find(_ privateIds: [Int]) -> <%= model.relation_name %> { 33 | return <%= model.relation_name %>().find(privateIds) 34 | } 35 | 36 | static func findBy(<%= model.property_key_type_pairs(true) %>) -> <%= model.relation_name %> { 37 | return <%= model.relation_name %>().findBy(<%= model.property_key_value_pairs %>) 38 | } 39 | 40 | static func filter(<%= model.property_key_type_pairs(true) %>) -> <%= model.relation_name %> { 41 | return <%= model.relation_name %>().filter(<%= model.property_key_value_pairs %>) 42 | } 43 | 44 | static func limit(length: UInt, offset: UInt = 0) -> <%= model.relation_name %> { 45 | return <%= model.relation_name %>().limit(length, offset: offset) 46 | } 47 | 48 | static func take(length: UInt) -> <%= model.relation_name %> { 49 | return <%= model.relation_name %>().limit(length) 50 | } 51 | 52 | static func offset(offset: UInt) -> <%= model.relation_name %> { 53 | return <%= model.relation_name %>().offset(offset) 54 | } 55 | 56 | static func groupBy(columns: <%= model.name %>.Column...) -> <%= model.relation_name %> { 57 | return <%= model.relation_name %>().groupBy(columns: columns) 58 | } 59 | 60 | static func groupBy(columns: [<%= model.name %>.Column]) -> <%= model.relation_name %> { 61 | return <%= model.relation_name %>().groupBy(columns: columns) 62 | } 63 | 64 | static func orderBy(column: <%= model.name %>.Column) -> <%= model.relation_name %> { 65 | return <%= model.relation_name %>().orderBy(column: column) 66 | } 67 | 68 | static func orderBy(column: <%= model.name %>.Column, asc: Bool) -> <%= model.relation_name %> { 69 | return <%= model.relation_name %>().orderBy(column: column, asc: asc) 70 | } 71 | } 72 | 73 | public extension <%= model.relation_name %> { 74 | func findBy(<%= model.property_key_type_pairs(true) %>) -> Self { 75 | var attributes: [<%= model.name %>.Column: Any] = [:] 76 | <% model.properties.each do |property| %><%= "if (#{property.name} != #{property.type_without_optional}DefaultValue) { attributes[.#{property.name}] = #{property.name} }" %> 77 | <% end %>return self.filter(conditions: attributes) 78 | } 79 | 80 | func filter(<%= model.property_key_type_pairs(true) %>) -> Self { 81 | return findBy(<%= model.property_key_value_pairs %>) 82 | } 83 | 84 | func filter(conditions: [<%= model.name %>.Column: Any]) -> Self { 85 | for (column, value) in conditions { 86 | let columnSQL = "\(expandColumn(column))" 87 | 88 | func filterByEqual(_ value: Any) { 89 | self.filter.append("\(columnSQL) = \(value)") 90 | } 91 | 92 | func filterByIn(_ value: [String]) { 93 | self.filter.append("\(columnSQL) IN (\(value.joined(separator: ", ")))") 94 | } 95 | 96 | if let value = value as? String { 97 | filterByEqual(value) 98 | } else if let value = value as? Int { 99 | filterByEqual(value) 100 | } else if let value = value as? Double { 101 | filterByEqual(value) 102 | } else if let value = value as? [String] { 103 | filterByIn(value.map { $0 }) 104 | } else if let value = value as? [Int] { 105 | filterByIn(value.map { $0.description }) 106 | } else if let value = value as? [Double] { 107 | filterByIn(value.map { $0.description }) 108 | } else { 109 | let valueMirror = Mirror(reflecting: value) 110 | print("!!!: UNSUPPORTED TYPE \(valueMirror.subjectType)") 111 | } 112 | 113 | } 114 | return self 115 | } 116 | 117 | func groupBy(columns: <%= model.name %>.Column...) -> Self { 118 | return self.groupBy(columns: columns) 119 | } 120 | 121 | func groupBy(columns: [<%= model.name %>.Column]) -> Self { 122 | func groupBy(column: <%= model.name %>.Column) { 123 | self.group.append("\(expandColumn(column))") 124 | } 125 | _ = columns.flatMap(groupBy) 126 | return self 127 | } 128 | 129 | func orderBy(column: <%= model.name %>.Column) -> Self { 130 | self.order.append("\(expandColumn(column))") 131 | return self 132 | } 133 | 134 | func orderBy(column: <%= model.name %>.Column, asc: Bool) -> Self { 135 | self.order.append("\(expandColumn(column)) \(asc ? "ASC" : "DESC")") 136 | return self 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /lib/metamodel/template/model/model_update.swift: -------------------------------------------------------------------------------- 1 | // MARK: - Update 2 | 3 | public extension <%= model.name %> { 4 | @discardableResult mutating func update(<%= model.property_exclude_id_key_type_pairs true %>) { 5 | var attributes: [<%= model.name %>.Column: Any] = [:] 6 | <% model.properties_exclude_id.each do |property| %><%= "if (#{property.name} != #{property.type_without_optional}DefaultValue) { attributes[.#{property.name}] = #{property.name} }" %> 7 | <% end %> 8 | self.update(attributes: attributes) 9 | } 10 | 11 | @discardableResult mutating func update(attributes: [<%= model.name %>.Column: Any]) { 12 | var setSQL: [String] = [] 13 | if let attributes = attributes as? [<%= model.name %>.Column: Unwrapped] { 14 | for (key, value) in attributes { 15 | switch key { 16 | <% model.properties_exclude_id.each do |property| %><%= """case .#{property.name}: setSQL.append(\"\\(key) = \\(value.unwrapped)\")""" %> 17 | <% end %>default: break 18 | } 19 | } 20 | let updateSQL = "UPDATE \(<%= model.name %>.tableName) SET \(setSQL.joined(separator: ", ")) \(itself)" 21 | guard let _ = executeSQL(updateSQL) else { return } 22 | for (key, value) in attributes { 23 | switch key { 24 | <% model.properties_exclude_id.each do |property| %><%= """case .#{property.name}: #{property.name} = value as#{property.is_optional? ? "?" : "!"} #{property.type_without_optional}""" %> 25 | <% end %>default: break 26 | } 27 | } 28 | } 29 | } 30 | 31 | var save: <%= model.name %> { 32 | mutating get { 33 | if let _ = <%= model.name %>.find(privateId) { 34 | update(attributes: [<% column_values = model.properties.map do |property| %><% ".#{property.name}: #{property.name}" %><% end %><%= column_values.join(", ") %>]) 35 | } else { 36 | <%= model.name %>.create(<%= model.property_key_value_pairs %>) 37 | } 38 | return self 39 | } 40 | } 41 | 42 | var commit: <%= model.name %> { 43 | mutating get { 44 | return save 45 | } 46 | } 47 | } 48 | 49 | public extension <%= model.relation_name %> { 50 | @discardableResult public func updateAll(<%= model.property_exclude_id_key_type_pairs true %>) -> Self { 51 | return update(<%= model.property_exclude_id_key_value_pairs %>) 52 | } 53 | 54 | @discardableResult public func update(<%= model.property_exclude_id_key_type_pairs true %>) -> Self { 55 | var attributes: [<%= model.name %>.Column: Any] = [:] 56 | <% model.properties_exclude_id.each do |property| %><%= "if (#{property.name} != #{property.type_without_optional}DefaultValue) { attributes[.#{property.name}] = #{property.name} }" %> 57 | <% end %> 58 | result.forEach { (element) in 59 | var element = element 60 | element.update(attributes: attributes) 61 | } 62 | return self 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/metamodel/template/model/static_methods.swift: -------------------------------------------------------------------------------- 1 | public extension <%= model.name %> { 2 | static var count: Int { 3 | get { 4 | let countSQL = "SELECT count(*) FROM \(tableName)" 5 | guard let count = executeScalarSQL(countSQL) as? Int64 else { return 0 } 6 | return Int(count) 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/metamodel/template/model/table_initialize.swift: -------------------------------------------------------------------------------- 1 | extension <%= model.name %> { 2 | static func initialize() { 3 | let initializeTableSQL = "<%= model.build_table %>" 4 | executeSQL(initializeTableSQL) 5 | } 6 | static func deinitialize() { 7 | let dropTableSQL = "DROP TABLE \(tableName)" 8 | executeSQL(dropTableSQL) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/metamodel/template/packing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Packing.swift 3 | // MetaModel 4 | // 5 | // Created by Draveness on 9/16/16. 6 | // Copyright © 2016 metamodel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol Packing { 12 | init(values: Array>); 13 | } 14 | 15 | class MetaModels { 16 | static func fromQuery(_ query: String) -> [T] where T: Packing { 17 | var models: [T] = [] 18 | guard let stmt = executeSQL(query) else { return models } 19 | for values in stmt { 20 | let association = T(values: values) 21 | models.append(association) 22 | } 23 | return models 24 | } 25 | } 26 | 27 | // MARK: - Model Packing 28 | <% models.each do |model| %> 29 | extension <%= model.name %>: Packing { 30 | init(values: Array>) { 31 | <% model.properties.each_with_index do |property, index| %><%= """let #{property.name}: #{property.real_type} = values[#{index+1}] as! #{property.real_type}""" %> 32 | <% end %> 33 | self.init(<%= model.property_key_value_pairs true %>) 34 | 35 | let privateId: Int64 = values[0] as! Int64 36 | self.privateId = Int(privateId) 37 | } 38 | } 39 | <% end %> 40 | 41 | // MARK: - Association Packing 42 | <% associations.each do |association| if association.is_active? %> 43 | extension <%= association.class_name %>: Packing { 44 | init(values: Array>) { 45 | let privateId: Int64 = values[0] as! Int64 46 | let <%= association.major_model_id %>: Int64 = values[1] as! Int64 47 | let <%= association.secondary_model_id %>: Int64 = values[2] as! Int64 48 | 49 | self.init(privateId: Int(privateId), <%= association.major_model_id %>: Int(<%= association.major_model_id %>), <%= association.secondary_model_id %>: Int(<%= association.secondary_model_id %>)) 50 | } 51 | } 52 | <% end %><% end %> 53 | -------------------------------------------------------------------------------- /lib/metamodel/template/triggers.swift: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Primix/MetaModel/709df74260aa4420e65dbcc7149d3e4c2d67f0ce/lib/metamodel/template/triggers.swift -------------------------------------------------------------------------------- /lib/metamodel/user_interface.rb: -------------------------------------------------------------------------------- 1 | module MetaModel 2 | # The code in this file is mainly borrowed from cocoapods/user_interface.rb 3 | # which used to generate output messages to user. 4 | module UserInterface 5 | require 'colored' 6 | 7 | @title_colors = %w( yellow green ) 8 | @title_level = 0 9 | @indentation_level = 2 10 | @treat_titles_as_messages = false 11 | @warnings = [] 12 | 13 | class << self 14 | include Config::Mixin 15 | 16 | attr_accessor :indentation_level 17 | attr_accessor :title_level 18 | attr_accessor :warnings 19 | 20 | # @return [IO] IO object to which UI output will be directed. 21 | # 22 | attr_accessor :output_io 23 | 24 | # @return [Bool] Whether the wrapping of the strings to the width of the 25 | # terminal should be disabled. 26 | # 27 | attr_accessor :disable_wrap 28 | alias_method :disable_wrap?, :disable_wrap 29 | 30 | # Prints a title taking an optional verbose prefix and 31 | # a relative indentation valid for the UI action in the passed 32 | # block. 33 | # 34 | # In verbose mode titles are printed with a color according 35 | # to their level. In normal mode titles are printed only if 36 | # they have nesting level smaller than 2. 37 | # 38 | # @todo Refactor to title (for always visible titles like search) 39 | # and sections (titles that represent collapsible sections). 40 | # 41 | # @param [String] title 42 | # The title to print 43 | # 44 | # @param [String] verbose_prefix 45 | # See #message 46 | # 47 | # @param [FixNum] relative_indentation 48 | # The indentation level relative to the current, 49 | # when the message is printed. 50 | # 51 | def section(title, verbose_prefix = '', relative_indentation = 0) 52 | if config.verbose? 53 | title(title, verbose_prefix, relative_indentation) 54 | elsif title_level < 1 55 | puts title 56 | end 57 | 58 | self.indentation_level += relative_indentation 59 | self.title_level += 1 60 | yield if block_given? 61 | self.indentation_level -= relative_indentation 62 | self.title_level -= 1 63 | end 64 | 65 | # In verbose mode it shows the sections and the contents. 66 | # In normal mode it just prints the title. 67 | # 68 | # @return [void] 69 | # 70 | def titled_section(title, options = {}) 71 | relative_indentation = options[:relative_indentation] || 0 72 | verbose_prefix = options[:verbose_prefix] || '' 73 | if config.verbose? 74 | title(title, verbose_prefix, relative_indentation) 75 | else 76 | puts title 77 | end 78 | 79 | self.indentation_level += relative_indentation 80 | self.title_level += 1 81 | yield if block_given? 82 | self.indentation_level -= relative_indentation 83 | self.title_level -= 1 84 | end 85 | 86 | # A title opposed to a section is always visible 87 | # 88 | # @param [String] title 89 | # The title to print 90 | # 91 | # @param [String] verbose_prefix 92 | # See #message 93 | # 94 | # @param [FixNum] relative_indentation 95 | # The indentation level relative to the current, 96 | # when the message is printed. 97 | # 98 | def title(title, verbose_prefix = '', relative_indentation = 2) 99 | if @treat_titles_as_messages 100 | message(title, verbose_prefix) 101 | else 102 | title = verbose_prefix + title if config.verbose? 103 | title = "\n#{title}" if @title_level < 2 104 | if (color = @title_colors[@title_level]) 105 | title = title.send(color) 106 | end 107 | puts "#{title}" 108 | end 109 | 110 | self.indentation_level += relative_indentation 111 | self.title_level += 1 112 | yield if block_given? 113 | self.indentation_level -= relative_indentation 114 | self.title_level -= 1 115 | end 116 | 117 | # Prints a verbose message taking an optional verbose prefix and 118 | # a relative indentation valid for the UI action in the passed 119 | # block. 120 | # 121 | # @todo Clean interface. 122 | # 123 | # @param [String] message 124 | # The message to print. 125 | # 126 | # @param [String] verbose_prefix 127 | # See #message 128 | # 129 | # @param [FixNum] relative_indentation 130 | # The indentation level relative to the current, 131 | # when the message is printed. 132 | # 133 | def message(message, verbose_prefix = '', relative_indentation = 2) 134 | message = verbose_prefix + message if config.verbose? 135 | puts_indented message if config.verbose? 136 | 137 | self.indentation_level += relative_indentation 138 | yield if block_given? 139 | self.indentation_level -= relative_indentation 140 | end 141 | 142 | # Prints an info to the user. The info is always displayed. 143 | # It respects the current indentation level only in verbose 144 | # mode. 145 | # 146 | # Any title printed in the optional block is treated as a message. 147 | # 148 | # @param [String] message 149 | # The message to print. 150 | # 151 | def info(message) 152 | indentation = config.verbose? ? self.indentation_level : 0 153 | indented = wrap_string(message, indentation) 154 | puts(indented) 155 | 156 | self.indentation_level += 2 157 | @treat_titles_as_messages = true 158 | yield if block_given? 159 | @treat_titles_as_messages = false 160 | self.indentation_level -= 2 161 | end 162 | 163 | # Prints an important message to the user. 164 | # 165 | # @param [String] message The message to print. 166 | # 167 | # return [void] 168 | # 169 | def notice(message) 170 | puts("\n[!] #{message}".green) 171 | end 172 | 173 | # Returns a string containing relative location of a path from the Podfile. 174 | # The returned path is quoted. If the argument is nil it returns the 175 | # empty string. 176 | # 177 | # @param [#to_str] pathname 178 | # The path to print. 179 | # 180 | def path(pathname) 181 | if pathname 182 | from_path = config.podfile_path.dirname if config.podfile_path 183 | from_path ||= Pathname.pwd 184 | path = begin 185 | Pathname(pathname).relative_path_from(from_path) 186 | rescue 187 | pathname 188 | end 189 | "`#{path}`" 190 | else 191 | '' 192 | end 193 | end 194 | 195 | # Prints the textual representation of a given set. 196 | # 197 | # @param [Set] set 198 | # the set that should be presented. 199 | # 200 | # @param [Symbol] mode 201 | # the presentation mode, either `:normal` or `:name_and_version`. 202 | # 203 | def pod(set, mode = :normal) 204 | if mode == :name_and_version 205 | puts_indented "#{set.name} #{set.versions.first.version}" 206 | else 207 | pod = Specification::Set::Presenter.new(set) 208 | title = "-> #{pod.name} (#{pod.version})" 209 | if pod.spec.deprecated? 210 | title += " #{pod.deprecation_description}" 211 | colored_title = title.red 212 | else 213 | colored_title = title.green 214 | end 215 | 216 | title(colored_title, '', 1) do 217 | puts_indented pod.summary if pod.summary 218 | puts_indented "pod '#{pod.name}', '~> #{pod.version}'" 219 | labeled('Homepage', pod.homepage) 220 | labeled('Source', pod.source_url) 221 | labeled('Versions', pod.versions_by_source) 222 | if mode == :stats 223 | labeled('Authors', pod.authors) if pod.authors =~ /,/ 224 | labeled('Author', pod.authors) if pod.authors !~ /,/ 225 | labeled('License', pod.license) 226 | labeled('Platform', pod.platform) 227 | labeled('Stars', pod.github_stargazers) 228 | labeled('Forks', pod.github_forks) 229 | end 230 | labeled('Subspecs', pod.subspecs) 231 | end 232 | end 233 | end 234 | 235 | # Prints a message with a label. 236 | # 237 | # @param [String] label 238 | # The label to print. 239 | # 240 | # @param [#to_s] value 241 | # The value to print. 242 | # 243 | # @param [FixNum] justification 244 | # The justification of the label. 245 | # 246 | def labeled(label, value, justification = 12) 247 | if value 248 | title = "- #{label}:" 249 | if value.is_a?(Array) 250 | lines = [wrap_string(title, self.indentation_level)] 251 | value.each do |v| 252 | lines << wrap_string("- #{v}", self.indentation_level + 2) 253 | end 254 | puts lines.join("\n") 255 | else 256 | puts wrap_string(title.ljust(justification) + "#{value}", self.indentation_level) 257 | end 258 | end 259 | end 260 | 261 | # Prints a message respecting the current indentation level and 262 | # wrapping it to the terminal width if necessary. 263 | # 264 | # @param [String] message 265 | # The message to print. 266 | # 267 | def puts_indented(message = '') 268 | indented = wrap_string(message, self.indentation_level) 269 | puts(indented) 270 | end 271 | 272 | # Prints the stored warnings. This method is intended to be called at the 273 | # end of the execution of the binary. 274 | # 275 | # @return [void] 276 | # 277 | def print_warnings 278 | STDOUT.flush 279 | warnings.each do |warning| 280 | next if warning[:verbose_only] && !config.verbose? 281 | STDERR.puts("\n[!] #{warning[:message]}".yellow) 282 | warning[:actions].each do |action| 283 | string = "- #{action}" 284 | string = wrap_string(string, 4) 285 | puts(string) 286 | end 287 | end 288 | end 289 | 290 | # Presents a choice among the elements of an array to the user. 291 | # 292 | # @param [Array<#to_s>] array 293 | # The list of the elements among which the user should make his 294 | # choice. 295 | # 296 | # @param [String] message 297 | # The message to display to the user. 298 | # 299 | # @return [Fixnum] The index of the chosen array item. 300 | # 301 | def choose_from_array(array, message) 302 | array.each_with_index do |item, index| 303 | UI.puts "#{index + 1}: #{item}" 304 | end 305 | 306 | UI.puts message 307 | 308 | index = UI.gets.chomp.to_i - 1 309 | if index < 0 || index > array.count - 1 310 | raise Informative, "#{index + 1} is invalid [1-#{array.count}]" 311 | else 312 | index 313 | end 314 | end 315 | 316 | public 317 | 318 | # @!group Basic methods 319 | #-----------------------------------------------------------------------# 320 | 321 | # prints a message followed by a new line. 322 | # 323 | # @param [String] message 324 | # The message to print. 325 | # 326 | def puts(message = '') 327 | begin 328 | (output_io || STDOUT).puts(message) 329 | rescue Errno::EPIPE 330 | exit 0 331 | end 332 | end 333 | 334 | # prints a message followed by a new line. 335 | # 336 | # @param [String] message 337 | # The message to print. 338 | # 339 | def print(message) 340 | begin 341 | (output_io || STDOUT).print(message) 342 | rescue Errno::EPIPE 343 | exit 0 344 | end 345 | end 346 | 347 | # gets input from $stdin 348 | # 349 | def gets 350 | $stdin.gets 351 | end 352 | 353 | # Stores important warning to the user optionally followed by actions 354 | # that the user should take. To print them use {#print_warnings}. 355 | # 356 | # @param [String] message The message to print. 357 | # @param [Array] actions The actions that the user should take. 358 | # @param [Bool] verbose_only 359 | # Restrict the appearance of the warning to verbose mode only 360 | # 361 | # return [void] 362 | # 363 | def warn(message, actions = [], verbose_only = false) 364 | warnings << { :message => message, :actions => actions, :verbose_only => verbose_only } 365 | end 366 | 367 | # Pipes all output inside given block to a pager. 368 | # 369 | # @yield Code block in which inputs to {#puts} and {#print} methods will be printed to the piper. 370 | # 371 | def with_pager 372 | prev_handler = Signal.trap('INT', 'IGNORE') 373 | IO.popen((ENV['PAGER'] || 'less -R'), 'w') do |io| 374 | UI.output_io = io 375 | yield 376 | end 377 | ensure 378 | Signal.trap('INT', prev_handler) 379 | UI.output_io = nil 380 | end 381 | 382 | private 383 | 384 | # @!group Helpers 385 | #-----------------------------------------------------------------------# 386 | 387 | # @return [String] Wraps a string taking into account the width of the 388 | # terminal and an option indent. Adapted from 389 | # http://blog.macromates.com/2006/wrapping-text-with-regular-expressions/ 390 | # 391 | # @param [String] txt The string to wrap 392 | # 393 | # @param [String] indent The string to use to indent the result. 394 | # 395 | # @return [String] The formatted string. 396 | # 397 | # @note If MetaModel is not being run in a terminal or the width of the 398 | # terminal is too small a width of 80 is assumed. 399 | # 400 | def wrap_string(string, indent = 0) 401 | if disable_wrap 402 | (' ' * indent) + string 403 | else 404 | first_space = ' ' * indent 405 | indented = CLAide::Command::Banner::TextWrapper.wrap_with_indent(string, indent, 9999) 406 | first_space + indented 407 | end 408 | end 409 | end 410 | end 411 | UI = UserInterface 412 | end 413 | -------------------------------------------------------------------------------- /lib/metamodel/version.rb: -------------------------------------------------------------------------------- 1 | module MetaModel 2 | # The version of the MetaModel command line tool. 3 | # 4 | VERSION = '0.4.0' unless defined? MetaModel::VERSION 5 | end 6 | -------------------------------------------------------------------------------- /metamodel.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | require File.expand_path('../lib/metamodel/version', __FILE__) 3 | require 'date' 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "metamodel" 7 | s.version = MetaModel::VERSION 8 | s.date = Date.today 9 | s.license = "MIT" 10 | s.email = ["stark.draven@gmail.com"] 11 | s.homepage = "https://github.com/MModel/MetaModel" 12 | s.authors = ["Draveness Zuo"] 13 | 14 | s.summary = "The Cocoa models generator." 15 | s.description = "Automatically generate model layout for iOS project." 16 | 17 | s.files = Dir["lib/**/*.rb"] + %w{ bin/meta README.md LICENSE } + Dir["lib/**/*.swift"] 18 | 19 | s.executables = %w{ meta } 20 | s.require_paths = %w{ lib } 21 | 22 | s.add_runtime_dependency 'claide', '>= 1.0.0', '< 2.0' 23 | s.add_runtime_dependency 'colored', '~> 1.2' 24 | s.add_runtime_dependency 'xcodeproj', '~> 1.2' 25 | s.add_runtime_dependency 'activesupport', '>= 4.2.6', '< 7.0' 26 | s.add_runtime_dependency "mustache", "~> 1.0" 27 | s.add_runtime_dependency "git", "~> 1.3" 28 | 29 | s.add_development_dependency 'bundler', '~> 1.3' 30 | s.add_development_dependency 'rake', '~> 10.0' 31 | 32 | end 33 | -------------------------------------------------------------------------------- /scaffold/user.rb: -------------------------------------------------------------------------------- 1 | metamodel_version '0.0.1' 2 | 3 | define :User do 4 | # define User model like this 5 | attr :nickname, :string 6 | attr :avatar, :string? 7 | attr :email, :string, :unique, default: "default@gmail.com" 8 | end 9 | --------------------------------------------------------------------------------