Skip to content
This repository has been archived by the owner on Mar 15, 2021. It is now read-only.

Data core #8

Merged
merged 18 commits into from
Feb 21, 2021
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 75 additions & 17 deletions Pym.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

19 changes: 17 additions & 2 deletions Pym/App/Resources/Pym.xcdatamodeld/Pym.xcdatamodel/contents
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17709" systemVersion="20C69" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<elements/>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17709" systemVersion="20D74" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="ActivityModel" representedClassName="PymCore.ActivityModel" syncable="YES">
<attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="name" optional="YES" attributeType="String"/>
<relationship name="entries" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MoodEntryModel" inverseName="activities" inverseEntity="MoodEntryModel"/>
</entity>
<entity name="MoodEntryModel" representedClassName="PymCore.MoodEntryModel" syncable="YES">
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="feelingsValue" attributeType="Integer 16" usesScalarValueType="YES"/>
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="ratingValue" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="activities" toMany="YES" deletionRule="Nullify" destinationEntity="ActivityModel" inverseName="entries" inverseEntity="ActivityModel"/>
</entity>
<elements>
<element name="ActivityModel" positionX="288.4912719726562" positionY="-169.95947265625" width="128" height="88"/>
<element name="MoodEntryModel" positionX="279.6931762695312" positionY="21.792236328125" width="128" height="118"/>
</elements>
</model>
7 changes: 7 additions & 0 deletions Pym/PymCore/Sources/DataSources/ExternalDataSource.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Foundation

public protocol ExternalDataSource {
var sourceName: String { get }

func getEvents(from _: Date, until _: Date) -> [ExternalEvent]
}
7 changes: 7 additions & 0 deletions Pym/PymCore/Sources/DataSources/ExternalEvent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Foundation

public struct ExternalEvent: Identifiable {
public let id: UUID
public let title: String
public let content: String
}
101 changes: 100 additions & 1 deletion Pym/PymCore/Sources/Generated/CoreData.generated.swift
Original file line number Diff line number Diff line change
@@ -1 +1,100 @@
// WARNING: Will be overridden by SwiftGen — https://github.com/SwiftGen/SwiftGen
// swiftlint:disable all
manuelfuchs marked this conversation as resolved.
Show resolved Hide resolved
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen

// swiftlint:disable superfluous_disable_command implicit_return
// swiftlint:disable sorted_imports
import CoreData
import Foundation

// swiftlint:disable attributes file_length vertical_whitespace_closing_braces
// swiftlint:disable identifier_name line_length type_body_length

// MARK: - ActivityModel

public class ActivityModel: NSManagedObject {
public class var entityName: String {
"ActivityModel"
}

public class func entity(in managedObjectContext: NSManagedObjectContext) -> NSEntityDescription? {
NSEntityDescription.entity(forEntityName: entityName, in: managedObjectContext)
}

@available(*, deprecated, renamed: "makeFetchRequest", message: "To avoid collisions with the less concrete method in `NSManagedObject`, please use `makeFetchRequest()` instead.")
@nonobjc public class func fetchRequest() -> NSFetchRequest<ActivityModel> {
NSFetchRequest<ActivityModel>(entityName: entityName)
}

@nonobjc public class func makeFetchRequest() -> NSFetchRequest<ActivityModel> {
NSFetchRequest<ActivityModel>(entityName: entityName)
}

// swiftlint:disable discouraged_optional_boolean discouraged_optional_collection
@NSManaged public var id: UUID?
@NSManaged public var name: String?
@NSManaged public var entries: Set<MoodEntryModel>?
// swiftlint:enable discouraged_optional_boolean discouraged_optional_collection
}

// MARK: Relationship Entries

public extension ActivityModel {
@objc(addEntriesObject:)
@NSManaged func addToEntries(_ value: MoodEntryModel)

@objc(removeEntriesObject:)
@NSManaged func removeFromEntries(_ value: MoodEntryModel)

@objc(addEntries:)
@NSManaged func addToEntries(_ values: Set<MoodEntryModel>)

@objc(removeEntries:)
@NSManaged func removeFromEntries(_ values: Set<MoodEntryModel>)
}

// MARK: - MoodEntryModel

public class MoodEntryModel: NSManagedObject {
public class var entityName: String {
"MoodEntryModel"
}

public class func entity(in managedObjectContext: NSManagedObjectContext) -> NSEntityDescription? {
NSEntityDescription.entity(forEntityName: entityName, in: managedObjectContext)
}

@available(*, deprecated, renamed: "makeFetchRequest", message: "To avoid collisions with the less concrete method in `NSManagedObject`, please use `makeFetchRequest()` instead.")
@nonobjc public class func fetchRequest() -> NSFetchRequest<MoodEntryModel> {
NSFetchRequest<MoodEntryModel>(entityName: entityName)
}

@nonobjc public class func makeFetchRequest() -> NSFetchRequest<MoodEntryModel> {
NSFetchRequest<MoodEntryModel>(entityName: entityName)
}

// swiftlint:disable discouraged_optional_boolean discouraged_optional_collection
@NSManaged public var date: Date
@NSManaged public var feelingsValue: Int16
@NSManaged public var id: UUID
@NSManaged public var ratingValue: Int16
@NSManaged public var activities: Set<ActivityModel>
// swiftlint:enable discouraged_optional_boolean discouraged_optional_collection
}

// MARK: Relationship Activities

public extension MoodEntryModel {
@objc(addActivitiesObject:)
@NSManaged func addToActivities(_ value: ActivityModel)

@objc(removeActivitiesObject:)
@NSManaged func removeFromActivities(_ value: ActivityModel)

@objc(addActivities:)
@NSManaged func addToActivities(_ values: Set<ActivityModel>)

@objc(removeActivities:)
@NSManaged func removeFromActivities(_ values: Set<ActivityModel>)
}

// swiftlint:enable identifier_name line_length type_body_length
30 changes: 30 additions & 0 deletions Pym/PymCore/Sources/Persistence/DTOs/MoodEntry.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Foundation

public class MoodEntry {
public var id: UUID?
public let date: Date
public let rating: MoodRating
public let feelings: [Feeling]
public let activities: [String]

init(date: Date, rating: MoodRating, feelings: [Feeling], activities: [String]) {
self.date = date
self.rating = rating
self.feelings = feelings
self.activities = activities
}

convenience init(id: UUID, date: Date, rating: MoodRating, feelings: [Feeling], activities: [String]) {
manuelfuchs marked this conversation as resolved.
Show resolved Hide resolved
self.init(date: date, rating: rating, feelings: feelings, activities: activities)
self.id = id
}

convenience init(from model: MoodEntryModel) {
self.init(date: model.date,
rating: model.rating,
feelings: model.feelings,
activities: model.activities.filter { $0.name != nil }
.map { $0.name! })
manuelfuchs marked this conversation as resolved.
Show resolved Hide resolved
id = model.id
}
}
60 changes: 60 additions & 0 deletions Pym/PymCore/Sources/Persistence/DataAccessController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import CoreData

public struct DataAccessController {
public static let shared = DataAccessController()
static var persistenceController = PersistenceController.shared

private init() {}

public func store(entry dto: MoodEntry) {
let context = DataAccessController.persistenceController.container.viewContext
let model = MoodEntryModel(context: context)
model.id = UUID()
model.date = dto.date
model.feelings = dto.feelings
model.rating = dto.rating
dto.activities.forEach { activityName in
var activity = self.getActivityBy(name: activityName)
if activity == nil {
activity = ActivityModel(context: context)
activity!.name = activityName
}
model.addToActivities(activity!)
}

DataAccessController.persistenceController.saveContext()

dto.id = model.id
}

public func getEntries(from startDate: Date, until endDate: Date) -> [MoodEntry] {
let context = DataAccessController.persistenceController.container.viewContext

let request = NSFetchRequest<MoodEntryModel>(entityName: MoodEntryModel.entityName)
request.returnsObjectsAsFaults = false
request.predicate = NSPredicate(format: "(date >= %@) AND (date <= %@)", startDate as NSDate, endDate as NSDate)

request.sortDescriptors = [NSSortDescriptor(key: "id", ascending: false)]
if let results = try? context.fetch(request) {
return results.map { MoodEntry(from: $0) }
} else {
return []
}
}

private func getActivityBy(name activityName: String) -> ActivityModel? {
let context = DataAccessController.persistenceController.container.viewContext

let request = NSFetchRequest<ActivityModel>(entityName: ActivityModel.entityName)
request.predicate = NSPredicate(format: "name = '\(activityName)'")
request.fetchLimit = 1

if let results = try? context.fetch(request),
results.count == 1
{
manuelfuchs marked this conversation as resolved.
Show resolved Hide resolved
return results.first
} else {
return nil
}
}
}
30 changes: 30 additions & 0 deletions Pym/PymCore/Sources/Persistence/Enums.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Foundation
manuelfuchs marked this conversation as resolved.
Show resolved Hide resolved

public enum MoodRating: Int16 {
case bad = 1
case poor = 2
manuelfuchs marked this conversation as resolved.
Show resolved Hide resolved
case moderate = 3
case good = 4
case great = 5
}

public enum Feeling: Int, CaseIterable, Comparable {
case angry = 1
case upset = 2
case irritated = 4
case clear = 8
case curious = 16
case enthusiastic = 32
case happy = 64
case relaxed = 128
case scared = 256
case anxious = 512
case worried = 1028
case amazed = 2056
case surprised = 4112
case confused = 8224

public static func < (lhs: Feeling, rhs: Feeling) -> Bool {
lhs.rawValue < rhs.rawValue
}
}
35 changes: 35 additions & 0 deletions Pym/PymCore/Sources/Persistence/Models/ModelExtensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import CoreData
manuelfuchs marked this conversation as resolved.
Show resolved Hide resolved
import Foundation

public extension MoodEntryModel {
var rating: MoodRating {
get {
MoodRating(rawValue: ratingValue)!
}
set(newRating) {
ratingValue = newRating.rawValue
}
}

var feelings: [Feeling] {
get {
var feelingsArr: [Feeling] = []

var remainingFeelingsValue = feelingsValue
Feeling.allCases
manuelfuchs marked this conversation as resolved.
Show resolved Hide resolved
.reduce(into: [Feeling]()) { copiedArray, feeling in copiedArray.append(feeling) }
.sorted { (feeling1, feeling2) -> Bool in feeling1.rawValue > feeling2.rawValue }
manuelfuchs marked this conversation as resolved.
Show resolved Hide resolved
.forEach {
if $0.rawValue <= remainingFeelingsValue {
remainingFeelingsValue -= Int16($0.rawValue)
feelingsArr.append($0)
}
}

return feelingsArr
}
set(newFeelings) {
feelingsValue = Int16(newFeelings.reduce(0) { $0 + $1.rawValue })
}
}
}
12 changes: 12 additions & 0 deletions Pym/PymCore/Sources/Persistence/PersistenceController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,16 @@ struct PersistenceController {
}
})
}

public func saveContext() {
let context = container.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
manuelfuchs marked this conversation as resolved.
Show resolved Hide resolved
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}
33 changes: 33 additions & 0 deletions Pym/PymCore/Tests/DataAccessControllerTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// DataAccessControllerTests.swift
// PymTests
//
// Created by Manuel Fuchs on 18.02.21.
//

@testable import PymCore
import XCTest

class DataAccessControllerTests: XCTestCase {
override func setUpWithError() throws {
DataAccessController.persistenceController = PersistenceController.preview
}

override func tearDownWithError() throws {}

func testStoreEntryTest() throws {
let exptectedEntry = MoodEntry(date: Date(), rating: .bad, feelings: [.angry, .confused], activities: ["tennis", "programming"])
manuelfuchs marked this conversation as resolved.
Show resolved Hide resolved
DataAccessController.shared.store(entry: exptectedEntry)

let actualEntries = DataAccessController.shared.getEntries(from: Date() - 10 * 60, until: Date() + 10 * 60)
XCTAssertEqual(1, actualEntries.count)

let actualEntry = actualEntries.first
XCTAssertNotNil(actualEntry)
XCTAssertEqual(exptectedEntry.activities.sorted(), actualEntry?.activities.sorted())
XCTAssertEqual(exptectedEntry.date, actualEntry?.date)
XCTAssertEqual(exptectedEntry.feelings.sorted(), actualEntry?.feelings.sorted())
XCTAssertEqual(exptectedEntry.id, actualEntry?.id)
XCTAssertEqual(exptectedEntry.rating, actualEntry?.rating)
}
}
44 changes: 44 additions & 0 deletions Pym/PymCore/Tests/ModelTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import CoreData
@testable import PymCore
import XCTest

class ModelTests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}

override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}

func testFeelingArrayResolve1() throws {
let entry = MoodEntryModel(context: PersistenceController.preview.container.viewContext)

entry.feelingsValue = 30
let expectedFeelings: [Feeling] = [.upset, .irritated, .clear, .curious]
let actualFeelings = entry.feelings
XCTAssertEqual(expectedFeelings.count, actualFeelings.count)
XCTAssertEqual(expectedFeelings.sorted(), actualFeelings.sorted())
}

func testFeelingArrayResolve2() throws {
let entry = MoodEntryModel(context: PersistenceController.preview.container.viewContext)

let expectedFeelings: [Feeling] = [.angry, .enthusiastic, .worried, .irritated]
entry.feelings = expectedFeelings
let actualFeelings = entry.feelings

XCTAssertEqual(expectedFeelings.count, actualFeelings.count)
XCTAssertEqual(expectedFeelings.sorted(), actualFeelings.sorted())
}

func testRatingResolve() throws {
let entry = MoodEntryModel(context: PersistenceController.preview.container.viewContext)

for expectedRating in [MoodRating.bad, MoodRating.good, MoodRating.great, MoodRating.moderate, MoodRating.poor] {
entry.rating = expectedRating
XCTAssertEqual(expectedRating.rawValue, entry.ratingValue)
XCTAssertEqual(expectedRating, entry.rating)
}
}
}
Loading