Skip to content
This repository has been archived by the owner on Jul 8, 2019. It is now read-only.

Commit

Permalink
Browse files Browse the repository at this point in the history
Initial Commit
  • Loading branch information
mattt committed Jul 24, 2012
0 parents commit 459becf
Show file tree
Hide file tree
Showing 17 changed files with 460 additions and 0 deletions.
3 changes: 3 additions & 0 deletions Gemfile
@@ -0,0 +1,3 @@
source :rubygems

gemspec
19 changes: 19 additions & 0 deletions LICENSE
@@ -0,0 +1,19 @@
Copyright (c) 2012 Mattt Thompson (http://mattt.me/)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
52 changes: 52 additions & 0 deletions README.md
@@ -0,0 +1,52 @@
# Rack::CoreData
**Automatically generate REST APIs for Core Data models**

> This is still in early stages of development, so proceed with caution when using this in a production application. Any bug reports, feature requests, or general feedback at this point would be greatly appreciated.
Building web services for iOS apps is a constant struggle to coordinating data models. You're _probably_ not running Objective-C on the server, so you're stuck duplicating your business logic--allthewhile doing your best to maintain the correct conventions and idioms for each platform.

`Rack::CoreData` aims to bridge the client/server divide, and save you time.

Simply point `Rack::CoreData` at your Core Data model file, and a RESTful webserive is automatically created for you, with all of the resource endpoints you might expect in Rails.

Since we're running on Rack, each endpoint can be overriden if you need to add or change any existing behavior. Likewise, any of the models in the application can be re-opened to make any necessary adjustments.

Think of it like an API scaffold: while you may well throw all of it away eventually, having something to start with will allow you to iterate on the most important parts of your application while you're the most excited about them.

## Usage

### Gemfile

```Ruby
$ gem 'rack-core-data', :require => 'rack/core-data'
```

### config.ru

```ruby
require 'bundler'
Bundler.require

# Rack::CoreData requires a Sequel connection to a database
DB = Sequel.connect(ENV['DATABASE_URL'] || "postgres://localhost:5432/coredata")

run Rack::CoreData('./Example.xcdatamodeld')
```

## Examples

An example web API using a Core Data model can be found the `/example` directory.

It uses the same data model as the [AFIncrementalStore](https://github.com/afnetworking/afincrementalStore/) example iOS project, so try running that against Rack::CoreData running on localhost.

## Contact

Mattt Thompson

- http://github.com/mattt
- http://twitter.com/mattt
- m@mattt.me

## License

Rack::CoreData is available under the MIT license. See the LICENSE file for more info.
10 changes: 10 additions & 0 deletions Rakefile
@@ -0,0 +1,10 @@
require "bundler"
Bundler.setup

gemspec = eval(File.read("rack-core-data.gemspec"))

task :build => "#{gemspec.full_name}.gem"

file "#{gemspec.full_name}.gem" => gemspec.files + ["rack-core-data.gemspec"] do
system "gem build rack-core-data.gemspec"
end
8 changes: 8 additions & 0 deletions example/Example.xcdatamodeld/.xccurrentversion
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>IncrementalStoreExample.xcdatamodel</string>
</dict>
</plist>
16 changes: 16 additions & 0 deletions example/Example.xcdatamodeld/Example.xcdatamodel/contents
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model name="" userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="1487" systemVersion="12A269" minimumToolsVersion="Automatic" macOSVersion="Automatic" iOSVersion="Automatic">
<entity name="Artist" syncable="YES">
<attribute name="artistDescription" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="songs" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="Song" inverseName="artist" inverseEntity="Song" syncable="YES"/>
</entity>
<entity name="Song" syncable="YES">
<attribute name="title" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="artist" optional="YES" minCount="1" maxCount="1" deletionRule="Nullify" destinationEntity="Artist" inverseName="songs" inverseEntity="Artist" syncable="YES"/>
</entity>
<elements>
<element name="Artist" positionX="-567" positionY="27" width="128" height="90"/>
<element name="Song" positionX="-254" positionY="144" width="128" height="75"/>
</elements>
</model>
6 changes: 6 additions & 0 deletions example/Gemfile
@@ -0,0 +1,6 @@
source :rubygems

gem 'rack-core-data', :require => 'rack/core-data', :path => File.join(__FILE__, "../..")

gem 'thin'
gem 'pg'
1 change: 1 addition & 0 deletions example/Procfile
@@ -0,0 +1 @@
web: bundle exec thin start -p $PORT
13 changes: 13 additions & 0 deletions example/README.md
@@ -0,0 +1,13 @@
# Rack::CoreData Example

## Instructions

To run the example application, ensure that you have Postgres running locally (see [Postgres.app](http://postgresapp.com) for an easy way to get set up on a Mac), and run the following commands:

```sh
$ cd example
$ psql -c "CREATE DATABASE core_data_example;"
$ echo "DATABASE_URL=postgres://localhost:5432/core_data_example" > .env
$ bundle
$ foreman start
```
29 changes: 29 additions & 0 deletions example/config.ru
@@ -0,0 +1,29 @@
require 'bundler'
Bundler.require

DB = Sequel.connect(ENV['DATABASE_URL'] || "postgres://localhost:5432/coredata")

run Rack::CoreData('./Example.xcdatamodeld')

# Seed data if no records currently exist
if Rack::CoreData::Models::Artist.count == 0
SONGS_BY_ARTIST = {
"Ratatat"=> ["Shiller", "Falcon Jab", "Mi Viejo", "Mirando", "Flynn", "Bird-Priest", "Shempi", "Imperials", "Dura", "Brulee", "Mumtaz Khan", "Gipsy Threat", "Black Heroes"],
"Phoenix"=>["Lisztomania", "1901", "Fences", "Love Like A Sunset", "Lasso", "Rome", "Countdown (Sick For The Big Sun)", "Girlfriend", "Armistice"],
"Hot Chip"=>["Thieves In The Night", "Hand Me Down Your Love", "I Feel Better", "One Life Stand", "Brothers", "Slush", "Alley Cats", "We Have Love", "Keep Quiet", "Take It In"],
"Fleet Foxes"=>["Sun It Rises", "White Winter Hymnal", "Ragged Wood", "Tiger Mountain Peasant Song", "Quiet Houses", "He Doesnt Know Why", "Heard Them Stirring", "Your Protector", "Meadowlarks", "Blue Ridge Mountains", "Oliver James"],
"Grizzly Bear"=>["Southern Point", "Two Weeks", "All We Ask", "Fine For Now", "Cheerleader", "Dory", "Ready, Able", "About Face", "Hold Still", "While You Wait for the Others", "I live With You", "Foreground"],
"Cold War Kids"=>["Against Privacy", "Mexican Dogs", "Every Valley Is Not a Lake", "Something Is Not Right with Me", "Welcome to the Occupation", "Golden Gate Jumpers", "Avalanche in B", "I've Seen Enough", "Every Man I Fall For", "Dreams Old Men Dream", "On the Night My Love Broke Through", "Relief", "Cryptomnesia"],
"Two Door Cinema Club"=>["Cigarettes in the Theatre", "Come Back Home", "Do You Want It All", "This is the Life", "Something Good Can Work", "I Can Talk", "Undercover Martyn", "What You Know", "Eat That up, It's Good for You", "You're Not Stubborn"],
"Wild Beasts"=>["The Fun Powder Plot", "Hooting & Howling", "All The Kings Men", "When I'm Sleepy", "We Still Got The Taste Dancing On Our Tongues", "Two Dancers", "Two Dancers II", "This Is Our Lot", "Underbelly", "The Empty Nest"],
"Janelle Monae"=>["The March Of The Wolfmasters", "Violet Stars Happy Hunting!!!", "Many Moons", "Cybertronic Purgatory", "Sinceraly, Jane.", "Mr. President", "Smile"],
"Bibio"=>["Ambivalence Avenue", "Jealous Of Roses", "All The Flowers", "Fire Ant", "Haikuesque (When She Laughs)", "Sugarette", "Lovers Carvings", "Abrasion", "S'Vive", "The Palm Of Your Wave", "Cry ! Baby !", "Dwrcan"]
}

SONGS_BY_ARTIST.each do |artist, songs|
artist = Rack::CoreData::Models::Artist.create(name: artist, artistDescription: "Lorem ipsum dolar sit amet")
songs.each do |song|
Rack::CoreData::Models::Song.create(artist: artist, title: song)
end
end
end
147 changes: 147 additions & 0 deletions lib/rack/core-data.rb
@@ -0,0 +1,147 @@
require 'rack'
require 'sinatra/base'

require 'sequel'
require 'active_support/inflector'

require 'rack/core-data/data_model'
require 'rack/core-data/version'

module Rack::CoreData::Models
end

module Rack
def self.CoreData(xcdatamodel)
app = Class.new(Sinatra::Base) do
before do
content_type :json
end
end

model = CoreData::DataModel.new(xcdatamodel)

# Create each model class before implementing, in order to correctly set up relationships
model.entities.each do |entity|
klass = Rack::CoreData::Models.const_set(entity.name.capitalize, Class.new(Sequel::Model))
end

model.entities.each do |entity|
klass = Rack::CoreData::Models.const_get(entity.name.capitalize)
klass.dataset = entity.name.downcase.pluralize.to_sym

klass.class_eval do
strict_param_setting = false
plugin :json_serializer, :naked => true, :include => :url, :except => :id
plugin :schema

def url
"/#{self.class.table_name}/#{id}"
end

entity.relationships.each do |relationship|
options = {:class => Rack::CoreData::Models.const_get(relationship.destination.capitalize)}

if relationship.to_many?
one_to_many relationship.name.to_sym, options
else
many_to_one relationship.name.to_sym, options
end
end

set_schema do
primary_key :id

entity.attributes.each do |attribute|
options = {
:null => attribute.optional?,
:index => attribute.indexed?,
:default => attribute.default_value
}

type = case attribute.type
when "Integer 16" then :int2
when "Integer 32" then :int4
when "Integer 64" then :int8
when "Float" then :float4
when "Decimal" then :float8
when "Date" then :timestamp
when "Boolean" then :boolean
when "Binary" then :bytea
else :varchar
end

column attribute.name, type, options
end

entity.relationships.each do |relationship|
options = {
:index => true,
:null => relationship.optional?
}

if not relationship.to_many?
column "#{relationship.name}_id".to_sym, :integer, options
end
end
end

create_table unless table_exists?
end

app.class_eval do
include Rack::CoreData::Models
klass = Rack::CoreData::Models.const_get(entity.name.capitalize)

get "/#{entity.name.downcase.pluralize}/?" do
klass.all.to_json
end

post "/#{entity.name.downcase.pluralize}/?" do
record = klass.new(params)
if record.save
status 201
record.to_json
else
status 406
record.errors.to_json
end
end

get "/#{entity.name.downcase.pluralize}/:id/?" do
klass[params[:id]].to_json
end

put "/#{entity.name.downcase.pluralize}/:id/?" do
record = klass[params[:id]] or halt 404
if record.update(params)
status 200
record.to_json
else
status 406
record.errors.to_json
end
end

delete "/#{entity.name.downcase.pluralize}/:id/?" do
record = klass[params[:id]] or halt 404
if record.destroy
status 200
else
status 406
record.errors.to_json
end
end

entity.relationships.each do |relationship|
next unless relationship.to_many?

get "/#{entity.name.downcase.pluralize}/:id/#{relationship.name}/?" do
klass[params[:id]].send(relationship.name).to_json
end
end
end
end

return app
end
end
36 changes: 36 additions & 0 deletions lib/rack/core-data/data_model.rb
@@ -0,0 +1,36 @@
require 'nokogiri'

module Rack::CoreData
class DataModel
attr_reader :name, :version, :entities

def initialize(data_model)
loop do
case data_model
when File, /^\<\?xml/
data_model = ::Nokogiri::XML(data_model) and redo
when String
case data_model
when /\.xcdatamodeld?$/
data_model = Dir[File.join(data_model, "/**/contents")].first and redo
else
data_model = ::File.read(data_model) and redo
end
when ::Nokogiri::XML::Document
break
else
raise ArgumentError
end
end

model = data_model.at_xpath('model')
@name = model['name']
@version = model['systemVersion']
@entities = model.xpath('entity').collect{|element| Entity.new(element)}
end
end
end

require 'rack/core-data/data_model/entity'
require 'rack/core-data/data_model/attribute'
require 'rack/core-data/data_model/relationship'
33 changes: 33 additions & 0 deletions lib/rack/core-data/data_model/attribute.rb
@@ -0,0 +1,33 @@
class Rack::CoreData::DataModel
class Attribute
attr_reader :name, :type, :identifier, :version_hash_modifier, :default_value

def initialize(attribute)
raise ArgumentError unless ::Nokogiri::XML::Element === attribute

@name = attribute['name']
@type = attribute['attributeType']
@identifier = attribute['elementID']
@version_hash_modifier = attribute['versionHashModifier']
@default_value = case @type
when "Integer 16", "Integer 32", "Integer 64"
attribute['defaultValueString'].to_i
when "Float", "Decimal"
attribute['defaultValueString'].to_f
end if attribute['defaultValueString']

@optional = attribute['optional'] == "YES"
@transient = attribute['transient'] == "YES"
@indexed = attribute['indexed'] == "YES"
@syncable = attribute['syncable'] == "YES"
end

def to_s
@name
end

[:optional, :transient, :indexed, :syncable].each do |symbol|
define_method("#{symbol}?") {!!instance_variable_get(("@#{symbol}").intern)}
end
end
end

0 comments on commit 459becf

Please sign in to comment.